From 49baf33e92e23394a67530e1598620f2275fea3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lie=20DELHAIE?= Date: Sat, 26 Jul 2025 12:51:12 +0200 Subject: [PATCH] fix --- .vscode/launch.json | 18 +++++++++++ cmd/cli/commands/run/run.go | 10 ++++-- cmd/cli/commands/sync/sync.go | 41 +++++++++++++++++++------ cmd/cli/main.go | 7 ++++- cmd/server/api/api.go | 7 +++-- cmd/server/data/data.go | 9 +++++- go.mod | 7 ++++- go.sum | 18 +++++++++++ pkg/remote/client/client.go | 58 ++++++++++++++++++++++++++++++----- 9 files changed, 149 insertions(+), 26 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0200674 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "args": ["sync"], + "console": "integratedTerminal", + "program": "${workspaceFolder}/cmd/cli" + } + ] +} \ No newline at end of file diff --git a/cmd/cli/commands/run/run.go b/cmd/cli/commands/run/run.go index 8978951..e546f94 100644 --- a/cmd/cli/commands/run/run.go +++ b/cmd/cli/commands/run/run.go @@ -13,6 +13,7 @@ import ( "time" "github.com/google/subcommands" + "github.com/schollz/progressbar/v3" ) type ( @@ -37,15 +38,22 @@ func (p *RunCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s return subcommands.ExitFailure } + pg := progressbar.New(len(datastore)) + defer pg.Close() + for _, metadata := range datastore { + pg.Describe("Scanning " + metadata.Name + "...") metadataPath := filepath.Join(game.DatastorePath(), metadata.ID) 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 } + pg.Add(1) } + pg.Finish() + return subcommands.ExitSuccess } @@ -141,7 +149,5 @@ func archiveIfChanged(id, srcDir, destTarGz, stateFile string) error { return fmt.Errorf("updating state file: %w", err) } - fmt.Println(id) - return nil } diff --git a/cmd/cli/commands/sync/sync.go b/cmd/cli/commands/sync/sync.go index 1c8698c..8eaa0d4 100644 --- a/cmd/cli/commands/sync/sync.go +++ b/cmd/cli/commands/sync/sync.go @@ -69,7 +69,11 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) continue } - hremote, _ := client.Hash(r.GameID) + hremote, err := client.Hash(r.GameID) + if err != nil { + fmt.Fprintln(os.Stderr, "error: failed to get the file hash from the remote:", err) + continue + } vlocal, err := game.Version(r.GameID) if err != nil { @@ -77,18 +81,21 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) continue } - vremote, _ := client.Version(r.GameID) - - if hlocal == hremote { - fmt.Println("already up-to-date") + vremote, err := client.Version(r.GameID) + if err != nil { + fmt.Fprintln(os.Stderr, "error: failed to get the file version from the remote:", err) continue } - if vremote == 0 { - if err := push(r.GameID, m, client); err != nil { - fmt.Fprintln(os.Stderr, "failed to push:", err) - return subcommands.ExitFailure + if hlocal == hremote { + if vlocal != vremote { + slog.Debug("version is not the same, but the hash is equal. Updating local database") + if err := game.SetVersion(r.GameID, vremote); err != nil { + fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err) + continue + } } + fmt.Println("already up-to-date") continue } @@ -101,7 +108,14 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) } if vlocal < vremote { - fmt.Println("pull") + if err := push(r.GameID, m, 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 + } continue } @@ -109,6 +123,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) fmt.Println("conflict") continue } + } return subcommands.ExitSuccess @@ -119,3 +134,9 @@ func push(gameID string, m game.Metadata, cli *client.Client) error { return cli.Push(gameID, archivePath, m) } + +func pull(gameID string, cli *client.Client) error { + archivePath := filepath.Join(game.DatastorePath(), gameID, "data.tar.gz") + + return cli.Pull(gameID, archivePath) +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 1a2d07a..7bb4312 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -10,6 +10,7 @@ import ( "cloudsave/cmd/cli/commands/version" "context" "flag" + "fmt" "os" "github.com/google/subcommands" @@ -31,5 +32,9 @@ func main() { flag.Parse() ctx := context.Background() - os.Exit(int(subcommands.Execute(ctx))) + + exitCode := subcommands.Execute(ctx) + fmt.Println() + + os.Exit(int(exitCode)) } diff --git a/cmd/server/api/api.go b/cmd/server/api/api.go index 71c1f46..fd95160 100644 --- a/cmd/server/api/api.go +++ b/cmd/server/api/api.go @@ -4,6 +4,7 @@ import ( "cloudsave/cmd/server/data" "cloudsave/pkg/game" "crypto/md5" + "encoding/hex" "encoding/json" "fmt" "io" @@ -227,7 +228,7 @@ func (s HTTPServer) hash(w http.ResponseWriter, r *http.Request) { // Get checksum result sum := hasher.Sum(nil) - ok(sum, w, r) + ok(hex.EncodeToString(sum), w, r) } func (s HTTPServer) version(w http.ResponseWriter, r *http.Request) { @@ -269,7 +270,7 @@ func (s HTTPServer) version(w http.ResponseWriter, r *http.Request) { func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata, error) { var name string if v, ok := values["name"]; ok { - if len(v) != 0 { + if len(v) == 0 { return game.Metadata{}, fmt.Errorf("error: corrupted metadata") } @@ -280,7 +281,7 @@ func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata var version int if v, ok := values["version"]; ok { - if len(v) != 0 { + if len(v) == 0 { return game.Metadata{}, fmt.Errorf("error: corrupted metadata") } if v, err := strconv.Atoi(v[0]); err == nil { diff --git a/cmd/server/data/data.go b/cmd/server/data/data.go index e57ef20..cddfe7f 100644 --- a/cmd/server/data/data.go +++ b/cmd/server/data/data.go @@ -14,7 +14,7 @@ func Write(gameID, documentRoot string, r io.Reader) error { partPath := filepath.Join(dataFolderPath, "data.tar.gz.part") finalFilePath := filepath.Join(dataFolderPath, "data.tar.gz") - if err := os.MkdirAll(dataFolderPath, 0740); err != nil { + if err := makeDataFolder(gameID, documentRoot); err != nil { return err } @@ -40,6 +40,9 @@ func Write(gameID, documentRoot string, r io.Reader) error { } func UpdateMetadata(gameID, documentRoot string, m game.Metadata) error { + if err := makeDataFolder(gameID, documentRoot); err != nil { + return err + } path := filepath.Join(documentRoot, "data", gameID, "metadata.json") f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0740) @@ -51,3 +54,7 @@ func UpdateMetadata(gameID, documentRoot string, m game.Metadata) error { e := json.NewEncoder(f) return e.Encode(m) } + +func makeDataFolder(gameID, documentRoot string) error { + return os.MkdirAll(filepath.Join(documentRoot, "data", gameID), 0740) +} diff --git a/go.mod b/go.mod index b3522a7..9d82114 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,13 @@ go 1.24 require ( github.com/go-chi/chi/v5 v5.2.1 github.com/google/subcommands v1.2.0 + github.com/schollz/progressbar/v3 v3.18.0 golang.org/x/crypto v0.38.0 golang.org/x/term v0.32.0 ) -require golang.org/x/sys v0.33.0 // indirect +require ( + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.33.0 // indirect +) diff --git a/go.sum b/go.sum index f308fb6..aeaaa97 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,28 @@ +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= +github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/remote/client/client.go b/pkg/remote/client/client.go index c8b18dd..ed6b584 100644 --- a/pkg/remote/client/client.go +++ b/pkg/remote/client/client.go @@ -42,7 +42,7 @@ func (c *Client) Hash(gameID string) (string, error) { return "", err } - if h, ok := (o).(string); ok { + if h, ok := (o.Data).(string); ok { return h, nil } @@ -60,8 +60,8 @@ func (c *Client) Version(gameID string) (int, error) { return 0, err } - if h, ok := (o).(int); ok { - return h, nil + if h, ok := (o.Data).(float64); ok { + return int(h), nil } return 0, errors.New("invalid payload sent by the server") @@ -121,6 +121,48 @@ func (c *Client) Push(gameID, archivePath string, m game.Metadata) error { return nil } +func (c *Client) Pull(gameID, archivePath string) error { + u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "data") + if err != nil { + return err + } + + cli := http.Client{} + + req, err := http.NewRequest("GET", u, nil) + if err != nil { + return err + } + + req.SetBasicAuth(c.username, c.password) + + f, err := os.OpenFile(archivePath+".part", os.O_CREATE|os.O_WRONLY, 0740) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer f.Close() + + res, err := cli.Do(req) + if err != nil { + return fmt.Errorf("cannot connect to remote: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return fmt.Errorf("cannot connect to remote: server return code: %s", res.Status) + } + + if _, err := io.Copy(f, req.Body); err != nil { + return fmt.Errorf("an error occured while copying the file from the remote: %w", err) + } + + if err := os.Rename(archivePath+".part", archivePath); err != nil { + return fmt.Errorf("failed to move temporary data: %w", err) + } + + return nil +} + func (c *Client) Ping() bool { cli := http.Client{} @@ -152,31 +194,31 @@ func (c *Client) Ping() bool { return true } -func (c *Client) get(url string) (any, error) { +func (c *Client) get(url string) (obj.HTTPObject, error) { cli := http.Client{} req, err := http.NewRequest("GET", url, nil) if err != nil { - return nil, err + return obj.HTTPObject{}, err } req.SetBasicAuth(c.username, c.password) res, err := cli.Do(req) if err != nil { - return nil, err + return obj.HTTPObject{}, err } defer res.Body.Close() if res.StatusCode != 200 { - return nil, fmt.Errorf("server returns an unexpected status code: %d %s (expected 200)", res.StatusCode, res.Status) + return obj.HTTPObject{}, fmt.Errorf("server returns an unexpected status code: %d %s (expected 200)", res.StatusCode, res.Status) } var httpObject obj.HTTPObject d := json.NewDecoder(res.Body) err = d.Decode(&httpObject) if err != nil { - return nil, err + return obj.HTTPObject{}, err } return httpObject, nil