This commit is contained in:
2025-07-28 13:46:10 +02:00
parent 49baf33e92
commit 4e3e5ab8b1
6 changed files with 199 additions and 6 deletions

View File

@@ -44,11 +44,20 @@ func (p *RunCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
for _, metadata := range datastore { for _, metadata := range datastore {
pg.Describe("Scanning " + metadata.Name + "...") pg.Describe("Scanning " + metadata.Name + "...")
metadataPath := filepath.Join(game.DatastorePath(), metadata.ID) 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")) err := archiveIfChanged(metadata.ID, metadata.Path, filepath.Join(metadataPath, "data.tar.gz"), filepath.Join(metadataPath, ".last_run"))
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err) fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err)
return subcommands.ExitFailure 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) pg.Add(1)
} }

View File

@@ -1,6 +1,7 @@
package sync package sync
import ( import (
"cloudsave/cmd/cli/tools/prompt"
"cloudsave/pkg/game" "cloudsave/pkg/game"
"cloudsave/pkg/remote" "cloudsave/pkg/remote"
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
@@ -11,6 +12,7 @@ import (
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/google/subcommands" "github.com/google/subcommands"
) )
@@ -87,6 +89,12 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue 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 hlocal == hremote {
if vlocal != vremote { if vlocal != vremote {
slog.Debug("version is not the same, but the hash is equal. Updating local database") 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 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) fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure 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) fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err)
continue continue
} }
if err := game.SetDate(r.GameID, dtremote); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err)
continue
}
continue continue
} }
if vlocal == vremote { 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 continue
} }

View File

@@ -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
}
}

View File

@@ -60,6 +60,7 @@ func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServ
saveRouter.Get("/{id}/data", s.download) saveRouter.Get("/{id}/data", s.download)
saveRouter.Get("/{id}/hash", s.hash) saveRouter.Get("/{id}/hash", s.hash)
saveRouter.Get("/{id}/version", s.version) 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) 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) { func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata, error) {
var name string var name string
if v, ok := values["name"]; ok { if v, ok := values["name"]; ok {

View File

@@ -9,6 +9,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"time"
) )
type ( type (
@@ -17,6 +18,7 @@ type (
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
Version int `json:"version"` Version int `json:"version"`
Date time.Time `json:"date"`
} }
) )
@@ -186,3 +188,32 @@ func SetVersion(gameID string, version int) error {
return nil 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
}

View File

@@ -13,6 +13,7 @@ import (
"net/url" "net/url"
"os" "os"
"strconv" "strconv"
"time"
) )
type ( type (
@@ -67,6 +68,24 @@ func (c *Client) Version(gameID string) (int, error) {
return 0, errors.New("invalid payload sent by the server") 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 { func (c *Client) Push(gameID, archivePath string, m game.Metadata) error {
u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "data") u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "data")
if err != nil { if err != nil {