diff --git a/cmd/cli/commands/sync/sync.go b/cmd/cli/commands/sync/sync.go index b0936b9..1f4bf16 100644 --- a/cmd/cli/commands/sync/sync.go +++ b/cmd/cli/commands/sync/sync.go @@ -65,6 +65,20 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) continue } + exists, err := client.Exists(r.GameID) + if err != nil { + slog.Error(err.Error()) + continue + } + + if !exists { + if err := push(r.GameID, m, client); err != nil { + fmt.Fprintln(os.Stderr, "failed to push:", err) + return subcommands.ExitFailure + } + continue + } + hlocal, err := game.Hash(r.GameID) if err != nil { slog.Error(err.Error()) @@ -83,22 +97,16 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) continue } - vremote, err := client.Version(r.GameID) + remoteMetadata, err := client.Metadata(r.GameID) if err != nil { - fmt.Fprintln(os.Stderr, "error: failed to get the file version from the remote:", err) - continue - } - - dtremote, err := client.Date(r.GameID) - if err != nil { - fmt.Fprintln(os.Stderr, "error: failed to get the file hash from the remote:", err) + fmt.Fprintln(os.Stderr, "error: failed to get the game metadata from the remote:", err) continue } if hlocal == hremote { - if vlocal != vremote { + if vlocal != remoteMetadata.Version { slog.Debug("version is not the same, but the hash is equal. Updating local database") - if err := game.SetVersion(r.GameID, vremote); err != nil { + if err := game.SetVersion(r.GameID, remoteMetadata.Version); err != nil { fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err) continue } @@ -107,7 +115,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) continue } - if vlocal > vremote { + if vlocal > remoteMetadata.Version { if err := push(r.GameID, m, client); err != nil { fmt.Fprintln(os.Stderr, "failed to push:", err) return subcommands.ExitFailure @@ -115,35 +123,36 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) continue } - if vlocal < vremote { + if vlocal < remoteMetadata.Version { if err := pull(r.GameID, client); err != nil { fmt.Fprintln(os.Stderr, "failed to push:", err) return subcommands.ExitFailure } - if err := game.SetVersion(r.GameID, vremote); err != nil { + if err := game.SetVersion(r.GameID, remoteMetadata.Version); err != nil { fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err) continue } - if err := game.SetDate(r.GameID, dtremote); err != nil { + if err := game.SetDate(r.GameID, remoteMetadata.Date); err != nil { fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err) continue } continue } - if vlocal == vremote { + if vlocal == remoteMetadata.Version { g, err := game.One(r.GameID) if err != nil { slog.Warn("a conflict was found but the game is not found in the database") slog.Debug("debug info", "gameID", r.GameID) continue } - fmt.Println("there are conflicts") + fmt.Println() + fmt.Println("--- /!\\ CONFLICT ---") fmt.Println("----") fmt.Println(g.Name, "(", g.Path, ")") fmt.Println("----") fmt.Println("Your version:", g.Date.Format(time.RFC1123)) - fmt.Println("Their version:", dtremote.Format(time.RFC1123)) + fmt.Println("Their version:", remoteMetadata.Date.Format(time.RFC1123)) fmt.Println() res := prompt.Conflict() @@ -163,11 +172,11 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) fmt.Fprintln(os.Stderr, "failed to push:", err) return subcommands.ExitFailure } - if err := game.SetVersion(r.GameID, vremote); err != nil { + if err := game.SetVersion(r.GameID, remoteMetadata.Version); err != nil { fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err) continue } - if err := game.SetDate(r.GameID, dtremote); err != nil { + if err := game.SetDate(r.GameID, remoteMetadata.Date); err != nil { fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err) continue } diff --git a/cmd/cli/tools/prompt/prompt.go b/cmd/cli/tools/prompt/prompt.go index 8dc6912..7bcc386 100644 --- a/cmd/cli/tools/prompt/prompt.go +++ b/cmd/cli/tools/prompt/prompt.go @@ -27,7 +27,7 @@ func ScanBool(msg string, defaultValue bool) bool { } func Conflict() ConflictResponse { - fmt.Println("[M: My, T: Their, A: Abort]: ") + fmt.Print("[M: My, T: Their, A: Abort]: ") var r string if _, err := fmt.Scanln(&r); err != nil { diff --git a/cmd/server/api/api.go b/cmd/server/api/api.go index 4db85a7..37d4b58 100644 --- a/cmd/server/api/api.go +++ b/cmd/server/api/api.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "strconv" + "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -41,6 +42,7 @@ func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServ }) router.Use(middleware.Logger) router.Use(recoverMiddleware) + router.Use(middleware.GetHead) router.Use(middleware.Compress(5, "application/gzip")) router.Use(BasicAuth("cloudsave", creds)) router.Use(middleware.Heartbeat("/heartbeat")) @@ -59,8 +61,7 @@ func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServ saveRouter.Post("/{id}/data", s.upload) saveRouter.Get("/{id}/data", s.download) saveRouter.Get("/{id}/hash", s.hash) - saveRouter.Get("/{id}/version", s.version) - saveRouter.Get("/{id}/date", s.date) + saveRouter.Get("/{id}/metadata", s.metadata) }) }) }) @@ -232,7 +233,7 @@ func (s HTTPServer) hash(w http.ResponseWriter, r *http.Request) { ok(hex.EncodeToString(sum), w, r) } -func (s HTTPServer) version(w http.ResponseWriter, r *http.Request) { +func (s HTTPServer) metadata(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") path := filepath.Clean(filepath.Join(s.documentRoot, "data", id)) @@ -265,43 +266,7 @@ func (s HTTPServer) version(w http.ResponseWriter, r *http.Request) { return } - ok(metadata.Version, w, r) -} - -func (s HTTPServer) date(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - path := filepath.Clean(filepath.Join(s.documentRoot, "data", id)) - - sdir, err := os.Stat(path) - if err != nil { - notFound("id not found", w, r) - return - } - - if !sdir.IsDir() { - notFound("id not found", w, r) - return - } - - path = filepath.Join(path, "metadata.json") - - f, err := os.OpenFile(path, os.O_RDONLY, 0) - if err != nil { - notFound("id not found", w, r) - return - } - defer f.Close() - - var metadata game.Metadata - d := json.NewDecoder(f) - err = d.Decode(&metadata) - if err != nil { - fmt.Fprintln(os.Stderr, "error: an error occured while reading data:", err) - internalServerError(w, r) - return - } - - ok(metadata.Date, w, r) + ok(metadata, w, r) } func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata, error) { @@ -309,7 +274,6 @@ func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata if v, ok := values["name"]; ok { if len(v) == 0 { return game.Metadata{}, fmt.Errorf("error: corrupted metadata") - } name = v[0] } else { @@ -323,6 +287,22 @@ func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata } if v, err := strconv.Atoi(v[0]); err == nil { version = v + } else { + return game.Metadata{}, err + } + } else { + return game.Metadata{}, fmt.Errorf("error: cannot find metadata in the form") + } + + var date time.Time + if v, ok := values["date"]; ok { + if len(v) == 0 { + return game.Metadata{}, fmt.Errorf("error: corrupted metadata") + } + if v, err := time.Parse(time.RFC3339, v[0]); err == nil { + date = v + } else { + return game.Metadata{}, err } } else { return game.Metadata{}, fmt.Errorf("error: cannot find metadata in the form") @@ -332,5 +312,6 @@ func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata ID: gameID, Version: version, Name: name, + Date: date, }, nil } diff --git a/pkg/game/game.go b/pkg/game/game.go index c0e91ec..267976c 100644 --- a/pkg/game/game.go +++ b/pkg/game/game.go @@ -163,23 +163,29 @@ func Version(gameID string) (int, error) { func SetVersion(gameID string, version int) error { path := filepath.Join(datastorepath, gameID, "metadata.json") - f, err := os.OpenFile(path, os.O_RDWR, 0740) + f, err := os.OpenFile(path, os.O_RDONLY, 0) if err != nil { return err } - defer f.Close() var metadata Metadata d := json.NewDecoder(f) err = d.Decode(&metadata) if err != nil { + f.Close() return err } - f.Seek(0, io.SeekStart) + f.Close() metadata.Version = version + f, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0740) + if err != nil { + return err + } + defer f.Close() + e := json.NewEncoder(f) err = e.Encode(metadata) if err != nil { @@ -192,23 +198,29 @@ func SetVersion(gameID string, version int) error { func SetDate(gameID string, dt time.Time) error { path := filepath.Join(datastorepath, gameID, "metadata.json") - f, err := os.OpenFile(path, os.O_RDWR, 0740) + f, err := os.OpenFile(path, os.O_RDONLY, 0) if err != nil { return err } - defer f.Close() var metadata Metadata d := json.NewDecoder(f) err = d.Decode(&metadata) if err != nil { + f.Close() return err } - f.Seek(0, io.SeekStart) + f.Close() metadata.Date = dt + f, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0740) + if err != nil { + return err + } + defer f.Close() + e := json.NewEncoder(f) err = e.Encode(metadata) if err != nil { diff --git a/pkg/remote/client/client.go b/pkg/remote/client/client.go index 2257fd2..9391bed 100644 --- a/pkg/remote/client/client.go +++ b/pkg/remote/client/client.go @@ -4,6 +4,7 @@ import ( "bytes" "cloudsave/pkg/game" "cloudsave/pkg/remote/obj" + customtime "cloudsave/pkg/tools/time" "encoding/json" "errors" "fmt" @@ -14,6 +15,8 @@ import ( "os" "strconv" "time" + + "github.com/schollz/progressbar/v3" ) type ( @@ -32,6 +35,37 @@ func New(baseURL, username, password string) *Client { } } +func (c *Client) Exists(gameID string) (bool, error) { + u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "hash") + if err != nil { + return false, err + } + + req, err := http.NewRequest("HEAD", u, nil) + if err != nil { + return false, err + } + + req.SetBasicAuth(c.username, c.password) + + cli := http.Client{} + + r, err := cli.Do(req) + if err != nil { + return false, err + } + defer r.Body.Close() + + switch r.StatusCode { + case 200: + return true, nil + case 404: + return false, nil + } + + return false, fmt.Errorf("an error occured: server response: %s", r.Status) +} + func (c *Client) Hash(gameID string) (string, error) { u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "hash") if err != nil { @@ -50,40 +84,28 @@ func (c *Client) Hash(gameID string) (string, error) { return "", errors.New("invalid payload sent by the server") } -func (c *Client) Version(gameID string) (int, error) { - u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "version") +func (c *Client) Metadata(gameID string) (game.Metadata, error) { + u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "metadata") if err != nil { - return 0, err + return game.Metadata{}, err } o, err := c.get(u) if err != nil { - return 0, err + return game.Metadata{}, err } - if h, ok := (o.Data).(float64); ok { - return int(h), nil + if m, ok := (o.Data).(map[string]any); ok { + gm := game.Metadata{ + ID: m["id"].(string), + Name: m["name"].(string), + Version: int(m["version"].(float64)), + Date: customtime.MustParse(time.RFC3339, m["date"].(string)), + } + return gm, nil } - return 0, errors.New("invalid payload sent by the server") -} - -func (c *Client) Date(gameID string) (time.Time, error) { - u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "version") - if err != nil { - return time.Time{}, err - } - - o, err := c.get(u) - if err != nil { - return time.Time{}, err - } - - if h, ok := (o.Data).(time.Time); ok { - return h, nil - } - - return time.Time{}, errors.New("invalid payload sent by the server") + return game.Metadata{}, errors.New("invalid payload sent by the server") } func (c *Client) Push(gameID, archivePath string, m game.Metadata) error { @@ -112,6 +134,7 @@ func (c *Client) Push(gameID, archivePath string, m game.Metadata) error { writer.WriteField("name", m.Name) writer.WriteField("version", strconv.Itoa(m.Version)) + writer.WriteField("date", m.Date.Format(time.RFC3339)) if err := writer.Close(); err != nil { return err @@ -171,7 +194,13 @@ func (c *Client) Pull(gameID, archivePath string) error { return fmt.Errorf("cannot connect to remote: server return code: %s", res.Status) } - if _, err := io.Copy(f, req.Body); err != nil { + bar := progressbar.DefaultBytes( + res.ContentLength, + "Pulling...", + ) + defer bar.Close() + + if _, err := io.Copy(io.MultiWriter(f, bar), res.Body); err != nil { return fmt.Errorf("an error occured while copying the file from the remote: %w", err) } diff --git a/pkg/tools/time/time.go b/pkg/tools/time/time.go new file mode 100644 index 0000000..70c8c5d --- /dev/null +++ b/pkg/tools/time/time.go @@ -0,0 +1,11 @@ +package time + +import "time" + +func MustParse(layout, value string) time.Time { + t, err := time.Parse(layout, value) + if err != nil { + panic(err) + } + return t +}