This commit is contained in:
2025-07-29 17:34:27 +02:00
parent 68e13938b7
commit 7b3b91c998
6 changed files with 271 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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