package main import ( "bufio" "context" "crypto/rand" "encoding/binary" "encoding/hex" "encoding/json" "encoding/xml" "errors" "fmt" "io" "math/big" "net" "net/http" "os" "os/signal" "strconv" "syscall" "time" rooteway "git.loyso.art/frx/eway" "git.loyso.art/frx/eway/cmd/cli/components" "git.loyso.art/frx/eway/internal/crypto" "git.loyso.art/frx/eway/internal/entity" "git.loyso.art/frx/eway/internal/export" "git.loyso.art/frx/eway/internal/interconnect/eway" "github.com/rodaine/table" "github.com/rs/zerolog" "github.com/urfave/cli/v3" ) func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer func() { cancel() }() err := runcli(ctx) if err != nil { fmt.Fprintf(os.Stderr, "unable to handle app: %v\n", err) os.Exit(1) } os.Exit(0) } func runcli(ctx context.Context) (err error) { app := setupCLI() return app.Run(ctx, os.Args) } func setupDI() cli.BeforeFunc { return func(ctx context.Context, cmd *cli.Command) error { cfgpath := cmd.String("config") debugLevel := cmd.Bool("verbose") jsonFormat := cmd.Bool("json") err := components.SetupDI(ctx, cfgpath, debugLevel, jsonFormat) if err != nil { return fmt.Errorf("setting up di: %w", err) } return nil } } func releaseDI() cli.AfterFunc { return func(ctx context.Context, c *cli.Command) error { return components.Shutdown() } } func setupCLI() *cli.Command { app := &cli.Command{ Name: "ewaycli", Description: "a cli for running eway logic", Version: fmt.Sprintf("%s (%s) %s", rooteway.Version(), rooteway.Commit(), rooteway.BuildTime()), Flags: []cli.Flag{ &cli.StringFlag{ Name: "config", Usage: "path to config in TOML format", Value: "config.toml", TakesFile: true, }, &cli.BoolFlag{ Name: "verbose", Aliases: []string{"d"}, Usage: "enables verbose logging", }, &cli.BoolFlag{ Name: "json", Usage: "enables json log format", Persistent: true, }, }, Before: setupDI(), After: releaseDI(), Commands: []*cli.Command{ newAppCmd(), newCryptoCmd(), newParseCmd(), newImportCmd(), newExportCmd(), newViewCmd(), }, } return app } func newCryptoCmd() *cli.Command { return &cli.Command{ Name: "crypto", Usage: "methods for encrypt/decrypt various things", Commands: []*cli.Command{ newCryptoEncyptCmd(), newCryptoDecryptCmd(), }, } } func newCryptoEncyptCmd() *cli.Command { return &cli.Command{ Name: "encrypt", Usage: "encypt incoming text", Flags: []cli.Flag{ &cli.StringFlag{ Name: "text", Aliases: []string{"t"}, Required: true, }, }, Action: cryptoDeEncryptAction(true), } } func newAppCmd() *cli.Command { return &cli.Command{ Name: "app", Usage: "commands for running different applications", Commands: []*cli.Command{ newAppYmlExporter(), }, } } func newAppYmlExporter() *cli.Command { return &cli.Command{ Name: "ymlexporter", Usage: "server on given port a http api for accessing yml catalog", Flags: []cli.Flag{ &cli.IntFlag{ Name: "port", Usage: "Port for accepting connections", Value: 9448, Validator: func(i int64) error { if i > 1<<16 || i == 0 { return cli.Exit("bad port value, allowed values should not exceed 65535", 1) } return nil }, }, &cli.StringFlag{ Name: "src", TakesFile: true, Usage: "Source to catalog. Should be a valid xml file", Value: "yml_catalog.xml", Validator: func(s string) error { _, err := os.Stat(s) if err != nil { return err } return nil }, }, }, Action: appYMLExporterAction, } } func newParseCmd() *cli.Command { return &cli.Command{ Name: "parse", Usage: "category for parsing items from various sources", Commands: []*cli.Command{ newParseEwayCmd(), }, } } func newParseEwayCmd() *cli.Command { return &cli.Command{ Name: "eway", Usage: "parse all available eway goods", Commands: []*cli.Command{ newParseEwayGetCmd(), newParseEwayListCmd(), newParseEwayDumpCmd(), }, } } func newParseEwayGetCmd() *cli.Command { return &cli.Command{ Name: "get", Usage: "loads information about the product by parsing product's html page", Flags: []cli.Flag{ &cli.IntFlag{ Name: "cart", Aliases: []string{"c"}, Usage: "cart of the product", Required: true, }, }, Action: decorateAction(parseEwayGetAction), } } func newParseEwayListCmd() *cli.Command { return &cli.Command{ Name: "list", Usage: "parse all available eway goods", Flags: []cli.Flag{ &cli.IntFlag{ Name: "page", Usage: "choose page to load", Value: 1, }, &cli.IntFlag{ Name: "limit", Usage: "limits output", Value: 100, }, &cli.IntFlag{ Name: "min-stock", Usage: "filters by minimum available items in stock", }, &cli.BoolFlag{ Name: "pretty", Usage: "pretty prints output", }, }, Action: decorateAction(parseEwayListAction), } } func newParseEwayDumpCmd() *cli.Command { return &cli.Command{ Name: "dump", Usage: "dumps content of eway catalog inside db", Action: decorateAction(parseEwayDumpAction), } } func newImportCmd() *cli.Command { return &cli.Command{ Name: "import", Usage: "category for importing data from sources", Commands: []*cli.Command{ newImportFromFileCmd(), }, } } func newImportFromFileCmd() *cli.Command { return &cli.Command{ Name: "file", Usage: "imports from file into db", Flags: []cli.Flag{ &cli.StringFlag{ Name: "src", Value: "", Usage: "source of the data.", Required: true, }, }, Action: decorateAction(importFromFileAction), } } func newExportCmd() *cli.Command { return &cli.Command{ Name: "export", Usage: "category for exporting stored data", Commands: []*cli.Command{ newExportYMLCatalogCmd(), }, } } func newExportYMLCatalogCmd() *cli.Command { return &cli.Command{ Name: "yml_catalog", Usage: "export data into as yml_catalog in xml format", Flags: []cli.Flag{ &cli.StringFlag{ Name: "out", Aliases: []string{"o"}, Usage: "destination path", Value: "yml_catalog.xml", TakesFile: true, }, &cli.IntFlag{ Name: "limit", Usage: "limits output offers to required value", }, &cli.BoolFlag{ Name: "pretty", Usage: "prettify output", }, }, Action: decorateAction(exportYMLCatalogAction), } } func newViewCmd() *cli.Command { return &cli.Command{ Name: "view", Usage: "Set of commands to view the data inside db", Commands: []*cli.Command{ newViewCategoriesCmd(), newViewItemsCmd(), }, } } func newViewCategoriesCmd() *cli.Command { return &cli.Command{ Name: "categories", Usage: "Set of commands to work with categories", Commands: []*cli.Command{ newViewCategoriesListCmd(), }, } } func newViewItemsCmd() *cli.Command { return &cli.Command{ Name: "items", Usage: "Set of command to work with items", Commands: []*cli.Command{ newViewItemsGetCmd(), newViewItemsCountCmd(), }, } } func newViewItemsCountCmd() *cli.Command { return &cli.Command{ Name: "count", Usage: "iterates over collection and counts number of items", Action: decorateAction(viewItemsCountAction), } } func newViewItemsGetCmd() *cli.Command { return &cli.Command{ Name: "get", Usage: "gets goods item by its id", Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", Usage: "id of the goods item. Either id or cart-id should be set", }, &cli.IntFlag{ Name: "cart-id", Usage: "cart-id of the item. Either cart-id or id should be set", }, }, Action: decorateAction(viewItemsGetAction), } } func newViewCategoriesListCmd() *cli.Command { return &cli.Command{ Name: "list", Usage: "lists stored categories stored in database", Flags: []cli.Flag{ &cli.IntFlag{ Name: "limit", Usage: "limits output to selected items", Value: 20, }, &cli.IntFlag{ Name: "page", Usage: "in case of limit, selects page", Value: 0, }, &cli.BoolFlag{ Name: "with-total", Usage: "prints total count of categories", }, }, Action: decorateAction(viewCategoriesListAction), } } func viewItemsGetAction(ctx context.Context, c *cli.Command) error { r, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } id := c.String("id") cartID := c.Int("cart-id") if id == "" && cartID == 0 { return cli.Exit("oneof: id or cart-id should be set", 1) } else if id != "" && cartID != 0 { return cli.Exit("oneof: id or cart-id should be set", 1) } var item entity.GoodsItem if id != "" { item, err = r.GoodsItem().Get(ctx, id) } else { item, err = r.GoodsItem().GetByCart(ctx, cartID) } if err != nil { return fmt.Errorf("getting item: %w", err) } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") err = enc.Encode(item) if err != nil { return fmt.Errorf("encoding item: %w", err) } return nil } func viewItemsCountAction(ctx context.Context, c *cli.Command) error { r, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } itemChan, err := r.GoodsItem().ListIter(ctx, 10) if err != nil { if !errors.Is(err, entity.ErrNotImplemented) { return fmt.Errorf("getting list iter: %w", err) } } var count int if err == nil { var done bool for !done { select { case _, ok := <-itemChan: if !ok { done = true continue } count++ case <-ctx.Done(): return ctx.Err() } } } items, err := r.GoodsItem().List(ctx) if err != nil { return fmt.Errorf("getting list: %w", err) } zerolog.Ctx(ctx).Info().Int("count", count).Int("list_count", len(items)).Msg("read all items") return nil } func viewCategoriesListAction(ctx context.Context, c *cli.Command) error { r, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } categories, err := r.Category().List(ctx) if err != nil { return fmt.Errorf("listing categories: %w", err) } limit := int(c.Int("limit")) page := int(c.Int("page")) total := len(categories) if page == 0 { page = 1 } if limit > 0 { offset := (page - 1) * limit if offset > len(categories) { offset = len(categories) - 1 } limit = offset + limit if limit > len(categories) { limit = len(categories) } categories = categories[offset:limit] } tbl := table.New("ID", "Name") for _, category := range categories { if category.ID == 0 && category.Name == "" { continue } tbl.AddRow(category.ID, category.Name) } tbl.Print() if c.Bool("with-total") { zerolog.Ctx(ctx).Info().Int("count", total).Msg("total categories stats") } return nil } func importFromFileAction(ctx context.Context, c *cli.Command) error { const maxBatch = 2000 r, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } filesrc := c.String("src") log := zerolog.Ctx(ctx) log.Debug().Str("filepath", filesrc).Msg("importing data from file") productsFile, err := os.Open(filesrc) if err != nil { return fmt.Errorf("opening file: %w", err) } defer func() { errClose := productsFile.Close() if errClose != nil { log.Warn().Err(errClose).Msg("unable to close file") } }() failedItems, err := os.Create("failed.json") if err != nil { log.Warn().Err(err).Msg("unable to open file for failed results") failedItems = os.Stdout } defer func() { if failedItems == os.Stdout { return } errClose := failedItems.Close() log.Err(errClose).Msg("closing file") }() var ( goodsItem entity.GoodsItem goodsItems []entity.GoodsItem failedToInsert int ) seenCategories := make(map[string]struct{}) categories, err := r.Category().List(ctx) if err != nil { return fmt.Errorf("listing categories: %w", err) } for _, category := range categories { seenCategories[category.Name] = struct{}{} } bfile := bufio.NewReader(productsFile) for { line, _, err := bfile.ReadLine() if err != nil { if errors.Is(err, io.EOF) { break } return fmt.Errorf("reading line: %w", err) } err = json.Unmarshal(line, &goodsItem) if err != nil { log.Warn().Err(err).Str("line", string(line)).Msg("unable to unmarshal line into item") _, _ = failedItems.Write(line) _, _ = failedItems.Write([]byte{'\n'}) failedToInsert++ continue } goodsItems = append(goodsItems, goodsItem) if _, ok := seenCategories[goodsItem.Type]; ok { continue } if goodsItem.Type == "" { log.Warn().Msg("bad item without proper type") _ = json.NewEncoder(failedItems).Encode(goodsItem) continue } _, err = r.Category().Create(ctx, goodsItem.Type) if err != nil { return fmt.Errorf("unable to create new category: %w", err) } log.Debug().Any("category", goodsItem.Type).Msg("inserted new category") seenCategories[goodsItem.Type] = struct{}{} } log.Debug().Int("count", len(goodsItems)).Int("failed", failedToInsert).Msg("preparing to upload") start := time.Now() batchSize := int(maxBatch) for i := 0; i < len(goodsItems); i += batchSize { to := i + batchSize if to > len(goodsItems) { to = len(goodsItems) } _, err = r.GoodsItem().UpsertMany(ctx, goodsItems[i:to]...) if err != nil { return fmt.Errorf("upserting items: %w", err) } log.Debug().Int("count", to-i).Msg("inserted batch") } log.Debug().Dur("elapsed", time.Since(start)).Msg("upload finished") return nil } type action func(ctx context.Context, c *cli.Command) error func decorateAction(a action) cli.ActionFunc { return func(ctx context.Context, c *cli.Command) error { var data [3]byte _, _ = rand.Read(data[:]) reqid := hex.EncodeToString(data[:]) log, err := components.GetLogger() if err != nil { return fmt.Errorf("getting logger: %w", err) } log = log.With().Str("reqid", reqid).Logger() ctx = log.WithContext(ctx) start := time.Now() defer func() { log.Info().Float64("elapsed", time.Since(start).Seconds()).Msg("command completed") }() log.Info().Msg("command execution started") return a(ctx, c) } } func exportYMLCatalogAction(ctx context.Context, c *cli.Command) error { path := c.String("out") limit := c.Int("limit") pretty := c.Bool("pretty") log, err := components.GetLogger() if err != nil { return fmt.Errorf("getting logger: %w", err) } r, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } categories, err := r.Category().List(ctx) if err != nil { return fmt.Errorf("listing categories: %w", err) } shop := export.Shop{ Currencies: []export.Currency{{ ID: "RUR", Rate: 1, }}, Categories: make([]export.Category, 0, len(categories)), } categoryByNameIdx := make(map[string]int64, len(categories)) for _, category := range categories { categoryByNameIdx[category.Name] = category.ID shop.Categories = append(shop.Categories, export.Category{ ID: category.ID, Name: category.Name, }) } itemsIter, err := r.GoodsItem().ListIter(ctx, 1) if err != nil { return fmt.Errorf("getting items iterator: %w", err) } for item := range itemsIter { offer := goodsItemAsOffer(item, categoryByNameIdx) shop.Offers = append(shop.Offers, offer) } if err = ctx.Err(); err != nil { return err } f, err := os.Create(path) if err != nil { return fmt.Errorf("creating file: %w", err) } defer func() { errClose := f.Close() if err == nil { err = errClose return } log.Err(errClose).Msg("file closed or not") }() if limit > 0 { shop.Offers = shop.Offers[:limit] } container := export.YmlContainer{ YmlCatalog: export.YmlCatalog{ Shop: shop, Date: time.Now(), }, } _, err = f.Write([]byte(xml.Header)) if err != nil { return fmt.Errorf("writing header: %w", err) } _, err = f.Write([]byte("\n")) enc := xml.NewEncoder(f) if pretty { enc.Indent("", " ") } return enc.Encode(container) } func parseEwayGetAction(ctx context.Context, cmd *cli.Command) error { cartID := cmd.Int("cart") client, err := components.GetEwayClient() if err != nil { return fmt.Errorf("getting eway client: %w", err) } pi, err := client.GetProductInfo(ctx, cartID) if err != nil { return fmt.Errorf("getting product info: %w", err) } enc := json.NewEncoder(cmd.Writer) enc.SetIndent("", " ") err = enc.Encode(pi) if err != nil { return fmt.Errorf("encoding data: %w", err) } return nil } func parseEwayListAction(ctx context.Context, cmd *cli.Command) error { page := cmd.Int("page") limit := cmd.Int("limit") atLeast := cmd.Int("min-stock") searchInStocks := atLeast > 0 client, err := components.GetEwayClient() if err != nil { return fmt.Errorf("getting eway client: %w", err) } log, err := components.GetLogger() if err != nil { return fmt.Errorf("getting logger: %w", err) } start := (page - 1) * limit items, total, err := client.GetGoodsNew(ctx, eway.GetGoodsNewParams{ Draw: 1, Start: int(start), Length: int(limit), SearchInStocks: searchInStocks, RemmantsAtleast: int(atLeast), }) if err != nil { return fmt.Errorf("getting new goods: %w", err) } productIDs := make([]int, 0, len(items)) for _, item := range items { productIDs = append(productIDs, int(item.Cart)) } remnants, err := client.GetGoodsRemnants(ctx, productIDs) if err != nil { return fmt.Errorf("getting remnants: %w", err) } goodsItems := make([]entity.GoodsItem, 0, len(items)) for _, item := range items { if ctx.Err() != nil { break } pi, err := client.GetProductInfo(ctx, int64(item.Cart)) if err != nil { log.Warn().Err(err).Msg("unable to get product info") } outGood, err := entity.MakeGoodsItem(item, remnants, pi) if err != nil { return fmt.Errorf("making goods item: %w", err) } goodsItems = append(goodsItems, outGood) } var stats = struct { Handled int `json:"handled"` Loaded int `json:"loaded"` Total int `json:"total"` }{ Handled: len(goodsItems), Loaded: len(items), Total: total, } if cmd.Bool("json") { enc := json.NewEncoder(cmd.Writer) if cmd.Bool("pretty") { enc.SetIndent("", " ") _ = enc.Encode(goodsItems) } enc.SetIndent("", "") _ = enc.Encode(stats) } else { tbl := table. New("sku", "category", "cart", "stock", "price", "parameters"). WithWriter(cmd.Writer) for _, outGood := range goodsItems { parameters, _ := json.MarshalIndent(outGood.Parameters, "", " ") tbl.AddRow( outGood.Articul, outGood.Type, outGood.Cart, outGood.Stock, outGood.Price, string(parameters), ) } tbl.Print() table. New("handled", "loaded", "total"). WithWriter(cmd.Writer). AddRow(stats.Handled, stats.Loaded, stats.Total). Print() } return nil } func parseEwayDumpAction(ctx context.Context, cmd *cli.Command) error { client, err := components.GetEwayClient() if err != nil { return fmt.Errorf("getting eway client: %w", err) } repository, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } logger, err := components.GetLogger() if err != nil { return fmt.Errorf("getting logger: %w", err) } const batchSize = 100 var i int var start int seenItems, err := entity.IterIntoMap[string, entity.GoodsItem](repository.GoodsItem().List(ctx)).Map(func(gi entity.GoodsItem) (string, error) { return gi.Articul, nil }) if err != nil { return fmt.Errorf("making seen items map: %w", err) } goodsItems := make([]entity.GoodsItem, 0, batchSize) productIDs := make([]int, 0, batchSize) knownCategories := make(map[string]struct{}) err = entity.IterWithErr(repository.Category().List(ctx)).Do(func(c entity.Category) error { knownCategories[c.Name] = struct{}{} return nil }) if err != nil { return fmt.Errorf("filling known categories: %w", err) } itemsUpdated := make(map[string]struct{}, len(seenItems)) stats := struct { fetchedInfo int handledAll int cachedInfo int skippedItem int }{} startFrom := time.Now() for { select { case <-ctx.Done(): return ctx.Err() default: } items, total, err := client.GetGoodsNew(ctx, eway.GetGoodsNewParams{ Draw: i, Start: start, Length: batchSize, SearchInStocks: true, RemmantsAtleast: 5, }) if err != nil { return fmt.Errorf("getting next goods batch: %w", err) } productIDs = productIDs[:0] for _, item := range items { productIDs = append(productIDs, int(item.Cart)) } remnants, err := client.GetGoodsRemnants(ctx, productIDs) if err != nil { return fmt.Errorf("getting goods remnants: %w", err) } goodsItems = goodsItems[:0] for _, item := range items { var pi entity.GoodsItemInfo seenItem := seenItems[item.SKU] if time.Since(seenItem.CreatedAt) < time.Hour*24 { logger.Debug().Str("sku", item.SKU).Msg("skipping item because it's too fresh") stats.skippedItem++ itemsUpdated[item.SKU] = struct{}{} continue } if len(seenItem.Parameters) != 0 && len(seenItem.PhotoURLs) != 0 { pi.Parameters = seenItem.Parameters pi.PhotoURLs = seenItem.PhotoURLs stats.cachedInfo++ } else { pi, err = client.GetProductInfo(ctx, int64(item.Cart)) if err != nil { return fmt.Errorf("getting product info: %w", err) } stats.fetchedInfo++ } goodsItem, err := entity.MakeGoodsItem(item, remnants, pi) if err != nil { logger.Warn().Err(err).Any("item", item).Msg("unable to make goods item") continue } itemsUpdated[goodsItem.Articul] = struct{}{} stats.handledAll++ goodsItems = append(goodsItems, goodsItem) if goodsItem.Type == "" { continue } if _, ok := knownCategories[goodsItem.Type]; ok { continue } category, err := repository.Category().Create(ctx, goodsItem.Type) if err != nil { return fmt.Errorf("creating category: %w", err) } logger.Debug(). Str("name", category.Name). Int64("id", category.ID). Msg("created new category") knownCategories[goodsItem.Type] = struct{}{} } _, err = repository.GoodsItem().UpsertMany(ctx, goodsItems...) if err != nil { return fmt.Errorf("upserting items: %w", err) } progressFloat := float64(start) / float64(total) progress := big.NewFloat(progressFloat).Text('f', 3) elapsed := time.Since(startFrom).Seconds() var left int if progressFloat != 0 { left = int(((1 - progressFloat) / progressFloat) * elapsed) } logger.Debug(). Int("from", start). Int("to", start+batchSize). Int("total", total). Str("progress", progress). Int("seconds_left", left). Msg("handled next batch items") if len(items) < batchSize { break } start += batchSize i++ } for k := range itemsUpdated { delete(seenItems, k) } logger.Info(). Int("handled", stats.handledAll). Int("cached", stats.cachedInfo). Int("fetched", stats.fetchedInfo). Int("skipped", stats.skippedItem). Int("to_delete", len(seenItems)). Msg("processed items") for k := range seenItems { _, err := repository.GoodsItem().Delete(ctx, k) if err != nil { logger.Warn().Err(err).Str("sku", k).Msg("unable to delete item") continue } logger.Debug().Str("sku", k).Msg("deleted item") } return nil } func goodsItemAsOffer(in entity.GoodsItem, categoryIDByName map[string]int64) (out export.Offer) { const defaultType = "vendor.model" const defaultCurrency = "RUR" const defaultAvailable = true const quantityParamName = "Количество на складе «Москва»" const basePictureURL = "https://eway.elevel.ru" imgurl := func(path string) string { return basePictureURL + path } categoryID := categoryIDByName[in.Type] pictureURLs := make([]string, 0, len(in.PhotoURLs)) for _, url := range in.PhotoURLs { pictureURLs = append(pictureURLs, imgurl(url)) } params := make([]export.Param, len(in.Parameters)) for k, v := range in.Parameters { params = append(params, export.Param{ Name: k, Value: v, }) } params = append(params, export.Param{ Name: quantityParamName, Value: strconv.Itoa(in.Stock), }) out = export.Offer{ ID: in.Cart, VendorCode: in.Articul, Price: int(in.TariffPrice), CategoryID: categoryID, PictureURLs: pictureURLs, Model: in.Name, Vendor: in.Producer, TypePrefix: in.Name, Description: in.Description, ManufacturerWarrany: true, Params: params, Type: defaultType, CurrencyID: defaultCurrency, Available: defaultAvailable, } return out } func appYMLExporterAction(ctx context.Context, cmd *cli.Command) error { port := cmd.Int("port") src := cmd.String("src") log, err := components.GetLogger() if err != nil { return fmt.Errorf("getting logger: %w", err) } mw := func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path remoteAddr := r.RemoteAddr xff := r.Header.Get("x-forwarded-for") method := r.Method ua := r.UserAgent() xreqid := r.Header.Get("x-request-id") if xreqid == "" { const reqsize = 4 var reqidRaw [reqsize]byte _, _ = rand.Read(reqidRaw[:]) value := binary.BigEndian.Uint32(reqidRaw[:]) xreqid = fmt.Sprintf("%x", value) w.Header().Set("x-request-id", xreqid) } start := time.Now() xlog := log.With().Str("request_id", xreqid).Logger() xlog.Debug(). Str("path", path). Str("remote_addr", remoteAddr). Str("xff", xff). Str("method", method). Str("user_agent", ua). Msg("incoming request") xctx := xlog.WithContext(r.Context()) r = r.WithContext(xctx) next(w, r) elapsed := time.Since(start).Truncate(time.Millisecond).Seconds() xlog.Info(). Float64("elapsed", elapsed). Msg("request completed") } } mux := http.NewServeMux() mux.HandleFunc("/yml_catalog.xml", mw(func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, src) })) srv := &http.Server{ Addr: net.JoinHostPort("0.0.0.0", strconv.Itoa(int(port))), Handler: mux, } go func() { <-ctx.Done() sdctx, sdcancel := context.WithTimeout(context.Background(), time.Second*5) defer sdcancel() errShutdown := srv.Shutdown(sdctx) if errShutdown != nil { log.Warn().Err(errShutdown).Msg("unable to shutdown server") } }() log.Info().Str("listen_addr", srv.Addr).Msg("running server") err = srv.ListenAndServe() if err != nil && errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("serving http api: %w", err) } return nil } func cryptoDeEncryptAction(encrypt bool) cli.ActionFunc { return func(ctx context.Context, c *cli.Command) (err error) { value := c.String("text") var out string if encrypt { out, err = crypto.Encrypt(value) } else { out, err = crypto.Decrypt(value) } if err != nil { return err } _, err = c.Writer.Write([]byte(out)) if err != nil { return err } _, err = c.Writer.Write([]byte{'\n'}) return err } }