diff --git a/cmd/cli/commands/run/run.go b/cmd/cli/commands/run/run.go index e546f94..c9d0684 100644 --- a/cmd/cli/commands/run/run.go +++ b/cmd/cli/commands/run/run.go @@ -44,11 +44,20 @@ func (p *RunCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s for _, metadata := range datastore { pg.Describe("Scanning " + metadata.Name + "...") metadataPath := filepath.Join(game.DatastorePath(), metadata.ID) + //todo transaction err := archiveIfChanged(metadata.ID, metadata.Path, filepath.Join(metadataPath, "data.tar.gz"), filepath.Join(metadataPath, ".last_run")) if err != nil { fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err) return subcommands.ExitFailure } + if err := game.SetVersion(metadata.ID, metadata.Version+1); err != nil { + fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err) + return subcommands.ExitFailure + } + if err := game.SetDate(metadata.ID, time.Now()); err != nil { + fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err) + return subcommands.ExitFailure + } pg.Add(1) } diff --git a/cmd/cli/commands/sync/sync.go b/cmd/cli/commands/sync/sync.go index 8eaa0d4..b0936b9 100644 --- a/cmd/cli/commands/sync/sync.go +++ b/cmd/cli/commands/sync/sync.go @@ -1,6 +1,7 @@ package sync import ( + "cloudsave/cmd/cli/tools/prompt" "cloudsave/pkg/game" "cloudsave/pkg/remote" "cloudsave/pkg/remote/client" @@ -11,6 +12,7 @@ import ( "log/slog" "os" "path/filepath" + "time" "github.com/google/subcommands" ) @@ -87,6 +89,12 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) 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) + continue + } + if hlocal == hremote { if vlocal != vremote { slog.Debug("version is not the same, but the hash is equal. Updating local database") @@ -108,7 +116,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) } if vlocal < vremote { - if err := push(r.GameID, m, client); err != nil { + if err := pull(r.GameID, client); err != nil { fmt.Fprintln(os.Stderr, "failed to push:", err) return subcommands.ExitFailure } @@ -116,11 +124,55 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err) continue } + if err := game.SetDate(r.GameID, dtremote); err != nil { + fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err) + continue + } continue } if vlocal == vremote { - fmt.Println("conflict") + 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(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() + + res := prompt.Conflict() + + switch res { + case prompt.My: + { + if err := push(r.GameID, m, client); err != nil { + fmt.Fprintln(os.Stderr, "failed to push:", err) + return subcommands.ExitFailure + } + } + + case prompt.Their: + { + 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 { + fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err) + continue + } + if err := game.SetDate(r.GameID, dtremote); err != nil { + fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err) + continue + } + } + } continue } diff --git a/cmd/cli/tools/prompt/prompt.go b/cmd/cli/tools/prompt/prompt.go new file mode 100644 index 0000000..8dc6912 --- /dev/null +++ b/cmd/cli/tools/prompt/prompt.go @@ -0,0 +1,45 @@ +package prompt + +import ( + "fmt" + "strings" +) + +type ( + ConflictResponse int +) + +const ( + My ConflictResponse = iota + Their + Abort +) + +func ScanBool(msg string, defaultValue bool) bool { + fmt.Printf("%s: ", msg) + + var r string + if _, err := fmt.Scanln(&r); err != nil { + panic(err) + } + + return strings.ToLower(r) == "y" +} + +func Conflict() ConflictResponse { + fmt.Println("[M: My, T: Their, A: Abort]: ") + + var r string + if _, err := fmt.Scanln(&r); err != nil { + panic(err) + } + + switch strings.ToLower(r) { + case "m": + return My + case "t": + return Their + default: + return Abort + } +} diff --git a/cmd/server/api/api.go b/cmd/server/api/api.go index fd95160..4db85a7 100644 --- a/cmd/server/api/api.go +++ b/cmd/server/api/api.go @@ -60,6 +60,7 @@ func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServ saveRouter.Get("/{id}/data", s.download) saveRouter.Get("/{id}/hash", s.hash) saveRouter.Get("/{id}/version", s.version) + saveRouter.Get("/{id}/date", s.date) }) }) }) @@ -267,6 +268,42 @@ func (s HTTPServer) version(w http.ResponseWriter, r *http.Request) { 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) +} + func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata, error) { var name string if v, ok := values["name"]; ok { diff --git a/pkg/game/game.go b/pkg/game/game.go index eb62c77..c0e91ec 100644 --- a/pkg/game/game.go +++ b/pkg/game/game.go @@ -9,14 +9,16 @@ import ( "io" "os" "path/filepath" + "time" ) type ( Metadata struct { - ID string `json:"id"` - Name string `json:"name"` - Path string `json:"path"` - Version int `json:"version"` + ID string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Version int `json:"version"` + Date time.Time `json:"date"` } ) @@ -186,3 +188,32 @@ func SetVersion(gameID string, version int) error { return nil } + +func SetDate(gameID string, dt time.Time) error { + path := filepath.Join(datastorepath, gameID, "metadata.json") + + f, err := os.OpenFile(path, os.O_RDWR, 0740) + if err != nil { + return err + } + defer f.Close() + + var metadata Metadata + d := json.NewDecoder(f) + err = d.Decode(&metadata) + if err != nil { + return err + } + + f.Seek(0, io.SeekStart) + + metadata.Date = dt + + e := json.NewEncoder(f) + err = e.Encode(metadata) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/remote/client/client.go b/pkg/remote/client/client.go index ed6b584..2257fd2 100644 --- a/pkg/remote/client/client.go +++ b/pkg/remote/client/client.go @@ -13,6 +13,7 @@ import ( "net/url" "os" "strconv" + "time" ) type ( @@ -67,6 +68,24 @@ func (c *Client) Version(gameID string) (int, error) { 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") +} + func (c *Client) Push(gameID, archivePath string, m game.Metadata) error { u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "data") if err != nil {