starting 0.0.2 dev

This commit is contained in:
2025-07-30 00:49:22 +02:00
parent c099d3e64f
commit c6edb91f29
16 changed files with 287 additions and 114 deletions

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
MAKE_PACKAGE=false MAKE_PACKAGE=false
VERSION=0.0.1 VERSION=0.0.2
usage() { usage() {
echo "Usage: $0 [OPTIONS]" echo "Usage: $0 [OPTIONS]"

View File

@@ -1,7 +1,7 @@
package add package add
import ( import (
"cloudsave/pkg/game" "cloudsave/pkg/repository"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -44,7 +44,7 @@ func (p *AddCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
p.name = filepath.Base(path) p.name = filepath.Base(path)
} }
m, err := game.Add(p.name, path) m, err := repository.Add(p.name, path)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to add game reference:", err) fmt.Fprintln(os.Stderr, "error: failed to add game reference:", err)
return subcommands.ExitFailure return subcommands.ExitFailure

View File

@@ -1,9 +1,9 @@
package list package list
import ( import (
"cloudsave/pkg/game"
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
"cloudsave/pkg/tools/prompt/credentials" "cloudsave/pkg/repository"
"cloudsave/cmd/cli/tools/prompt/credentials"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -57,7 +57,7 @@ func (p *ListCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
} }
func local() error { func local() error {
games, err := game.All() games, err := repository.All()
if err != nil { if err != nil {
return fmt.Errorf("failed to load datastore: %w", err) return fmt.Errorf("failed to load datastore: %w", err)
} }

View File

@@ -1,10 +1,10 @@
package pull package pull
import ( import (
"cloudsave/pkg/game"
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
"cloudsave/pkg/repository"
"cloudsave/pkg/tools/archive" "cloudsave/pkg/tools/archive"
"cloudsave/pkg/tools/prompt/credentials" "cloudsave/cmd/cli/tools/prompt/credentials"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -54,7 +54,7 @@ func (p *PullCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
return subcommands.ExitFailure return subcommands.ExitFailure
} }
archivePath := filepath.Join(game.DatastorePath(), gameID, "data.tar.gz") archivePath := filepath.Join(repository.DatastorePath(), gameID, "data.tar.gz")
m, err := cli.Metadata(gameID) m, err := cli.Metadata(gameID)
if err != nil { if err != nil {
@@ -62,7 +62,7 @@ func (p *PullCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
return subcommands.ExitFailure return subcommands.ExitFailure
} }
err = game.Register(m, path) err = repository.Register(m, path)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to register local metadata: %s", err) fmt.Fprintf(os.Stderr, "failed to register local metadata: %s", err)
return subcommands.ExitFailure return subcommands.ExitFailure

View File

@@ -1,9 +1,9 @@
package remote package remote
import ( import (
"cloudsave/pkg/game"
"cloudsave/pkg/remote" "cloudsave/pkg/remote"
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
"cloudsave/pkg/repository"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -62,7 +62,7 @@ func (p *RemoteCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface
} }
func list() error { func list() error {
games, err := game.All() games, err := repository.All()
if err != nil { if err != nil {
return fmt.Errorf("failed to load datastore: %w", err) return fmt.Errorf("failed to load datastore: %w", err)
} }

View File

@@ -1,7 +1,7 @@
package remove package remove
import ( import (
"cloudsave/pkg/game" "cloudsave/pkg/repository"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -31,7 +31,7 @@ func (p *RemoveCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
return subcommands.ExitUsageError return subcommands.ExitUsageError
} }
err := game.Remove(f.Arg(0)) err := repository.Remove(f.Arg(0))
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to unregister the game:", err) fmt.Fprintln(os.Stderr, "error: failed to unregister the game:", err)
return subcommands.ExitFailure return subcommands.ExitFailure

View File

@@ -1,9 +1,8 @@
package run package run
import ( import (
"archive/tar" "cloudsave/pkg/repository"
"cloudsave/pkg/game" "cloudsave/pkg/tools/archive"
"compress/gzip"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -32,7 +31,7 @@ func (*RunCmd) Usage() string {
func (p *RunCmd) SetFlags(f *flag.FlagSet) {} func (p *RunCmd) SetFlags(f *flag.FlagSet) {}
func (p *RunCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { func (p *RunCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
datastore, err := game.All() datastore, err := repository.All()
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err) fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
@@ -43,18 +42,18 @@ func (p *RunCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
for _, metadata := range datastore { for _, metadata := range datastore {
pg.Describe("Scanning " + metadata.Name + "...") pg.Describe("Scanning " + metadata.Name + "...")
metadataPath := filepath.Join(game.DatastorePath(), metadata.ID) metadataPath := filepath.Join(repository.DatastorePath(), metadata.ID)
//todo transaction //todo transaction
err := archiveIfChanged(metadata.ID, metadata.Path, filepath.Join(metadataPath, "data.tar.gz"), filepath.Join(metadataPath, ".last_run")) err := archiveIfChanged(metadata.ID, metadata.Path, filepath.Join(metadataPath, "data.tar.gz"), filepath.Join(metadataPath, ".last_run"))
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err) fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
if err := game.SetVersion(metadata.ID, metadata.Version+1); err != nil { if err := repository.SetVersion(metadata.ID, metadata.Version+1); err != nil {
fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err) fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
if err := game.SetDate(metadata.ID, time.Now()); err != nil { if err := repository.SetDate(metadata.ID, time.Now()); err != nil {
fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err) fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
@@ -69,12 +68,12 @@ func (p *RunCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
// archiveIfChanged will archive srcDir into destTarGz only if any file // archiveIfChanged will archive srcDir into destTarGz only if any file
// in srcDir has a modification time > the last run time stored in stateFile. // in srcDir has a modification time > the last run time stored in stateFile.
// After archiving, it updates stateFile to the current time. // After archiving, it updates stateFile to the current time.
func archiveIfChanged(id, srcDir, destTarGz, stateFile string) error { func archiveIfChanged(gameID, srcDir, destTarGz, stateFile string) error {
// 1) Load last run time // load last run time
var lastRun time.Time var lastRun time.Time
data, err := os.ReadFile(stateFile) data, err := os.ReadFile(stateFile)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("reading state file: %w", err) return fmt.Errorf("failed to reading state file: %w", err)
} }
if err == nil { if err == nil {
lastRun, err = time.Parse(time.RFC3339, string(data)) lastRun, err = time.Parse(time.RFC3339, string(data))
@@ -83,7 +82,7 @@ func archiveIfChanged(id, srcDir, destTarGz, stateFile string) error {
} }
} }
// 2) Check for changes // check for changes
changed := false changed := false
err = filepath.Walk(srcDir, func(path string, info os.FileInfo, walkErr error) error { err = filepath.Walk(srcDir, func(path string, info os.FileInfo, walkErr error) error {
if walkErr != nil { if walkErr != nil {
@@ -96,63 +95,29 @@ func archiveIfChanged(id, srcDir, destTarGz, stateFile string) error {
return nil return nil
}) })
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return fmt.Errorf("scanning source directory: %w", err) return fmt.Errorf("failed to scanning source directory: %w", err)
} }
if !changed { if !changed {
return nil return nil
} }
// 3) Create tar.gz // make a backup
if err := repository.Archive(gameID); err != nil {
return fmt.Errorf("failed to archive data: %w", err)
}
// create archive
f, err := os.Create(destTarGz) f, err := os.Create(destTarGz)
if err != nil { if err != nil {
return fmt.Errorf("creating archive file: %w", err) return fmt.Errorf("failed to creating archive file: %w", err)
} }
defer f.Close() defer f.Close()
gw := gzip.NewWriter(f) if err := archive.Tar(f, srcDir); err != nil {
defer gw.Close() return fmt.Errorf("failed archiving files")
tw := tar.NewWriter(gw)
defer tw.Close()
// Walk again to add files
err = filepath.Walk(srcDir, func(path string, info os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
// Create tar header
header, err := tar.FileInfoHeader(info, path)
if err != nil {
return err
}
// Preserve directory structure relative to srcDir
relPath, err := filepath.Rel(filepath.Dir(srcDir), path)
if err != nil {
return err
}
header.Name = relPath
if err := tw.WriteHeader(header); err != nil {
return err
}
if info.Mode().IsRegular() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(tw, file); err != nil {
return err
}
}
return nil
})
if err != nil {
return fmt.Errorf("writing tar entries: %w", err)
} }
// 4) Update state file
now := time.Now().UTC().Format(time.RFC3339) now := time.Now().UTC().Format(time.RFC3339)
if err := os.WriteFile(stateFile, []byte(now), 0644); err != nil { if err := os.WriteFile(stateFile, []byte(now), 0644); err != nil {
return fmt.Errorf("updating state file: %w", err) return fmt.Errorf("updating state file: %w", err)

View File

@@ -2,10 +2,10 @@ package sync
import ( import (
"cloudsave/cmd/cli/tools/prompt" "cloudsave/cmd/cli/tools/prompt"
"cloudsave/pkg/game" "cloudsave/cmd/cli/tools/prompt/credentials"
"cloudsave/pkg/remote" "cloudsave/pkg/remote"
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
"cloudsave/pkg/tools/prompt/credentials" "cloudsave/pkg/repository"
"context" "context"
"errors" "errors"
"flag" "flag"
@@ -35,7 +35,7 @@ func (p *SyncCmd) SetFlags(f *flag.FlagSet) {
} }
func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
games, err := game.All() games, err := repository.All()
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err) fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
@@ -72,7 +72,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
hlocal, err := game.Hash(r.GameID) hlocal, err := repository.Hash(r.GameID)
if err != nil { if err != nil {
slog.Error(err.Error()) slog.Error(err.Error())
continue continue
@@ -84,7 +84,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
vlocal, err := game.Version(r.GameID) vlocal, err := repository.Version(r.GameID)
if err != nil { if err != nil {
slog.Error(err.Error()) slog.Error(err.Error())
continue continue
@@ -99,7 +99,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
if hlocal == hremote { if hlocal == hremote {
if vlocal != remoteMetadata.Version { 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, remoteMetadata.Version); err != nil { if err := repository.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
} }
@@ -121,11 +121,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, remoteMetadata.Version); err != nil { if err := repository.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, remoteMetadata.Date); err != nil { if err := repository.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
} }
@@ -144,8 +144,8 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
return subcommands.ExitSuccess return subcommands.ExitSuccess
} }
func conflict(gameID string, m, remoteMetadata game.Metadata, cli *client.Client) error { func conflict(gameID string, m, remoteMetadata repository.Metadata, cli *client.Client) error {
g, err := game.One(gameID) g, err := repository.One(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", gameID) slog.Debug("debug info", "gameID", gameID)
@@ -174,10 +174,10 @@ func conflict(gameID string, m, remoteMetadata game.Metadata, cli *client.Client
if err := pull(gameID, cli); err != nil { if err := pull(gameID, cli); err != nil {
return fmt.Errorf("failed to push: %w", err) return fmt.Errorf("failed to push: %w", err)
} }
if err := game.SetVersion(gameID, remoteMetadata.Version); err != nil { if err := repository.SetVersion(gameID, remoteMetadata.Version); err != nil {
return fmt.Errorf("failed to synchronize version number: %w", err) return fmt.Errorf("failed to synchronize version number: %w", err)
} }
if err := game.SetDate(gameID, remoteMetadata.Date); err != nil { if err := repository.SetDate(gameID, remoteMetadata.Date); err != nil {
return fmt.Errorf("failed to synchronize date: %w", err) return fmt.Errorf("failed to synchronize date: %w", err)
} }
} }
@@ -185,14 +185,14 @@ func conflict(gameID string, m, remoteMetadata game.Metadata, cli *client.Client
return nil return nil
} }
func push(gameID string, m game.Metadata, cli *client.Client) error { func push(gameID string, m repository.Metadata, cli *client.Client) error {
archivePath := filepath.Join(game.DatastorePath(), gameID, "data.tar.gz") archivePath := filepath.Join(repository.DatastorePath(), gameID, "data.tar.gz")
return cli.Push(gameID, archivePath, m) return cli.Push(gameID, archivePath, m)
} }
func pull(gameID string, cli *client.Client) error { func pull(gameID string, cli *client.Client) error {
archivePath := filepath.Join(game.DatastorePath(), gameID, "data.tar.gz") archivePath := filepath.Join(repository.DatastorePath(), gameID, "data.tar.gz")
return cli.Pull(gameID, archivePath) return cli.Pull(gameID, archivePath)
} }

View File

@@ -3,7 +3,7 @@ package version
import ( import (
"cloudsave/pkg/constants" "cloudsave/pkg/constants"
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
"cloudsave/pkg/tools/prompt/credentials" "cloudsave/cmd/cli/tools/prompt/credentials"
"context" "context"
"flag" "flag"
"fmt" "fmt"

View File

@@ -2,7 +2,7 @@ package api
import ( import (
"cloudsave/cmd/server/data" "cloudsave/cmd/server/data"
"cloudsave/pkg/game" "cloudsave/pkg/repository"
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
@@ -61,6 +61,7 @@ func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServ
// Data routes // Data routes
gamesRouter.Group(func(saveRouter chi.Router) { gamesRouter.Group(func(saveRouter chi.Router) {
saveRouter.Post("/{id}/data", s.upload) saveRouter.Post("/{id}/data", s.upload)
saveRouter.Post("/{id}/hist/data", s.histUpload)
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}/metadata", s.metadata) saveRouter.Get("/{id}/metadata", s.metadata)
@@ -78,7 +79,7 @@ func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServ
func (s HTTPServer) all(w http.ResponseWriter, r *http.Request) { func (s HTTPServer) all(w http.ResponseWriter, r *http.Request) {
path := filepath.Join(s.documentRoot, "data") path := filepath.Join(s.documentRoot, "data")
datastore := make([]game.Metadata, 0) datastore := make([]repository.Metadata, 0)
if _, err := os.Stat(path); err != nil { if _, err := os.Stat(path); err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
@@ -104,7 +105,7 @@ func (s HTTPServer) all(w http.ResponseWriter, r *http.Request) {
continue continue
} }
var m game.Metadata var m repository.Metadata
err = json.Unmarshal(content, &m) err = json.Unmarshal(content, &m)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "corrupted datastore: failed to parse %s/metadata.json: %s", d.Name(), err) fmt.Fprintf(os.Stderr, "corrupted datastore: failed to parse %s/metadata.json: %s", d.Name(), err)
@@ -209,6 +210,49 @@ func (s HTTPServer) upload(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
} }
func (s HTTPServer) histUpload(w http.ResponseWriter, r *http.Request) {
const (
sizeLimit int64 = 500 << 20 // 500 MB
)
id := chi.URLParam(r, "id")
dt, err := formParseDate("date", r.MultipartForm.Value)
if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to load payload:", err)
badRequest("bad payload", w, r)
return
}
// Limit max upload size
r.Body = http.MaxBytesReader(w, r.Body, sizeLimit)
// Parse multipart form
err = r.ParseMultipartForm(sizeLimit)
if err != nil {
fmt.Fprintln(os.Stderr, "error: failed to load payload:", err)
badRequest("bad payload", w, r)
return
}
// Retrieve file
file, _, err := r.FormFile("payload")
if err != nil {
fmt.Fprintln(os.Stderr, "error: cannot find payload in the form:", err)
badRequest("payload not found", w, r)
return
}
defer file.Close()
if err := data.WriteHist(id, s.documentRoot, dt, file); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to write file to disk:", err)
internalServerError(w, r)
return
}
// Respond success
w.WriteHeader(http.StatusCreated)
}
func (s HTTPServer) hash(w http.ResponseWriter, r *http.Request) { func (s HTTPServer) hash(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))
@@ -272,7 +316,7 @@ func (s HTTPServer) metadata(w http.ResponseWriter, r *http.Request) {
} }
defer f.Close() defer f.Close()
var metadata game.Metadata var metadata repository.Metadata
d := json.NewDecoder(f) d := json.NewDecoder(f)
err = d.Decode(&metadata) err = d.Decode(&metadata)
if err != nil { if err != nil {
@@ -284,49 +328,67 @@ func (s HTTPServer) metadata(w http.ResponseWriter, r *http.Request) {
ok(metadata, w, r) ok(metadata, w, r)
} }
func parseFormMetadata(gameID string, values map[string][]string) (game.Metadata, error) { func parseFormMetadata(gameID string, values map[string][]string) (repository.Metadata, error) {
var name string var name string
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 repository.Metadata{}, fmt.Errorf("error: corrupted metadata")
} }
name = v[0] name = v[0]
} else { } else {
return game.Metadata{}, fmt.Errorf("error: cannot find metadata in the form") return repository.Metadata{}, fmt.Errorf("error: cannot find metadata in the form")
} }
var version int var version int
if v, ok := values["version"]; ok { if v, ok := values["version"]; ok {
if len(v) == 0 { if len(v) == 0 {
return game.Metadata{}, fmt.Errorf("error: corrupted metadata") return repository.Metadata{}, fmt.Errorf("error: corrupted metadata")
} }
if v, err := strconv.Atoi(v[0]); err == nil { if v, err := strconv.Atoi(v[0]); err == nil {
version = v version = v
} else { } else {
return game.Metadata{}, err return repository.Metadata{}, err
} }
} else { } else {
return game.Metadata{}, fmt.Errorf("error: cannot find metadata in the form") return repository.Metadata{}, fmt.Errorf("error: cannot find metadata in the form")
} }
var date time.Time var date time.Time
if v, ok := values["date"]; ok { if v, ok := values["date"]; ok {
if len(v) == 0 { if len(v) == 0 {
return game.Metadata{}, fmt.Errorf("error: corrupted metadata") return repository.Metadata{}, fmt.Errorf("error: corrupted metadata")
} }
if v, err := time.Parse(time.RFC3339, v[0]); err == nil { if v, err := time.Parse(time.RFC3339, v[0]); err == nil {
date = v date = v
} else { } else {
return game.Metadata{}, err return repository.Metadata{}, err
} }
} else { } else {
return game.Metadata{}, fmt.Errorf("error: cannot find metadata in the form") return repository.Metadata{}, fmt.Errorf("error: cannot find metadata in the form")
} }
return game.Metadata{ return repository.Metadata{
ID: gameID, ID: gameID,
Version: version, Version: version,
Name: name, Name: name,
Date: date, Date: date,
}, nil }, nil
} }
func formParseDate(key string, values map[string][]string) (time.Time, error) {
var date time.Time
if v, ok := values[key]; ok {
if len(v) == 0 {
return time.Time{}, fmt.Errorf("error: corrupted metadata")
}
if v, err := time.Parse(time.RFC3339, v[0]); err == nil {
date = v
} else {
return time.Time{}, err
}
} else {
return time.Time{}, fmt.Errorf("error: cannot find metadata in the form")
}
return date, nil
}

View File

@@ -1,12 +1,13 @@
package data package data
import ( import (
"cloudsave/pkg/game" "cloudsave/pkg/repository"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"time"
) )
func Write(gameID, documentRoot string, r io.Reader) error { func Write(gameID, documentRoot string, r io.Reader) error {
@@ -39,7 +40,37 @@ func Write(gameID, documentRoot string, r io.Reader) error {
return nil return nil
} }
func UpdateMetadata(gameID, documentRoot string, m game.Metadata) error { func WriteHist(gameID, documentRoot string, dt time.Time, r io.Reader) error {
dataFolderPath := filepath.Join(documentRoot, "data", gameID, "hist")
partPath := filepath.Join(dataFolderPath, dt.Format("2006-01-02T15-04-05Z07-00")+".data.tar.gz.part")
finalFilePath := filepath.Join(dataFolderPath, dt.Format("2006-01-02T15-04-05Z07-00")+".data.tar.gz")
if err := makeDataFolder(gameID, documentRoot); err != nil {
return err
}
f, err := os.OpenFile(partPath, os.O_CREATE|os.O_WRONLY, 0740)
if err != nil {
return err
}
if _, err := io.Copy(f, r); err != nil {
f.Close()
if err := os.Remove(partPath); err != nil {
return fmt.Errorf("failed to write the file and cannot clean the folder: %w", err)
}
return fmt.Errorf("failed to write the file: %w", err)
}
f.Close()
if err := os.Rename(partPath, finalFilePath); err != nil {
return err
}
return nil
}
func UpdateMetadata(gameID, documentRoot string, m repository.Metadata) error {
if err := makeDataFolder(gameID, documentRoot); err != nil { if err := makeDataFolder(gameID, documentRoot); err != nil {
return err return err
} }
@@ -56,5 +87,13 @@ func UpdateMetadata(gameID, documentRoot string, m game.Metadata) error {
} }
func makeDataFolder(gameID, documentRoot string) error { func makeDataFolder(gameID, documentRoot string) error {
return os.MkdirAll(filepath.Join(documentRoot, "data", gameID), 0740) if err := os.MkdirAll(filepath.Join(documentRoot, "data", gameID), 0740); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(documentRoot, "data", gameID, "hist"), 0740); err != nil {
return err
}
return nil
} }

View File

@@ -1,5 +1,5 @@
package constants package constants
const Version = "0.0.1" const Version = "0.0.2"
const ApiVersion = 1 const ApiVersion = 1

View File

@@ -2,8 +2,8 @@ package client
import ( import (
"bytes" "bytes"
"cloudsave/pkg/game"
"cloudsave/pkg/remote/obj" "cloudsave/pkg/remote/obj"
"cloudsave/pkg/repository"
customtime "cloudsave/pkg/tools/time" customtime "cloudsave/pkg/tools/time"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -117,19 +117,19 @@ 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) Metadata(gameID string) (game.Metadata, error) { func (c *Client) Metadata(gameID string) (repository.Metadata, error) {
u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "metadata") u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "metadata")
if err != nil { if err != nil {
return game.Metadata{}, err return repository.Metadata{}, err
} }
o, err := c.get(u) o, err := c.get(u)
if err != nil { if err != nil {
return game.Metadata{}, err return repository.Metadata{}, err
} }
if m, ok := (o.Data).(map[string]any); ok { if m, ok := (o.Data).(map[string]any); ok {
gm := game.Metadata{ gm := repository.Metadata{
ID: m["id"].(string), ID: m["id"].(string),
Name: m["name"].(string), Name: m["name"].(string),
Version: int(m["version"].(float64)), Version: int(m["version"].(float64)),
@@ -138,10 +138,10 @@ func (c *Client) Metadata(gameID string) (game.Metadata, error) {
return gm, nil return gm, nil
} }
return game.Metadata{}, errors.New("invalid payload sent by the server") return repository.Metadata{}, 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 repository.Metadata) error {
u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "data") u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "data")
if err != nil { if err != nil {
return err return err
@@ -271,7 +271,7 @@ func (c *Client) Ping() error {
return nil return nil
} }
func (c *Client) All() ([]game.Metadata, error) { func (c *Client) All() ([]repository.Metadata, error) {
u, err := url.JoinPath(c.baseURL, "api", "v1", "games") u, err := url.JoinPath(c.baseURL, "api", "v1", "games")
if err != nil { if err != nil {
return nil, err return nil, err
@@ -283,10 +283,10 @@ func (c *Client) All() ([]game.Metadata, error) {
} }
if games, ok := (o.Data).([]any); ok { if games, ok := (o.Data).([]any); ok {
var res []game.Metadata var res []repository.Metadata
for _, g := range games { for _, g := range games {
if v, ok := g.(map[string]any); ok { if v, ok := g.(map[string]any); ok {
gm := game.Metadata{ gm := repository.Metadata{
ID: v["id"].(string), ID: v["id"].(string),
Name: v["name"].(string), Name: v["name"].(string),
Version: int(v["version"].(float64)), Version: int(v["version"].(float64)),

View File

@@ -1,4 +1,4 @@
package game package repository
import ( import (
"cloudsave/pkg/tools/id" "cloudsave/pkg/tools/id"
@@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@@ -135,6 +136,65 @@ func One(gameID string) (Metadata, error) {
return m, nil return m, nil
} }
func Archive(gameID string) error {
path := filepath.Join(datastorepath, gameID, "data.tar.gz")
// open old
f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
return fmt.Errorf("failed to open old file: %w", err)
}
defer f.Close()
histDirPath := filepath.Join(datastorepath, gameID, "hist")
if err := os.MkdirAll(histDirPath, 0740); err != nil {
return fmt.Errorf("failed to make 'hist' directory")
}
d, err := os.ReadDir(histDirPath)
if err != nil {
return fmt.Errorf("failed to open 'hist' directory")
}
// keep the dir under 6 files
if len(d) > 5 {
var oldest *fs.FileInfo
for _, hfile := range d {
finfo, err := hfile.Info()
if err != nil {
return fmt.Errorf("failed to read backup file: %w", err)
}
if oldest == nil {
oldest = &finfo
continue
}
if finfo.ModTime().Before((*oldest).ModTime()) {
oldest = &finfo
}
}
if err := os.Remove((*oldest).Name()); err != nil {
return fmt.Errorf("failed to remove the oldest backup file: %w", err)
}
}
// open new
nf, err := os.OpenFile(filepath.Join(datastorepath, gameID, "hist", time.Now().Format("2006-01-02T15-04-05Z07-00")+".data.tar.gz"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return fmt.Errorf("failed to open new file: %w", err)
}
defer nf.Close()
// copy
if _, err := io.Copy(nf, f); err != nil {
return fmt.Errorf("failed to copy data: %w", err)
}
return nil
}
func DatastorePath() string { func DatastorePath() string {
return datastorepath return datastorepath
} }

View File

@@ -3,6 +3,7 @@ package archive
import ( import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -71,3 +72,49 @@ func Untar(file io.Reader, path string) error {
} }
} }
} }
func Tar(file io.Writer, path string) error {
gw := gzip.NewWriter(file)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
// Walk again to add files
err := filepath.Walk(path, func(path string, info os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
// Create tar header
header, err := tar.FileInfoHeader(info, path)
if err != nil {
return err
}
// Preserve directory structure relative to srcDir
relPath, err := filepath.Rel(filepath.Dir(path), path)
if err != nil {
return err
}
header.Name = relPath
if err := tw.WriteHeader(header); err != nil {
return err
}
if info.Mode().IsRegular() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(tw, file); err != nil {
return err
}
}
return nil
})
if err != nil {
return fmt.Errorf("writing tar entries: %w", err)
}
return nil
}