summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorAlexander Neonxp Kiryukhin <i@neonxp.ru>2024-12-09 01:07:15 +0300
committerAlexander Neonxp Kiryukhin <i@neonxp.ru>2024-12-09 01:07:15 +0300
commit34ccc98a942098faefb5f4211b215ff9ccc7ad0e (patch)
tree7696ab4d7c8d9fb09c7e2575d482517f68824ae3 /cmd
Начальный
Diffstat (limited to 'cmd')
-rw-r--r--cmd/frontend/frontend.go101
-rw-r--r--cmd/importer/import.go182
-rw-r--r--cmd/migrator/migrate/init.go18
-rw-r--r--cmd/migrator/migrate/migrate.go31
-rw-r--r--cmd/migrator/migrate/rollback.go31
-rw-r--r--cmd/migrator/migrator.go54
-rw-r--r--cmd/migrator/migrator/migrator.go21
-rw-r--r--cmd/serve/serve.go67
8 files changed, 505 insertions, 0 deletions
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)
+ },
+}