From 34ccc98a942098faefb5f4211b215ff9ccc7ad0e Mon Sep 17 00:00:00 2001 From: Alexander Neonxp Kiryukhin Date: Mon, 9 Dec 2024 01:07:15 +0300 Subject: Начальный MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/frontend/frontend.go | 101 +++++++++++++++++++++ cmd/importer/import.go | 182 ++++++++++++++++++++++++++++++++++++++ cmd/migrator/migrate/init.go | 18 ++++ cmd/migrator/migrate/migrate.go | 31 +++++++ cmd/migrator/migrate/rollback.go | 31 +++++++ cmd/migrator/migrator.go | 54 +++++++++++ cmd/migrator/migrator/migrator.go | 21 +++++ cmd/serve/serve.go | 67 ++++++++++++++ 8 files changed, 505 insertions(+) create mode 100644 cmd/frontend/frontend.go create mode 100644 cmd/importer/import.go create mode 100644 cmd/migrator/migrate/init.go create mode 100644 cmd/migrator/migrate/migrate.go create mode 100644 cmd/migrator/migrate/rollback.go create mode 100644 cmd/migrator/migrator.go create mode 100644 cmd/migrator/migrator/migrator.go create mode 100644 cmd/serve/serve.go (limited to 'cmd') diff --git a/cmd/frontend/frontend.go b/cmd/frontend/frontend.go new file mode 100644 index 0000000..fc7dbc8 --- /dev/null +++ b/cmd/frontend/frontend.go @@ -0,0 +1,101 @@ +package frontend + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/evanw/esbuild/pkg/api" + "github.com/urfave/cli/v3" +) + +var Command = &cli.Command{ + Name: "frontend", + Commands: []*cli.Command{ + { + Name: "build", + Action: func(ctx context.Context, c *cli.Command) error { + result := api.Build(api.BuildOptions{ + EntryPoints: []string{"./frontend/index.jsx"}, + Outdir: "static/assets", + Bundle: true, + Write: true, + LogLevel: api.LogLevelInfo, + ChunkNames: "chunks/[name]-[hash]", + MinifyWhitespace: true, + MinifyIdentifiers: true, + MinifySyntax: true, + Splitting: false, + Sourcemap: api.SourceMapInline, + Format: api.FormatDefault, + Color: api.ColorAlways, + Define: map[string]string{ + "process.env.NODE_ENV": `"dev"`, + }, + AssetNames: "assets/[name]-[hash]", + Loader: map[string]api.Loader{ + ".png": api.LoaderFile, + ".css": api.LoaderCSS, + }, + }) + if len(result.Errors) > 0 { + errs := make([]string, 0, len(result.Errors)) + for _, e := range result.Errors { + errs = append(errs, fmt.Sprintf("%s: %s", e.PluginName, e.Text)) + } + + return errors.New(strings.Join(errs, ", ")) + } + + return nil + }, + }, + { + Name: "watch", + Action: func(ctx context.Context, c *cli.Command) error { + bctx, result := api.Context(api.BuildOptions{ + EntryPoints: []string{"./frontend/index.jsx"}, + Outdir: "static/assets", + Bundle: true, + Write: true, + LogLevel: api.LogLevelInfo, + ChunkNames: "chunks/[name]-[hash]", + MinifyWhitespace: false, + MinifyIdentifiers: false, + MinifySyntax: false, + Splitting: false, + Sourcemap: api.SourceMapInline, + Format: api.FormatESModule, + Color: api.ColorAlways, + Define: map[string]string{ + "process.env.NODE_ENV": `"dev"`, + }, + AssetNames: "assets/[name]-[hash]", + Loader: map[string]api.Loader{ + ".png": api.LoaderFile, + ".css": api.LoaderCSS, + }, + }) + if result != nil && len(result.Errors) > 0 { + errs := make([]string, 0, len(result.Errors)) + for _, e := range result.Errors { + errs = append(errs, fmt.Sprintf("%s: %s", e.PluginName, e.Text)) + } + + return errors.New(strings.Join(errs, ", ")) + } + + if err := bctx.Watch(api.WatchOptions{}); err != nil { + return err + } + + fmt.Printf("watching...\n") + + <-make(chan struct{}) + + return nil + }, + }, + }, +} diff --git a/cmd/importer/import.go b/cmd/importer/import.go new file mode 100644 index 0000000..f08117d --- /dev/null +++ b/cmd/importer/import.go @@ -0,0 +1,182 @@ +package importer + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "unicode/utf16" + + "git.neonxp.ru/neonxp/guessr/pkg/config" + "git.neonxp.ru/neonxp/guessr/pkg/db" + "git.neonxp.ru/neonxp/guessr/pkg/model" + "github.com/urfave/cli/v3" +) + +var Command = &cli.Command{ + Name: "import", + Usage: "import json with places", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "download-img", + Usage: "download images locally", + }, + }, + ArgsUsage: "path to json file", + Action: func(ctx context.Context, c *cli.Command) error { + downloadImg := c.Bool("download-img") + + cfg, err := config.New() + if err != nil { + return err + } + + dbClient := db.New(cfg.DB) + + fp, err := os.Open(c.Args().First()) + if err != nil { + return err + } + defer fp.Close() + + places := Places{} + dec := json.NewDecoder(fp) + + if err := dec.Decode(&places); err != nil { + return err + } + + for _, p := range places { + decodedValue := unescape(p.Name) + h := hash(p.Img) + prefix := h[0:1] + path := filepath.Join(".", "static", "places", prefix) + + file := filepath.Join(path, h+".jpg") + + if downloadImg { + //nolint:gomnd + if err := os.MkdirAll(path, 0o777); err != nil { + return err + } + + if err := downloadFile(p.Img, file); err != nil { + return err + } + } + + np := model.Place{ + GUID: p.GUID, + Img: strings.Replace(file, "static", "", 1), + Name: strings.ReplaceAll(decodedValue, "�", `"`), + Position: &model.Point{ + Lat: p.Position.Lat, + Lon: p.Position.Lng, + }, + } + + if _, err := dbClient.NewInsert().Model(&np).Exec(ctx); err != nil { + return err + } + } + + return nil + }, +} + +type Place struct { + GUID string `json:"guid"` + Position PositionEntity `json:"position"` + Img string `json:"img"` + Name string `json:"name"` +} + +type PositionEntity struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` +} + +type Places []Place + +func hash(in string) string { + hasher := sha256.New() + if _, err := hasher.Write([]byte(in)); err != nil { + return "" + } + + return base64.URLEncoding.EncodeToString(hasher.Sum(nil)) +} + +func unescape(input string) string { + output := make([]rune, 0, len(input)) + length := len(input) + + for index := 0; index < length; { + //nolint:nestif + if input[index] == '%' { + if index <= length-6 && input[index+1] == 'u' { + byte16, err := hex.DecodeString(input[index+2 : index+6]) + if err == nil { + //nolint:gomnd + value := uint16(byte16[0])<<8 + uint16(byte16[1]) + chr := utf16.Decode([]uint16{value})[0] + output = append(output, chr) + index += 6 + + continue + } + } + + if index <= length-3 { + byte8, err := hex.DecodeString(input[index+1 : index+3]) + if err == nil { + value := uint16(byte8[0]) + chr := utf16.Decode([]uint16{value})[0] + output = append(output, chr) + index += 3 + + continue + } + } + } + + output = append(output, rune(input[index])) + index++ + } + + return string(output) +} + +func downloadFile(fileURL, fileName string) error { + //nolint:gosec,noctx + response, err := http.Get(fileURL) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.New("received non 200 response code") + } + // Create a empty file + file, err := os.Create(fileName) + if err != nil { + return err + } + defer file.Close() + + // Write the bytes to the fiel + _, err = io.Copy(file, response.Body) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/migrator/migrate/init.go b/cmd/migrator/migrate/init.go new file mode 100644 index 0000000..6f06c2d --- /dev/null +++ b/cmd/migrator/migrate/init.go @@ -0,0 +1,18 @@ +package migrate + +import ( + "context" + + "git.neonxp.ru/neonxp/guessr/cmd/migrator/migrator" + "git.neonxp.ru/neonxp/guessr/migrations" + "github.com/urfave/cli/v3" +) + +func Init(ctx context.Context, c *cli.Command) error { + m, err := migrator.New(c, migrations.Migrations) + if err != nil { + return err + } + + return m.Init(ctx) +} diff --git a/cmd/migrator/migrate/migrate.go b/cmd/migrator/migrate/migrate.go new file mode 100644 index 0000000..6245879 --- /dev/null +++ b/cmd/migrator/migrate/migrate.go @@ -0,0 +1,31 @@ +package migrate + +import ( + "context" + "fmt" + + "git.neonxp.ru/neonxp/guessr/cmd/migrator/migrator" + "git.neonxp.ru/neonxp/guessr/migrations" + "github.com/urfave/cli/v3" +) + +func Migrate(ctx context.Context, c *cli.Command) error { + m, err := migrator.New(c, migrations.Migrations) + if err != nil { + return err + } + + group, err := m.Migrate(ctx) + if err != nil { + return err + } + + if group.ID == 0 { + fmt.Printf("there are no new migrations to run\n") + return nil + } + + fmt.Printf("migrated to %s\n", group) + + return nil +} diff --git a/cmd/migrator/migrate/rollback.go b/cmd/migrator/migrate/rollback.go new file mode 100644 index 0000000..af56654 --- /dev/null +++ b/cmd/migrator/migrate/rollback.go @@ -0,0 +1,31 @@ +package migrate + +import ( + "context" + "fmt" + + "git.neonxp.ru/neonxp/guessr/cmd/migrator/migrator" + "git.neonxp.ru/neonxp/guessr/migrations" + "github.com/urfave/cli/v3" +) + +func Rollback(ctx context.Context, c *cli.Command) error { + m, err := migrator.New(c, migrations.Migrations) + if err != nil { + return err + } + + group, err := m.Rollback(ctx) + if err != nil { + return err + } + + if group.ID == 0 { + fmt.Printf("there are no groups to roll back\n") + return nil + } + + fmt.Printf("rolled back %s\n", group) + + return nil +} diff --git a/cmd/migrator/migrator.go b/cmd/migrator/migrator.go new file mode 100644 index 0000000..31f8dab --- /dev/null +++ b/cmd/migrator/migrator.go @@ -0,0 +1,54 @@ +package migrator + +import ( + "context" + "fmt" + "strings" + + "git.neonxp.ru/neonxp/guessr/cmd/migrator/migrate" + "git.neonxp.ru/neonxp/guessr/cmd/migrator/migrator" + "git.neonxp.ru/neonxp/guessr/migrations" + "github.com/urfave/cli/v3" +) + +var Command = &cli.Command{ + Name: "db", + Usage: "manage database migrations", + Commands: []*cli.Command{ + { + Name: "init", + Usage: "create migration tables", + Action: migrate.Init, + }, + { + Name: "migrate", + Usage: "migrate database", + Action: migrate.Migrate, + }, + { + Name: "rollback", + Usage: "rollback the last migration group", + Action: migrate.Rollback, + }, + + { + Name: "create", + Usage: "create migration", + Action: func(ctx context.Context, c *cli.Command) error { + migrator, err := migrator.New(c, migrations.Migrations) + if err != nil { + return err + } + + name := strings.Join(c.Args().Slice(), "_") + mf, err := migrator.CreateGoMigration(ctx, name) + if err != nil { + return err + } + fmt.Printf("created migration %s (%s)\n", mf.Name, mf.Path) + + return nil + }, + }, + }, +} diff --git a/cmd/migrator/migrator/migrator.go b/cmd/migrator/migrator/migrator.go new file mode 100644 index 0000000..f487e64 --- /dev/null +++ b/cmd/migrator/migrator/migrator.go @@ -0,0 +1,21 @@ +package migrator + +import ( + "git.neonxp.ru/neonxp/guessr/pkg/config" + "git.neonxp.ru/neonxp/guessr/pkg/db" + "github.com/uptrace/bun/migrate" + "github.com/urfave/cli/v3" +) + +func New(c *cli.Command, mig *migrate.Migrations) (*migrate.Migrator, error) { + cfg, err := config.New() + if err != nil { + return nil, err + } + + dbClient := db.New(cfg.DB) + + migrator := migrate.NewMigrator(dbClient, mig) + + return migrator, nil +} diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go new file mode 100644 index 0000000..8591084 --- /dev/null +++ b/cmd/serve/serve.go @@ -0,0 +1,67 @@ +package serve + +import ( + "context" + "os" + "time" + + "git.neonxp.ru/neonxp/guessr/pkg/api" + "git.neonxp.ru/neonxp/guessr/pkg/config" + "git.neonxp.ru/neonxp/guessr/pkg/db" + "git.neonxp.ru/neonxp/guessr/pkg/middleware" + "git.neonxp.ru/neonxp/guessr/pkg/service" + "github.com/gorilla/sessions" + echosession "github.com/labstack/echo-contrib/session" + "github.com/labstack/echo/v4" + echomiddleware "github.com/labstack/echo/v4/middleware" + "github.com/urfave/cli/v3" + "golang.org/x/time/rate" +) + +var Command = &cli.Command{ + Name: "serve", + Usage: "start api server", + Action: func(ctx context.Context, c *cli.Command) error { + cfg, err := config.New() + if err != nil { + return err + } + dbClient := db.New(cfg.DB) + placesService := service.New(dbClient) + + apiHandler := api.New(placesService) + + e := echo.New() + e.Debug = cfg.Debug + + e.Use( + echomiddleware.Recover(), + echomiddleware.Logger(), + echomiddleware.RemoveTrailingSlash(), + ) + + func(g *echo.Group) { + g.Use( + echosession.Middleware(sessions.NewCookieStore([]byte(cfg.Keys[0]), []byte(cfg.Keys[1]))), + middleware.PopulateState(), + echomiddleware.RateLimiter( + echomiddleware.NewRateLimiterMemoryStore( + rate.Every(time.Second), + ), + ), + ) + g.POST("/next", apiHandler.PostNext) + g.POST("/guess", apiHandler.PostGuess) + g.GET("/state", apiHandler.GetState) + g.POST("/state", apiHandler.PostState) + }(e.Group("/api")) + + // if cfg.Debug { + e.StaticFS("/", os.DirFS("./static")) + // } else { + // e.StaticFS("/", static.FS) + // } + + return e.Start(cfg.Listen) + }, +} -- cgit v1.2.3