diff --git a/cmd/cli/commands/pull/pull.go b/cmd/cli/commands/pull/pull.go new file mode 100644 index 0000000..a20d4ef --- /dev/null +++ b/cmd/cli/commands/pull/pull.go @@ -0,0 +1,88 @@ +package pull + +import ( + "cloudsave/pkg/game" + "cloudsave/pkg/remote/client" + "cloudsave/pkg/tools/archive" + "cloudsave/pkg/tools/prompt/credentials" + "context" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/google/subcommands" +) + +type ( + PullCmd struct { + } +) + +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 +` +} + +func (p *PullCmd) SetFlags(f *flag.FlagSet) { + +} + +func (p *PullCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + if f.NArg() != 3 { + fmt.Fprintln(os.Stderr, "error: missing arguments") + return subcommands.ExitUsageError + } + + url := f.Arg(0) + gameID := f.Arg(1) + path := f.Arg(2) + + username, password, err := credentials.Read() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read std output: %s", err) + return subcommands.ExitFailure + } + + cli := client.New(url, username, password) + + if err := cli.Ping(); err != nil { + fmt.Fprintf(os.Stderr, "failed to connect to the remote: %s", err) + return subcommands.ExitFailure + } + + archivePath := filepath.Join(game.DatastorePath(), gameID, "data.tar.gz") + + m, err := cli.Metadata(gameID) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to get metadata: %s", err) + return subcommands.ExitFailure + } + + err = game.Register(m, path) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to register local metadata: %s", err) + return subcommands.ExitFailure + } + + if err := cli.Pull(gameID, archivePath); err != nil { + fmt.Fprintf(os.Stderr, "failed to pull from the remote: %s", err) + return subcommands.ExitFailure + } + + fi, err := os.OpenFile(archivePath, os.O_RDONLY, 0) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open archive: %s", err) + return subcommands.ExitFailure + } + + if err := archive.Untar(fi, path); err != nil { + fmt.Fprintf(os.Stderr, "failed to unarchive file: %s", err) + return subcommands.ExitFailure + } + + return subcommands.ExitSuccess +} diff --git a/cmd/cli/commands/version/version.go b/cmd/cli/commands/version/version.go index e20a341..79c5bf5 100644 --- a/cmd/cli/commands/version/version.go +++ b/cmd/cli/commands/version/version.go @@ -2,9 +2,12 @@ package version import ( "cloudsave/pkg/constants" + "cloudsave/pkg/remote/client" + "cloudsave/pkg/tools/prompt/credentials" "context" "flag" "fmt" + "os" "runtime" "strconv" @@ -13,6 +16,7 @@ import ( type ( VersionCmd struct { + remote bool } ) @@ -25,14 +29,60 @@ func (*VersionCmd) Usage() string { } func (p *VersionCmd) SetFlags(f *flag.FlagSet) { + f.BoolVar(&p.remote, "a", false, "get a remote version information") } func (p *VersionCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + 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 + } + local() + return subcommands.ExitSuccess +} + +func local() { fmt.Println("Client: CloudSave cli") fmt.Println(" Version: " + constants.Version) fmt.Println(" API version: " + strconv.Itoa(constants.ApiVersion)) fmt.Println(" Go version: " + runtime.Version()) fmt.Println(" OS/Arch: " + runtime.GOOS + "/" + runtime.GOARCH) - - return subcommands.ExitSuccess +} + +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) + } + + info, err := cli.Version() + if err != nil { + return fmt.Errorf("failed to load games from remote: %w", err) + } + + fmt.Println() + fmt.Println("Remote:", url) + fmt.Println("---") + fmt.Println("Server:") + fmt.Println(" Version: " + info.Version) + fmt.Println(" API version: " + strconv.Itoa(info.APIVersion)) + fmt.Println(" Go version: " + info.GoVersion) + fmt.Println(" OS/Arch: " + info.OSName + "/" + info.OSArchitecture) + + return nil } diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 8bd433d..db4ce2a 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -3,6 +3,7 @@ package main import ( "cloudsave/cmd/cli/commands/add" "cloudsave/cmd/cli/commands/list" + "cloudsave/cmd/cli/commands/pull" "cloudsave/cmd/cli/commands/remote" "cloudsave/cmd/cli/commands/remove" "cloudsave/cmd/cli/commands/run" @@ -28,6 +29,7 @@ func main() { subcommands.Register(&remote.RemoteCmd{}, "remote") subcommands.Register(&sync.SyncCmd{}, "remote") + subcommands.Register(&pull.PullCmd{}, "remote") flag.Parse() ctx := context.Background() diff --git a/pkg/game/game.go b/pkg/game/game.go index 267976c..f3ca35d 100644 --- a/pkg/game/game.go +++ b/pkg/game/game.go @@ -68,6 +68,29 @@ func Add(name, path string) (Metadata, error) { return m, nil } +func Register(m Metadata, path string) error { + m.Path = path + + err := os.MkdirAll(filepath.Join(datastorepath, m.ID), 0740) + if err != nil { + panic("cannot make directory for the game:" + err.Error()) + } + + f, err := os.OpenFile(filepath.Join(datastorepath, m.ID, "metadata.json"), os.O_CREATE|os.O_WRONLY, 0740) + if err != nil { + return fmt.Errorf("cannot open the metadata file in the datastore: %w", err) + } + defer f.Close() + + e := json.NewEncoder(f) + err = e.Encode(m) + if err != nil { + return fmt.Errorf("cannot write into the metadata file in the datastore: %w", err) + } + + return nil +} + func All() ([]Metadata, error) { ds, err := os.ReadDir(datastorepath) if err != nil { diff --git a/pkg/remote/client/client.go b/pkg/remote/client/client.go index a0a3d07..cafca98 100644 --- a/pkg/remote/client/client.go +++ b/pkg/remote/client/client.go @@ -25,6 +25,14 @@ type ( username string password string } + + Information struct { + Version string `json:"version"` + APIVersion int `json:"api_version"` + GoVersion string `json:"go_version"` + OSName string `json:"os_name"` + OSArchitecture string `json:"os_architecture"` + } ) func New(baseURL, username, password string) *Client { @@ -66,6 +74,31 @@ func (c *Client) Exists(gameID string) (bool, error) { return false, fmt.Errorf("an error occured: server response: %s", r.Status) } +func (c *Client) Version() (Information, error) { + u, err := url.JoinPath(c.baseURL, "api", "v1", "version") + if err != nil { + return Information{}, err + } + + o, err := c.get(u) + if err != nil { + return Information{}, err + } + + if info, ok := (o.Data).(map[string]any); ok { + i := Information{ + Version: info["version"].(string), + APIVersion: int(info["api_version"].(float64)), + GoVersion: info["go_version"].(string), + OSName: info["os_name"].(string), + OSArchitecture: info["os_architecture"].(string), + } + return i, nil + } + + return Information{}, errors.New("invalid payload sent by the server") +} + func (c *Client) Hash(gameID string) (string, error) { u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "hash") if err != nil { diff --git a/pkg/tools/archive/archive.go b/pkg/tools/archive/archive.go new file mode 100644 index 0000000..63b1688 --- /dev/null +++ b/pkg/tools/archive/archive.go @@ -0,0 +1,73 @@ +package archive + +import ( + "archive/tar" + "compress/gzip" + "io" + "os" + "path/filepath" +) + +func Untar(file io.Reader, path string) error { + gzr, err := gzip.NewReader(file) + if err != nil { + return err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + for { + header, err := tr.Next() + + switch { + + // if no more files are found return + case err == io.EOF: + return nil + + // return any other error + case err != nil: + return err + + // if the header is nil, just skip it (not sure how this happens) + case header == nil: + continue + } + + // the target location where the dir/file should be created + target := filepath.Join(path, header.Name) + + // the following switch could also be done using fi.Mode(), not sure if there + // a benefit of using one vs. the other. + // fi := header.FileInfo() + + // check the file type + switch header.Typeflag { + + // if its a dir and it doesn't exist create it + case tar.TypeDir: + if _, err := os.Stat(target); err != nil { + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + } + + // if it's a file create it + case tar.TypeReg: + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return err + } + + // copy over contents + if _, err := io.Copy(f, tr); err != nil { + return err + } + + // manually close here after each file operation; defering would cause each file close + // to wait until all operations have completed. + f.Close() + } + } +}