This commit is contained in:
2025-07-28 16:02:34 +02:00
parent 4e3e5ab8b1
commit d8e0bffe56
6 changed files with 135 additions and 93 deletions

View File

@@ -65,6 +65,20 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
exists, err := client.Exists(r.GameID)
if err != nil {
slog.Error(err.Error())
continue
}
if !exists {
if err := push(r.GameID, m, client); err != nil {
fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure
}
continue
}
hlocal, err := game.Hash(r.GameID) hlocal, err := game.Hash(r.GameID)
if err != nil { if err != nil {
slog.Error(err.Error()) slog.Error(err.Error())
@@ -83,22 +97,16 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
vremote, err := client.Version(r.GameID) remoteMetadata, err := client.Metadata(r.GameID)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to get the file version from the remote:", err) fmt.Fprintln(os.Stderr, "error: failed to get the game metadata from the remote:", err)
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 continue
} }
if hlocal == hremote { if hlocal == hremote {
if vlocal != vremote { if vlocal != remoteMetadata.Version {
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")
if err := game.SetVersion(r.GameID, vremote); err != nil { if err := game.SetVersion(r.GameID, remoteMetadata.Version); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err) fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err)
continue continue
} }
@@ -107,7 +115,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
if vlocal > vremote { if vlocal > remoteMetadata.Version {
if err := push(r.GameID, m, client); err != nil { if err := push(r.GameID, m, 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
@@ -115,35 +123,36 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
if vlocal < vremote { if vlocal < remoteMetadata.Version {
if err := pull(r.GameID, 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
} }
if err := game.SetVersion(r.GameID, vremote); err != nil { if err := game.SetVersion(r.GameID, remoteMetadata.Version); err != nil {
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 { if err := game.SetDate(r.GameID, remoteMetadata.Date); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err) fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err)
continue continue
} }
continue continue
} }
if vlocal == vremote { if vlocal == remoteMetadata.Version {
g, err := game.One(r.GameID) g, err := game.One(r.GameID)
if err != nil { if err != nil {
slog.Warn("a conflict was found but the game is not found in the database") slog.Warn("a conflict was found but the game is not found in the database")
slog.Debug("debug info", "gameID", r.GameID) slog.Debug("debug info", "gameID", r.GameID)
continue continue
} }
fmt.Println("there are conflicts") fmt.Println()
fmt.Println("--- /!\\ CONFLICT ---")
fmt.Println("----") fmt.Println("----")
fmt.Println(g.Name, "(", g.Path, ")") fmt.Println(g.Name, "(", g.Path, ")")
fmt.Println("----") fmt.Println("----")
fmt.Println("Your version:", g.Date.Format(time.RFC1123)) fmt.Println("Your version:", g.Date.Format(time.RFC1123))
fmt.Println("Their version:", dtremote.Format(time.RFC1123)) fmt.Println("Their version:", remoteMetadata.Date.Format(time.RFC1123))
fmt.Println() fmt.Println()
res := prompt.Conflict() res := prompt.Conflict()
@@ -163,11 +172,11 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
fmt.Fprintln(os.Stderr, "failed to push:", err) fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
if err := game.SetVersion(r.GameID, vremote); err != nil { if err := game.SetVersion(r.GameID, remoteMetadata.Version); err != nil {
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 { if err := game.SetDate(r.GameID, remoteMetadata.Date); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err) fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err)
continue continue
} }

View File

@@ -27,7 +27,7 @@ func ScanBool(msg string, defaultValue bool) bool {
} }
func Conflict() ConflictResponse { func Conflict() ConflictResponse {
fmt.Println("[M: My, T: Their, A: Abort]: ") fmt.Print("[M: My, T: Their, A: Abort]: ")
var r string var r string
if _, err := fmt.Scanln(&r); err != nil { if _, err := fmt.Scanln(&r); err != nil {

View File

@@ -12,6 +12,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
@@ -41,6 +42,7 @@ func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServ
}) })
router.Use(middleware.Logger) router.Use(middleware.Logger)
router.Use(recoverMiddleware) router.Use(recoverMiddleware)
router.Use(middleware.GetHead)
router.Use(middleware.Compress(5, "application/gzip")) router.Use(middleware.Compress(5, "application/gzip"))
router.Use(BasicAuth("cloudsave", creds)) router.Use(BasicAuth("cloudsave", creds))
router.Use(middleware.Heartbeat("/heartbeat")) router.Use(middleware.Heartbeat("/heartbeat"))
@@ -59,8 +61,7 @@ func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServ
saveRouter.Post("/{id}/data", s.upload) saveRouter.Post("/{id}/data", s.upload)
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}/metadata", s.metadata)
saveRouter.Get("/{id}/date", s.date)
}) })
}) })
}) })
@@ -232,7 +233,7 @@ func (s HTTPServer) hash(w http.ResponseWriter, r *http.Request) {
ok(hex.EncodeToString(sum), w, r) ok(hex.EncodeToString(sum), w, r)
} }
func (s HTTPServer) version(w http.ResponseWriter, r *http.Request) { func (s HTTPServer) metadata(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") id := chi.URLParam(r, "id")
path := filepath.Clean(filepath.Join(s.documentRoot, "data", id)) path := filepath.Clean(filepath.Join(s.documentRoot, "data", id))
@@ -265,43 +266,7 @@ func (s HTTPServer) version(w http.ResponseWriter, r *http.Request) {
return return
} }
ok(metadata.Version, w, r) ok(metadata, 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) {
@@ -309,7 +274,6 @@ func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata
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")
} }
name = v[0] name = v[0]
} else { } else {
@@ -323,6 +287,22 @@ func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata
} }
if v, err := strconv.Atoi(v[0]); err == nil { if v, err := strconv.Atoi(v[0]); err == nil {
version = v version = v
} else {
return game.Metadata{}, err
}
} else {
return game.Metadata{}, fmt.Errorf("error: cannot find metadata in the form")
}
var date time.Time
if v, ok := values["date"]; ok {
if len(v) == 0 {
return game.Metadata{}, fmt.Errorf("error: corrupted metadata")
}
if v, err := time.Parse(time.RFC3339, v[0]); err == nil {
date = v
} else {
return game.Metadata{}, err
} }
} else { } else {
return game.Metadata{}, fmt.Errorf("error: cannot find metadata in the form") return game.Metadata{}, fmt.Errorf("error: cannot find metadata in the form")
@@ -332,5 +312,6 @@ func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata
ID: gameID, ID: gameID,
Version: version, Version: version,
Name: name, Name: name,
Date: date,
}, nil }, nil
} }

View File

@@ -163,23 +163,29 @@ func Version(gameID string) (int, error) {
func SetVersion(gameID string, version int) error { func SetVersion(gameID string, version int) error {
path := filepath.Join(datastorepath, gameID, "metadata.json") path := filepath.Join(datastorepath, gameID, "metadata.json")
f, err := os.OpenFile(path, os.O_RDWR, 0740) f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil { if err != nil {
return err return err
} }
defer f.Close()
var metadata Metadata var metadata Metadata
d := json.NewDecoder(f) d := json.NewDecoder(f)
err = d.Decode(&metadata) err = d.Decode(&metadata)
if err != nil { if err != nil {
f.Close()
return err return err
} }
f.Seek(0, io.SeekStart) f.Close()
metadata.Version = version metadata.Version = version
f, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0740)
if err != nil {
return err
}
defer f.Close()
e := json.NewEncoder(f) e := json.NewEncoder(f)
err = e.Encode(metadata) err = e.Encode(metadata)
if err != nil { if err != nil {
@@ -192,23 +198,29 @@ func SetVersion(gameID string, version int) error {
func SetDate(gameID string, dt time.Time) error { func SetDate(gameID string, dt time.Time) error {
path := filepath.Join(datastorepath, gameID, "metadata.json") path := filepath.Join(datastorepath, gameID, "metadata.json")
f, err := os.OpenFile(path, os.O_RDWR, 0740) f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil { if err != nil {
return err return err
} }
defer f.Close()
var metadata Metadata var metadata Metadata
d := json.NewDecoder(f) d := json.NewDecoder(f)
err = d.Decode(&metadata) err = d.Decode(&metadata)
if err != nil { if err != nil {
f.Close()
return err return err
} }
f.Seek(0, io.SeekStart) f.Close()
metadata.Date = dt metadata.Date = dt
f, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0740)
if err != nil {
return err
}
defer f.Close()
e := json.NewEncoder(f) e := json.NewEncoder(f)
err = e.Encode(metadata) err = e.Encode(metadata)
if err != nil { if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"cloudsave/pkg/game" "cloudsave/pkg/game"
"cloudsave/pkg/remote/obj" "cloudsave/pkg/remote/obj"
customtime "cloudsave/pkg/tools/time"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -14,6 +15,8 @@ import (
"os" "os"
"strconv" "strconv"
"time" "time"
"github.com/schollz/progressbar/v3"
) )
type ( type (
@@ -32,6 +35,37 @@ func New(baseURL, username, password string) *Client {
} }
} }
func (c *Client) Exists(gameID string) (bool, error) {
u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "hash")
if err != nil {
return false, err
}
req, err := http.NewRequest("HEAD", u, nil)
if err != nil {
return false, err
}
req.SetBasicAuth(c.username, c.password)
cli := http.Client{}
r, err := cli.Do(req)
if err != nil {
return false, err
}
defer r.Body.Close()
switch r.StatusCode {
case 200:
return true, nil
case 404:
return false, nil
}
return false, fmt.Errorf("an error occured: server response: %s", r.Status)
}
func (c *Client) Hash(gameID string) (string, error) { func (c *Client) Hash(gameID string) (string, error) {
u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "hash") u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "hash")
if err != nil { if err != nil {
@@ -50,40 +84,28 @@ func (c *Client) Hash(gameID string) (string, error) {
return "", errors.New("invalid payload sent by the server") return "", errors.New("invalid payload sent by the server")
} }
func (c *Client) Version(gameID string) (int, error) { func (c *Client) Metadata(gameID string) (game.Metadata, error) {
u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "version") u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "metadata")
if err != nil { if err != nil {
return 0, err return game.Metadata{}, err
} }
o, err := c.get(u) o, err := c.get(u)
if err != nil { if err != nil {
return 0, err return game.Metadata{}, err
} }
if h, ok := (o.Data).(float64); ok { if m, ok := (o.Data).(map[string]any); ok {
return int(h), nil gm := game.Metadata{
ID: m["id"].(string),
Name: m["name"].(string),
Version: int(m["version"].(float64)),
Date: customtime.MustParse(time.RFC3339, m["date"].(string)),
}
return gm, nil
} }
return 0, errors.New("invalid payload sent by the server") return game.Metadata{}, 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 {
@@ -112,6 +134,7 @@ func (c *Client) Push(gameID, archivePath string, m game.Metadata) error {
writer.WriteField("name", m.Name) writer.WriteField("name", m.Name)
writer.WriteField("version", strconv.Itoa(m.Version)) writer.WriteField("version", strconv.Itoa(m.Version))
writer.WriteField("date", m.Date.Format(time.RFC3339))
if err := writer.Close(); err != nil { if err := writer.Close(); err != nil {
return err return err
@@ -171,7 +194,13 @@ func (c *Client) Pull(gameID, archivePath string) error {
return fmt.Errorf("cannot connect to remote: server return code: %s", res.Status) return fmt.Errorf("cannot connect to remote: server return code: %s", res.Status)
} }
if _, err := io.Copy(f, req.Body); err != nil { bar := progressbar.DefaultBytes(
res.ContentLength,
"Pulling...",
)
defer bar.Close()
if _, err := io.Copy(io.MultiWriter(f, bar), res.Body); err != nil {
return fmt.Errorf("an error occured while copying the file from the remote: %w", err) return fmt.Errorf("an error occured while copying the file from the remote: %w", err)
} }

11
pkg/tools/time/time.go Normal file
View File

@@ -0,0 +1,11 @@
package time
import "time"
func MustParse(layout, value string) time.Time {
t, err := time.Parse(layout, value)
if err != nil {
panic(err)
}
return t
}