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

View File

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

View File

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

View File

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

View File

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

7
go.mod
View File

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

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/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=

View File

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