This commit is contained in:
2025-07-29 17:04:45 +02:00
parent d8e0bffe56
commit 68e13938b7
10 changed files with 276 additions and 176 deletions

View File

@@ -41,7 +41,7 @@ func (p *AddCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
}
if p.name == "" {
p.name = filepath.Base(filepath.Dir(path))
p.name = filepath.Base(path)
}
m, err := game.Add(p.name, path)

View File

@@ -2,6 +2,8 @@ package list
import (
"cloudsave/pkg/game"
"cloudsave/pkg/remote/client"
"cloudsave/pkg/tools/prompt/credentials"
"context"
"flag"
"fmt"
@@ -12,6 +14,7 @@ import (
type (
ListCmd struct {
remote bool
}
)
@@ -24,20 +27,72 @@ func (*ListCmd) Usage() string {
}
func (p *ListCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&p.remote, "a", false, "list all including remote data")
}
func (p *ListCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
datastore, err := game.All()
if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err)
if p.remote {
if f.NArg() != 1 {
fmt.Fprintln(os.Stderr, "error: missing remote url")
return subcommands.ExitUsageError
}
username, password, err := credentials.Read()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to read std output: %s", err)
return subcommands.ExitFailure
}
if err := remote(f.Arg(0), username, password); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}
if err := local(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
return subcommands.ExitFailure
}
fmt.Println("ID | NAME | PATH")
fmt.Println("-- | ---- | ----")
for _, metadata := range datastore {
fmt.Println(metadata.ID, "|", metadata.Name, "|", metadata.Path)
}
return subcommands.ExitSuccess
}
func local() error {
games, err := game.All()
if err != nil {
return fmt.Errorf("failed to load datastore: %w", err)
}
for _, g := range games {
fmt.Println("ID:", g.ID)
fmt.Println("Name:", g.Name)
fmt.Println("Last Version:", g.Date, "( Version Number", g.Version, ")")
fmt.Println("---")
}
return nil
}
func remote(url, username, password string) error {
cli := client.New(url, username, password)
if err := cli.Ping(); err != nil {
return fmt.Errorf("failed to connect to the remote: %w", err)
}
games, err := cli.All()
if err != nil {
return fmt.Errorf("failed to load games from remote: %w", err)
}
fmt.Println()
fmt.Println("Remote:", url)
fmt.Println("---")
for _, g := range games {
fmt.Println("ID:", g.ID)
fmt.Println("Name:", g.Name)
fmt.Println("Last Version:", g.Date, "( Version Number", g.Version, ")")
fmt.Println("---")
}
return nil
}

View File

@@ -1,7 +1,9 @@
package remote
import (
"cloudsave/pkg/game"
"cloudsave/pkg/remote"
"cloudsave/pkg/remote/client"
"context"
"flag"
"fmt"
@@ -30,38 +32,60 @@ func (p *RemoteCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&p.set, "set", false, "set remote for a game")
}
func (p *RemoteCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if p.list {
remotes, err := remote.All()
if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err)
return subcommands.ExitFailure
func (p *RemoteCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
switch {
case p.list:
{
if err := list(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
return subcommands.ExitFailure
}
}
fmt.Println("ID | REMOTE URL")
fmt.Println("-- | ----------")
for _, remote := range remotes {
fmt.Println(remote.GameID, "|", remote.URL)
case p.set:
{
if f.NArg() != 2 {
subcommands.HelpCommand().Execute(ctx, f, nil)
return subcommands.ExitUsageError
}
if err := set(f.Arg(0), f.Arg(1)); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
return subcommands.ExitFailure
}
}
return subcommands.ExitSuccess
}
if p.set {
if f.NArg() != 2 {
fmt.Fprintln(os.Stderr, "error: the command is expecting for 2 arguments")
f.Usage()
default:
{
subcommands.HelpCommand().Execute(ctx, f, nil)
return subcommands.ExitUsageError
}
}
return subcommands.ExitSuccess
}
err := remote.Set(f.Arg(0), f.Arg(1))
if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to set remote:", err)
return subcommands.ExitFailure
}
fmt.Println(f.Arg(0))
return subcommands.ExitSuccess
func list() error {
games, err := game.All()
if err != nil {
return fmt.Errorf("failed to load datastore: %w", err)
}
f.Usage()
return subcommands.ExitUsageError
for _, g := range games {
r, err := remote.One(g.ID)
if err != nil {
return fmt.Errorf("failed to load datastore: %w", err)
}
cli := client.New(r.URL, "", "")
status := "OK"
if err := cli.Ping(); err != nil {
status = "ERROR: " + err.Error()
}
fmt.Printf("'%s' -> %s (%s)\n", g.Name, r.URL, status)
}
return nil
}
func set(gameID, url string) error {
return remote.Set(gameID, url)
}

View File

@@ -7,6 +7,7 @@ import (
"cloudsave/pkg/remote/client"
"cloudsave/pkg/tools/prompt/credentials"
"context"
"errors"
"flag"
"fmt"
"log/slog"
@@ -34,45 +35,37 @@ func (p *SyncCmd) SetFlags(f *flag.FlagSet) {
}
func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
remotes, err := remote.All()
games, err := game.All()
if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err)
return subcommands.ExitFailure
}
if len(remotes) == 0 {
fmt.Println("nothing to do: no remote found")
return subcommands.ExitSuccess
}
username, password, err := credentials.Read()
if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to read std output:", err)
return subcommands.ExitFailure
}
for _, r := range remotes {
m, err := game.One(r.GameID)
remoteCred := make(map[string]map[string]string)
for _, g := range games {
r, err := remote.One(g.ID)
if err != nil {
fmt.Fprintln(os.Stderr, "error: cannot get metadata for this game: %w", err)
if errors.Is(err, remote.ErrNoRemote) {
continue
}
fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err)
return subcommands.ExitFailure
}
client := client.New(r.URL, username, password)
if !client.Ping() {
slog.Warn("remote is unavailable", "url", r.URL)
continue
cli, err := connect(remoteCred, r)
if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to connect to the remote:", err)
return subcommands.ExitFailure
}
exists, err := client.Exists(r.GameID)
exists, err := cli.Exists(r.GameID)
if err != nil {
slog.Error(err.Error())
continue
}
if !exists {
if err := push(r.GameID, m, client); err != nil {
if err := push(r.GameID, g, cli); err != nil {
fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure
}
@@ -85,7 +78,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue
}
hremote, err := client.Hash(r.GameID)
hremote, err := cli.Hash(r.GameID)
if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to get the file hash from the remote:", err)
continue
@@ -97,7 +90,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue
}
remoteMetadata, err := client.Metadata(r.GameID)
remoteMetadata, err := cli.Metadata(r.GameID)
if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to get the game metadata from the remote:", err)
continue
@@ -116,7 +109,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
}
if vlocal > remoteMetadata.Version {
if err := push(r.GameID, m, client); err != nil {
if err := push(r.GameID, g, cli); err != nil {
fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure
}
@@ -124,7 +117,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
}
if vlocal < remoteMetadata.Version {
if err := pull(r.GameID, client); err != nil {
if err := pull(r.GameID, cli); err != nil {
fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure
}
@@ -140,56 +133,58 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
}
if vlocal == remoteMetadata.Version {
g, err := game.One(r.GameID)
if err != nil {
slog.Warn("a conflict was found but the game is not found in the database")
slog.Debug("debug info", "gameID", r.GameID)
if err := conflict(r.GameID, g, remoteMetadata, cli); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to resolve conflict:", err)
continue
}
fmt.Println()
fmt.Println("--- /!\\ CONFLICT ---")
fmt.Println("----")
fmt.Println(g.Name, "(", g.Path, ")")
fmt.Println("----")
fmt.Println("Your version:", g.Date.Format(time.RFC1123))
fmt.Println("Their version:", remoteMetadata.Date.Format(time.RFC1123))
fmt.Println()
res := prompt.Conflict()
switch res {
case prompt.My:
{
if err := push(r.GameID, m, client); err != nil {
fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure
}
}
case prompt.Their:
{
if err := pull(r.GameID, client); err != nil {
fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure
}
if err := game.SetVersion(r.GameID, remoteMetadata.Version); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err)
continue
}
if err := game.SetDate(r.GameID, remoteMetadata.Date); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err)
continue
}
}
}
continue
}
}
return subcommands.ExitSuccess
}
func conflict(gameID string, m, remoteMetadata game.Metadata, cli *client.Client) error {
g, err := game.One(gameID)
if err != nil {
slog.Warn("a conflict was found but the game is not found in the database")
slog.Debug("debug info", "gameID", gameID)
return nil
}
fmt.Println()
fmt.Println("--- /!\\ CONFLICT ---")
fmt.Println(g.Name, "(", g.Path, ")")
fmt.Println("----")
fmt.Println("Your version:", g.Date.Format(time.RFC1123))
fmt.Println("Their version:", remoteMetadata.Date.Format(time.RFC1123))
fmt.Println()
res := prompt.Conflict()
switch res {
case prompt.My:
{
if err := push(gameID, m, cli); err != nil {
return fmt.Errorf("failed to push: %w", err)
}
}
case prompt.Their:
{
if err := pull(gameID, cli); err != nil {
return fmt.Errorf("failed to push: %w", err)
}
if err := game.SetVersion(gameID, remoteMetadata.Version); err != nil {
return fmt.Errorf("failed to synchronize version number: %w", err)
}
if err := game.SetDate(gameID, remoteMetadata.Date); err != nil {
return fmt.Errorf("failed to synchronize date: %w", err)
}
}
}
return nil
}
func push(gameID string, m game.Metadata, cli *client.Client) error {
archivePath := filepath.Join(game.DatastorePath(), gameID, "data.tar.gz")
@@ -201,3 +196,30 @@ func pull(gameID string, cli *client.Client) error {
return cli.Pull(gameID, archivePath)
}
func connect(remoteCred map[string]map[string]string, r remote.Remote) (*client.Client, error) {
var cli *client.Client
if v, ok := remoteCred[r.URL]; ok {
cli = client.New(r.URL, v["username"], v["password"])
return cli, nil
}
username, password, err := credentials.Read()
if err != nil {
return nil, fmt.Errorf("failed to read std output: %w", err)
}
cli = client.New(r.URL, username, password)
if err := cli.Ping(); err != nil {
return nil, fmt.Errorf("failed to connect to the remote: %w", err)
}
c := make(map[string]string)
c["username"] = username
c["password"] = password
remoteCred[r.URL] = c
return cli, nil
}

View File

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