This commit is contained in:
2025-07-26 12:51:12 +02:00
parent f31a19beab
commit 49baf33e92
9 changed files with 149 additions and 26 deletions

18
.vscode/launch.json vendored Normal file
View File

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

View File

@@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/google/subcommands" "github.com/google/subcommands"
"github.com/schollz/progressbar/v3"
) )
type ( type (
@@ -37,15 +38,22 @@ func (p *RunCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
return subcommands.ExitFailure return subcommands.ExitFailure
} }
pg := progressbar.New(len(datastore))
defer pg.Close()
for _, metadata := range datastore { for _, metadata := range datastore {
pg.Describe("Scanning " + metadata.Name + "...")
metadataPath := filepath.Join(game.DatastorePath(), metadata.ID) metadataPath := filepath.Join(game.DatastorePath(), metadata.ID)
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
} }
pg.Add(1)
} }
pg.Finish()
return subcommands.ExitSuccess return subcommands.ExitSuccess
} }
@@ -141,7 +149,5 @@ func archiveIfChanged(id, srcDir, destTarGz, stateFile string) error {
return fmt.Errorf("updating state file: %w", err) return fmt.Errorf("updating state file: %w", err)
} }
fmt.Println(id)
return nil return nil
} }

View File

@@ -69,7 +69,11 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue 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) vlocal, err := game.Version(r.GameID)
if err != nil { if err != nil {
@@ -77,18 +81,21 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
vremote, _ := client.Version(r.GameID) vremote, err := client.Version(r.GameID)
if err != nil {
if hlocal == hremote { fmt.Fprintln(os.Stderr, "error: failed to get the file version from the remote:", err)
fmt.Println("already up-to-date")
continue continue
} }
if vremote == 0 { if hlocal == hremote {
if err := push(r.GameID, m, client); err != nil { if vlocal != vremote {
fmt.Fprintln(os.Stderr, "failed to push:", err) slog.Debug("version is not the same, but the hash is equal. Updating local database")
return subcommands.ExitFailure 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 continue
} }
@@ -101,7 +108,14 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
} }
if vlocal < vremote { 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 continue
} }
@@ -109,6 +123,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
fmt.Println("conflict") fmt.Println("conflict")
continue continue
} }
} }
return subcommands.ExitSuccess return subcommands.ExitSuccess
@@ -119,3 +134,9 @@ func push(gameID string, m game.Metadata, cli *client.Client) error {
return cli.Push(gameID, archivePath, m) 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)
}

View File

@@ -10,6 +10,7 @@ import (
"cloudsave/cmd/cli/commands/version" "cloudsave/cmd/cli/commands/version"
"context" "context"
"flag" "flag"
"fmt"
"os" "os"
"github.com/google/subcommands" "github.com/google/subcommands"
@@ -31,5 +32,9 @@ func main() {
flag.Parse() flag.Parse()
ctx := context.Background() ctx := context.Background()
os.Exit(int(subcommands.Execute(ctx)))
exitCode := subcommands.Execute(ctx)
fmt.Println()
os.Exit(int(exitCode))
} }

View File

@@ -4,6 +4,7 @@ import (
"cloudsave/cmd/server/data" "cloudsave/cmd/server/data"
"cloudsave/pkg/game" "cloudsave/pkg/game"
"crypto/md5" "crypto/md5"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -227,7 +228,7 @@ func (s HTTPServer) hash(w http.ResponseWriter, r *http.Request) {
// Get checksum result // Get checksum result
sum := hasher.Sum(nil) sum := hasher.Sum(nil)
ok(sum, w, r) ok(hex.EncodeToString(sum), w, r)
} }
func (s HTTPServer) version(w http.ResponseWriter, r *http.Request) { 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) { 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 {
if len(v) != 0 { if len(v) == 0 {
return game.Metadata{}, fmt.Errorf("error: corrupted metadata") 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 var version int
if v, ok := values["version"]; ok { if v, ok := values["version"]; ok {
if len(v) != 0 { if len(v) == 0 {
return game.Metadata{}, fmt.Errorf("error: corrupted metadata") return game.Metadata{}, fmt.Errorf("error: corrupted metadata")
} }
if v, err := strconv.Atoi(v[0]); err == nil { if v, err := strconv.Atoi(v[0]); err == nil {

View File

@@ -14,7 +14,7 @@ func Write(gameID, documentRoot string, r io.Reader) error {
partPath := filepath.Join(dataFolderPath, "data.tar.gz.part") partPath := filepath.Join(dataFolderPath, "data.tar.gz.part")
finalFilePath := filepath.Join(dataFolderPath, "data.tar.gz") finalFilePath := filepath.Join(dataFolderPath, "data.tar.gz")
if err := os.MkdirAll(dataFolderPath, 0740); err != nil { if err := makeDataFolder(gameID, documentRoot); err != nil {
return err return err
} }
@@ -40,6 +40,9 @@ func Write(gameID, documentRoot string, r io.Reader) error {
} }
func UpdateMetadata(gameID, documentRoot string, m game.Metadata) 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") path := filepath.Join(documentRoot, "data", gameID, "metadata.json")
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0740) 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) e := json.NewEncoder(f)
return e.Encode(m) return e.Encode(m)
} }
func makeDataFolder(gameID, documentRoot string) error {
return os.MkdirAll(filepath.Join(documentRoot, "data", gameID), 0740)
}

7
go.mod
View File

@@ -5,8 +5,13 @@ go 1.24
require ( require (
github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/chi/v5 v5.2.1
github.com/google/subcommands v1.2.0 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/crypto v0.38.0
golang.org/x/term v0.32.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
)

18
go.sum
View File

@@ -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 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 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 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 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 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 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 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 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=

View File

@@ -42,7 +42,7 @@ func (c *Client) Hash(gameID string) (string, error) {
return "", err return "", err
} }
if h, ok := (o).(string); ok { if h, ok := (o.Data).(string); ok {
return h, nil return h, nil
} }
@@ -60,8 +60,8 @@ func (c *Client) Version(gameID string) (int, error) {
return 0, err return 0, err
} }
if h, ok := (o).(int); ok { if h, ok := (o.Data).(float64); ok {
return h, nil return int(h), nil
} }
return 0, errors.New("invalid payload sent by the server") 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 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 { func (c *Client) Ping() bool {
cli := http.Client{} cli := http.Client{}
@@ -152,31 +194,31 @@ func (c *Client) Ping() bool {
return true return true
} }
func (c *Client) get(url string) (any, error) { func (c *Client) get(url string) (obj.HTTPObject, error) {
cli := http.Client{} cli := http.Client{}
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
return nil, err return obj.HTTPObject{}, err
} }
req.SetBasicAuth(c.username, c.password) req.SetBasicAuth(c.username, c.password)
res, err := cli.Do(req) res, err := cli.Do(req)
if err != nil { if err != nil {
return nil, err return obj.HTTPObject{}, err
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != 200 { 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 var httpObject obj.HTTPObject
d := json.NewDecoder(res.Body) d := json.NewDecoder(res.Body)
err = d.Decode(&httpObject) err = d.Decode(&httpObject)
if err != nil { if err != nil {
return nil, err return obj.HTTPObject{}, err
} }
return httpObject, nil return httpObject, nil