diff --git a/cmd/cli/commands/add/add.go b/cmd/cli/commands/add/add.go index 5e1c629..e4d9974 100644 --- a/cmd/cli/commands/add/add.go +++ b/cmd/cli/commands/add/add.go @@ -1,32 +1,39 @@ package add import ( + "cloudsave/pkg/remote" "cloudsave/pkg/repository" "context" "flag" "fmt" "os" "path/filepath" + "strings" "github.com/google/subcommands" ) type ( AddCmd struct { - name string + name string + remote string } ) func (*AddCmd) Name() string { return "add" } -func (*AddCmd) Synopsis() string { return "Add a folder to the sync list" } +func (*AddCmd) Synopsis() string { return "add a folder to the sync list" } func (*AddCmd) Usage() string { - return `add: - Add a folder to the sync list + return `Usage: cloudsave add [-name] [-remote] + +Add a folder to the track list + +Options: ` } func (p *AddCmd) SetFlags(f *flag.FlagSet) { f.StringVar(&p.name, "name", "", "Override the name of the game") + f.StringVar(&p.remote, "remote", "", "Defines a remote server to sync with") } func (p *AddCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { @@ -50,6 +57,10 @@ func (p *AddCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s return subcommands.ExitFailure } + if len(strings.TrimSpace(p.remote)) > 0 { + remote.Set(m.ID, p.remote) + } + fmt.Println(m.ID) return subcommands.ExitSuccess diff --git a/cmd/cli/commands/apply/apply.go b/cmd/cli/commands/apply/apply.go index 0e3e389..b6610d0 100644 --- a/cmd/cli/commands/apply/apply.go +++ b/cmd/cli/commands/apply/apply.go @@ -20,8 +20,9 @@ type ( func (*ListCmd) Name() string { return "apply" } func (*ListCmd) Synopsis() string { return "apply a backup" } func (*ListCmd) Usage() string { - return `apply: - Apply a backup + return `Usage: cloudsave apply + +Apply a backup ` } diff --git a/cmd/cli/commands/list/list.go b/cmd/cli/commands/list/list.go index 8987ae3..3b80f24 100644 --- a/cmd/cli/commands/list/list.go +++ b/cmd/cli/commands/list/list.go @@ -22,8 +22,11 @@ type ( func (*ListCmd) Name() string { return "list" } func (*ListCmd) Synopsis() string { return "list all game registered" } func (*ListCmd) Usage() string { - return `list: - List all game registered + return `Usage: cloudsave list [-include-backup] [-a] + +List all game registered + +Options: ` } diff --git a/cmd/cli/commands/pull/pull.go b/cmd/cli/commands/pull/pull.go index 5a0da4e..4490d1c 100644 --- a/cmd/cli/commands/pull/pull.go +++ b/cmd/cli/commands/pull/pull.go @@ -1,10 +1,10 @@ package pull import ( + "cloudsave/cmd/cli/tools/prompt/credentials" "cloudsave/pkg/remote/client" "cloudsave/pkg/repository" "cloudsave/pkg/tools/archive" - "cloudsave/cmd/cli/tools/prompt/credentials" "context" "flag" "fmt" @@ -22,8 +22,9 @@ type ( func (*PullCmd) Name() string { return "pull" } func (*PullCmd) Synopsis() string { return "pull a game save from the remote" } func (*PullCmd) Usage() string { - return `list: - Pull a game save from the remote + return `Usage: cloudsave pull + +Pull a game save from the remote ` } diff --git a/cmd/cli/commands/remote/remote.go b/cmd/cli/commands/remote/remote.go index e47cfc5..d35a1c5 100644 --- a/cmd/cli/commands/remote/remote.go +++ b/cmd/cli/commands/remote/remote.go @@ -20,10 +20,17 @@ type ( ) func (*RemoteCmd) Name() string { return "remote" } -func (*RemoteCmd) Synopsis() string { return "manage remote" } +func (*RemoteCmd) Synopsis() string { return "add or update the remote url" } func (*RemoteCmd) Usage() string { - return `remote: - manage remove + return `Usage: cloudsave remote <-set|-list> + +The -list argument lists all remotes for each registered game. +This command performs a connection test. + +The -set argument allow you to set (create or update) +the URL to the remote for a game + +Options ` } diff --git a/cmd/cli/commands/remove/remove.go b/cmd/cli/commands/remove/remove.go index 2459616..bdfc1e1 100644 --- a/cmd/cli/commands/remove/remove.go +++ b/cmd/cli/commands/remove/remove.go @@ -17,8 +17,10 @@ type ( func (*RemoveCmd) Name() string { return "remove" } func (*RemoveCmd) Synopsis() string { return "unregister a game" } func (*RemoveCmd) Usage() string { - return `remove: - Unregister a game + return `Usage: cloudsave remove + +Unregister a game +Caution: all the backup are deleted ` } diff --git a/cmd/cli/commands/run/run.go b/cmd/cli/commands/run/run.go index 45335ab..8c260fb 100644 --- a/cmd/cli/commands/run/run.go +++ b/cmd/cli/commands/run/run.go @@ -20,11 +20,14 @@ type ( } ) -func (*RunCmd) Name() string { return "run" } -func (*RunCmd) Synopsis() string { return "Check and process all the folder" } +func (*RunCmd) Name() string { return "scan" } +func (*RunCmd) Synopsis() string { return "check and process all the folder" } func (*RunCmd) Usage() string { - return `run: - Check and process all the folder + return `Usage: cloudsave scan + +Check if the files have been modified. If so, +the current archive is moved to the backup list +and a new archive is created with a new version number. ` } diff --git a/cmd/cli/commands/sync/sync.go b/cmd/cli/commands/sync/sync.go index 9fe9e5d..9702fbb 100644 --- a/cmd/cli/commands/sync/sync.go +++ b/cmd/cli/commands/sync/sync.go @@ -27,8 +27,9 @@ type ( func (*SyncCmd) Name() string { return "sync" } func (*SyncCmd) Synopsis() string { return "list all game registered" } func (*SyncCmd) Usage() string { - return `add: - List all game registered + return `Usage: cloudsave sync + +Synchronize the archives with the server defined for each game. ` } @@ -47,6 +48,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) r, err := remote.One(g.ID) if err != nil { if errors.Is(err, remote.ErrNoRemote) { + fmt.Println(g.Name + ": no remote configured") continue } fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err) @@ -85,6 +87,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) destroyPg() slog.Warn("failed to push backup files", "err", err) } + fmt.Println(g.Name + ": pushed") continue } @@ -136,7 +139,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) continue } } - fmt.Println("already up-to-date") + fmt.Println(g.Name + ": already up-to-date") continue } @@ -148,6 +151,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) return subcommands.ExitFailure } destroyPg() + fmt.Println(g.Name + ": pushed") continue } @@ -168,6 +172,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err) continue } + fmt.Println(g.Name + ": pulled") continue } diff --git a/cmd/cli/commands/version/version.go b/cmd/cli/commands/version/version.go index 98d6d20..0325b3b 100644 --- a/cmd/cli/commands/version/version.go +++ b/cmd/cli/commands/version/version.go @@ -1,9 +1,9 @@ package version import ( + "cloudsave/cmd/cli/tools/prompt/credentials" "cloudsave/pkg/constants" "cloudsave/pkg/remote/client" - "cloudsave/cmd/cli/tools/prompt/credentials" "context" "flag" "fmt" @@ -23,8 +23,11 @@ type ( func (*VersionCmd) Name() string { return "version" } func (*VersionCmd) Synopsis() string { return "show version and system information" } func (*VersionCmd) Usage() string { - return `add: - Show version and system information + return `Usage: cloudsave version [-a] + +Print the version of the software + +Options: ` } diff --git a/cmd/server/data/data.go b/cmd/server/data/data.go index 6b1c553..fd38fbb 100644 --- a/cmd/server/data/data.go +++ b/cmd/server/data/data.go @@ -9,12 +9,50 @@ import ( "io" "os" "path/filepath" + "sync" +) + +type ( + cache map[string]cachedInfo + + cachedInfo struct { + MD5 string + Version int + } ) var ( ErrBackupNotExists error = errors.New("backup not found") + + // singleton + hashCacheMu sync.RWMutex + hashCache cache = make(map[string]cachedInfo) ) +func (c cache) Get(gameID string) (cachedInfo, bool) { + hashCacheMu.RLock() + defer hashCacheMu.RUnlock() + + if v, ok := c[gameID]; ok { + return v, true + } + return cachedInfo{}, false +} + +func (c cache) Register(gameID string, v cachedInfo) { + hashCacheMu.Lock() + defer hashCacheMu.Unlock() + + c[gameID] = v +} + +func (c cache) Remove(gameID string) { + hashCacheMu.Lock() + defer hashCacheMu.Unlock() + + delete(c, gameID) +} + func Write(gameID, documentRoot string, r io.Reader) error { dataFolderPath := filepath.Join(documentRoot, "data", gameID) partPath := filepath.Join(dataFolderPath, "data.tar.gz.part") @@ -42,6 +80,7 @@ func Write(gameID, documentRoot string, r io.Reader) error { return err } + hashCache.Remove(gameID) return nil } @@ -97,6 +136,7 @@ func UpdateMetadata(gameID, documentRoot string, m repository.Metadata) error { func ArchiveInfo(gameID, documentRoot, uuid string) (repository.Backup, error) { dataFolderPath := filepath.Join(documentRoot, "data", gameID, "hist", uuid, "data.tar.gz") + cacheID := gameID + ":" + uuid finfo, err := os.Stat(dataFolderPath) if err != nil { @@ -106,11 +146,29 @@ func ArchiveInfo(gameID, documentRoot, uuid string) (repository.Backup, error) { return repository.Backup{}, err } + v, err := getVersion(gameID, documentRoot) + if err != nil { + return repository.Backup{}, fmt.Errorf("failed to read game metadata: %w", err) + } + + if m, ok := hashCache.Get(cacheID); ok { + return repository.Backup{ + CreatedAt: finfo.ModTime(), + UUID: uuid, + MD5: m.MD5, + }, nil + } + h, err := hash.FileMD5(dataFolderPath) if err != nil { return repository.Backup{}, fmt.Errorf("failed to calculate file md5: %w", err) } + hashCache.Register(cacheID, cachedInfo{ + Version: v, + MD5: h, + }) + return repository.Backup{ CreatedAt: finfo.ModTime(), UUID: uuid, @@ -118,6 +176,62 @@ func ArchiveInfo(gameID, documentRoot, uuid string) (repository.Backup, error) { }, nil } +func Hash(gameID, documentRoot string) (string, error) { + path := filepath.Clean(filepath.Join(documentRoot, "data", gameID)) + + sdir, err := os.Stat(path) + if err != nil { + return "", err + } + + if !sdir.IsDir() { + return "", err + } + + v, err := getVersion(gameID, documentRoot) + if err != nil { + return "", fmt.Errorf("failed to read game metadata: %w", err) + } + + if m, ok := hashCache.Get(gameID); ok { + if v == m.Version { + return m.MD5, nil + } + } + + path = filepath.Join(path, "data.tar.gz") + + h, err := hash.FileMD5(path) + if err != nil { + return "", err + } + + hashCache.Register(gameID, cachedInfo{ + Version: v, + MD5: h, + }) + + return h, nil +} + +func getVersion(gameID, documentRoot string) (int, error) { + path := filepath.Join(documentRoot, "data", gameID, "metadata.json") + + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0740) + if err != nil { + return 0, err + } + defer f.Close() + + d := json.NewDecoder(f) + var m repository.Metadata + if err := d.Decode(&m); err != nil { + return 0, err + } + + return m.Version, nil +} + func makeDataFolder(gameID, documentRoot string) error { if err := os.MkdirAll(filepath.Join(documentRoot, "data", gameID), 0740); err != nil { return err