12 Commits

Author SHA1 Message Date
573fba708e fix can't create entry 2025-08-17 17:45:50 +02:00
a7c85ea3c6 fix name 2025-08-17 01:12:31 +02:00
ea6948dbe2 Actualiser README.md 2025-08-17 00:50:56 +02:00
da2ad068b4 update version number 2025-08-17 00:43:58 +02:00
bbd0983e63 Merge pull request '0.0.4' (#4) from 0.0.4 into main
Reviewed-on: #4
2025-08-17 00:43:00 +02:00
54dcc7d006 show cmd 2025-08-17 00:42:12 +02:00
851ff89886 implement applu 2025-08-17 00:30:19 +02:00
aa29fae900 fix windows sync 2025-08-10 12:34:08 +02:00
822c93bf4c opti 2025-08-10 02:32:56 +02:00
ab857bd0dd ver num 2025-08-10 02:09:34 +02:00
e34dc704ca wip refactoring 2025-08-10 02:03:27 +02:00
810c5ac627 wip 2025-08-09 22:19:57 +02:00
20 changed files with 1071 additions and 834 deletions

View File

@@ -21,6 +21,8 @@ e.g.:
test:$2y$10$uULsuyROe3LVdTzFoBH7HO0zhvyKp6CX2FDNl7quXMFYqzitU0kc. test:$2y$10$uULsuyROe3LVdTzFoBH7HO0zhvyKp6CX2FDNl7quXMFYqzitU0kc.
``` ```
To generate bcrypt password, I recommand [hash_utils](https://git.thelilfrog.com/thelilfrog/hash_utils), which is offline and secure
The default path to this directory is `/var/lib/cloudsave`, this can be changed with the `-document-root` argument The default path to this directory is `/var/lib/cloudsave`, this can be changed with the `-document-root` argument
### Client ### Client
@@ -45,7 +47,7 @@ Run this command to start the scan, if needed, the tool will create a new archiv
```bash ```bash
cloudsave scan cloudsave scan
``` ```
#### Send everythings on the server #### Send everything on the server
This will pull and push data to the server. This will pull and push data to the server.

View File

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

View File

@@ -1,22 +1,21 @@
package add package add
import ( import (
"cloudsave/pkg/remote" "cloudsave/pkg/data"
"cloudsave/pkg/repository"
"context" "context"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/google/subcommands" "github.com/google/subcommands"
) )
type ( type (
AddCmd struct { AddCmd struct {
name string Service *data.Service
remote string name string
remote string
} }
) )
@@ -51,17 +50,16 @@ func (p *AddCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
p.name = filepath.Base(path) p.name = filepath.Base(path)
} }
m, err := repository.Add(p.name, path) gameID, err := p.Service.Add(p.name, path, p.remote)
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 this gamesave to the datastore:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
if len(strings.TrimSpace(p.remote)) > 0 { if err := p.Service.Scan(gameID); err != nil {
remote.Set(m.ID, p.remote) fmt.Fprintln(os.Stderr, "error: failed to scan:", err)
return subcommands.ExitFailure
} }
fmt.Println(m.ID)
return subcommands.ExitSuccess return subcommands.ExitSuccess
} }

View File

@@ -1,36 +1,35 @@
package apply package apply
import ( import (
"cloudsave/pkg/repository" "cloudsave/pkg/data"
"cloudsave/pkg/tools/archive"
"context" "context"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"path/filepath"
"github.com/google/subcommands" "github.com/google/subcommands"
) )
type ( type (
ListCmd struct { ApplyCmd struct {
Service *data.Service
} }
) )
func (*ListCmd) Name() string { return "apply" } func (*ApplyCmd) Name() string { return "apply" }
func (*ListCmd) Synopsis() string { return "apply a backup" } func (*ApplyCmd) Synopsis() string { return "apply a backup" }
func (*ListCmd) Usage() string { func (*ApplyCmd) Usage() string {
return `Usage: cloudsave apply <GAME_ID> <BACKUP_ID> return `Usage: cloudsave apply <GAME_ID> [BACKUP_ID]
Apply a backup Apply a backup
` `
} }
func (p *ListCmd) SetFlags(f *flag.FlagSet) { func (p *ApplyCmd) SetFlags(f *flag.FlagSet) {
} }
func (p *ListCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { func (p *ApplyCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if f.NArg() != 2 { if f.NArg() < 1 {
fmt.Fprintln(os.Stderr, "error: missing game ID and/or backup uuid") fmt.Fprintln(os.Stderr, "error: missing game ID and/or backup uuid")
return subcommands.ExitUsageError return subcommands.ExitUsageError
} }
@@ -38,31 +37,16 @@ func (p *ListCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
gameID := f.Arg(0) gameID := f.Arg(0)
uuid := f.Arg(1) uuid := f.Arg(1)
g, err := repository.One(gameID) if len(uuid) == 0 {
if err != nil { if err := p.Service.ApplyCurrent(gameID); err != nil {
fmt.Fprintf(os.Stderr, "error: failed to open game metadata: %s\n", err) fmt.Fprintf(os.Stderr, "error: failed to apply: %s", err)
return subcommands.ExitFailure return subcommands.ExitFailure
}
return subcommands.ExitSuccess
} }
if err := repository.RestoreArchive(gameID, uuid); err != nil { if err := p.Service.ApplyBackup(gameID, uuid); err != nil {
fmt.Fprintf(os.Stderr, "error: failed to restore backup: %s\n", err) fmt.Fprintf(os.Stderr, "error: failed to apply: %s", err)
return subcommands.ExitFailure
}
if err := os.RemoveAll(g.Path); err != nil {
fmt.Fprintf(os.Stderr, "error: failed to remove old data: %s\n", err)
return subcommands.ExitFailure
}
file, err := os.OpenFile(filepath.Join(repository.DatastorePath(), gameID, "data.tar.gz"), os.O_RDONLY, 0)
if err != nil {
fmt.Fprintf(os.Stderr, "error: failed to open archive: %s\n", err)
return subcommands.ExitFailure
}
defer file.Close()
if err := archive.Untar(file, g.Path); err != nil {
fmt.Fprintf(os.Stderr, "error: failed to extract archive: %s\n", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }

View File

@@ -2,8 +2,8 @@ package list
import ( import (
"cloudsave/cmd/cli/tools/prompt/credentials" "cloudsave/cmd/cli/tools/prompt/credentials"
"cloudsave/pkg/data"
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
"cloudsave/pkg/repository"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -14,8 +14,9 @@ import (
type ( type (
ListCmd struct { ListCmd struct {
remote bool Service *data.Service
backup bool remote bool
backup bool
} }
) )
@@ -44,25 +45,25 @@ func (p *ListCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
username, password, err := credentials.Read() username, password, err := credentials.Read()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to read std output: %s", err) fmt.Fprintf(os.Stderr, "error: failed to read std output: %s", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
if err := remote(f.Arg(0), username, password, p.backup); err != nil { if err := p.server(f.Arg(0), username, password, p.backup); err != nil {
fmt.Fprintln(os.Stderr, "error:", err) fmt.Fprintln(os.Stderr, "error:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
return subcommands.ExitSuccess return subcommands.ExitSuccess
} }
if err := local(p.backup); err != nil { if err := p.local(p.backup); err != nil {
fmt.Fprintln(os.Stderr, "error:", err) fmt.Fprintln(os.Stderr, "error:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
return subcommands.ExitSuccess return subcommands.ExitSuccess
} }
func local(includeBackup bool) error { func (p *ListCmd) local(includeBackup bool) error {
games, err := repository.All() games, err := p.Service.AllGames()
if err != nil { if err != nil {
return fmt.Errorf("failed to load datastore: %w", err) return fmt.Errorf("failed to load datastore: %w", err)
} }
@@ -70,9 +71,11 @@ func local(includeBackup bool) error {
for _, g := range games { for _, g := range games {
fmt.Println("ID:", g.ID) fmt.Println("ID:", g.ID)
fmt.Println("Name:", g.Name) fmt.Println("Name:", g.Name)
fmt.Println("Last Version:", g.Date, "( Version Number", g.Version, ")") fmt.Println("Last Version:", g.Date)
fmt.Println("Version:", g.Version)
fmt.Println("MD5:", g.MD5)
if includeBackup { if includeBackup {
bk, err := repository.Archives(g.ID) bk, err := p.Service.AllBackups(g.ID)
if err != nil { if err != nil {
return fmt.Errorf("failed to list backup files: %w", err) return fmt.Errorf("failed to list backup files: %w", err)
} }
@@ -89,7 +92,7 @@ func local(includeBackup bool) error {
return nil return nil
} }
func remote(url, username, password string, includeBackup bool) error { func (p *ListCmd) server(url, username, password string, includeBackup bool) error {
cli := client.New(url, username, password) cli := client.New(url, username, password)
if err := cli.Ping(); err != nil { if err := cli.Ping(); err != nil {
@@ -107,7 +110,9 @@ func remote(url, username, password string, includeBackup bool) error {
for _, g := range games { for _, g := range games {
fmt.Println("ID:", g.ID) fmt.Println("ID:", g.ID)
fmt.Println("Name:", g.Name) fmt.Println("Name:", g.Name)
fmt.Println("Last Version:", g.Date, "( Version Number", g.Version, ")") fmt.Println("Last Version:", g.Date)
fmt.Println("Version:", g.Version)
fmt.Println("MD5:", g.MD5)
if includeBackup { if includeBackup {
bk, err := cli.ListArchives(g.ID) bk, err := cli.ListArchives(g.ID)
if err != nil { if err != nil {

View File

@@ -2,20 +2,19 @@ package pull
import ( import (
"cloudsave/cmd/cli/tools/prompt/credentials" "cloudsave/cmd/cli/tools/prompt/credentials"
"cloudsave/pkg/data"
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
"cloudsave/pkg/repository"
"cloudsave/pkg/tools/archive"
"context" "context"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"path/filepath"
"github.com/google/subcommands" "github.com/google/subcommands"
) )
type ( type (
PullCmd struct { PullCmd struct {
Service *data.Service
} }
) )
@@ -44,45 +43,33 @@ func (p *PullCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
username, password, err := credentials.Read() username, password, err := credentials.Read()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to read std output: %s", err) fmt.Fprintf(os.Stderr, "error: failed to read std output: %s", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
cli := client.New(url, username, password) cli := client.New(url, username, password)
if err := cli.Ping(); err != nil { if err := cli.Ping(); err != nil {
fmt.Fprintf(os.Stderr, "failed to connect to the remote: %s", err) fmt.Fprintf(os.Stderr, "error: failed to connect to the remote: %s", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
archivePath := filepath.Join(repository.DatastorePath(), gameID, "data.tar.gz") if err := p.Service.PullCurrent(gameID, path, cli); err != nil {
fmt.Fprintf(os.Stderr, "error: failed to pull current archive: %s", err)
return subcommands.ExitFailure
}
m, err := cli.Metadata(gameID) ids, err := cli.ListArchives(gameID)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to get metadata: %s", err) fmt.Fprintf(os.Stderr, "error: failed to list backup archive: %s", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
err = repository.Register(m, path) for _, id := range ids {
if err != nil { if err := p.Service.PullBackup(gameID, id, cli); err != nil {
fmt.Fprintf(os.Stderr, "failed to register local metadata: %s", err) fmt.Fprintf(os.Stderr, "error: failed to pull backup archive %s: %s", id, err)
return subcommands.ExitFailure 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 return subcommands.ExitSuccess

View File

@@ -1,9 +1,9 @@
package remote package remote
import ( import (
"cloudsave/pkg/data"
"cloudsave/pkg/remote" "cloudsave/pkg/remote"
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
"cloudsave/pkg/repository"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -14,8 +14,9 @@ import (
type ( type (
RemoteCmd struct { RemoteCmd struct {
set bool Service *data.Service
list bool set bool
list bool
} }
) )
@@ -43,7 +44,7 @@ func (p *RemoteCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface
switch { switch {
case p.list: case p.list:
{ {
if err := list(); err != nil { if err := p.print(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err) fmt.Fprintln(os.Stderr, "error:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
@@ -68,8 +69,8 @@ func (p *RemoteCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface
return subcommands.ExitSuccess return subcommands.ExitSuccess
} }
func list() error { func (p *RemoteCmd) print() error {
games, err := repository.All() games, err := p.Service.AllGames()
if err != nil { if err != nil {
return fmt.Errorf("failed to load datastore: %w", err) return fmt.Errorf("failed to load datastore: %w", err)
} }
@@ -77,7 +78,7 @@ func list() error {
for _, g := range games { for _, g := range games {
r, err := remote.One(g.ID) r, err := remote.One(g.ID)
if err != nil { if err != nil {
return fmt.Errorf("failed to load datastore: %w", err) continue
} }
cli := client.New(r.URL, "", "") cli := client.New(r.URL, "", "")

View File

@@ -1,7 +1,7 @@
package remove package remove
import ( import (
"cloudsave/pkg/repository" "cloudsave/pkg/data"
"context" "context"
"flag" "flag"
"fmt" "fmt"
@@ -11,7 +11,9 @@ import (
) )
type ( type (
RemoveCmd struct{} RemoveCmd struct {
Service *data.Service
}
) )
func (*RemoveCmd) Name() string { return "remove" } func (*RemoveCmd) Name() string { return "remove" }
@@ -33,7 +35,7 @@ func (p *RemoveCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}
return subcommands.ExitUsageError return subcommands.ExitUsageError
} }
err := repository.Remove(f.Arg(0)) err := p.Service.RemoveGame(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,22 +1,18 @@
package run package run
import ( import (
"cloudsave/pkg/repository" "cloudsave/pkg/data"
"cloudsave/pkg/tools/archive"
"context" "context"
"flag" "flag"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath"
"time"
"github.com/google/subcommands" "github.com/google/subcommands"
"github.com/schollz/progressbar/v3"
) )
type ( type (
RunCmd struct { RunCmd struct {
Service *data.Service
} }
) )
@@ -34,26 +30,19 @@ and a new archive is created with a new version number.
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 := repository.All() datastore, err := p.Service.AllGames()
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
} }
for _, metadata := range datastore { for _, metadata := range datastore {
metadataPath := filepath.Join(repository.DatastorePath(), metadata.ID) if err := p.Service.MakeBackup(metadata.ID); err != nil {
//todo transaction fmt.Fprintln(os.Stderr, "error: failed to make backup:", err)
err := archiveIfChanged(metadata.ID, metadata.Path, filepath.Join(metadataPath, "data.tar.gz"), filepath.Join(metadataPath, ".last_run"))
if err != nil {
fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
if err := repository.SetVersion(metadata.ID, metadata.Version+1); err != nil { if err := p.Service.Scan(metadata.ID); err != nil {
fmt.Fprintf(os.Stderr, "error: cannot process the data of %s: %s\n", metadata.ID, err) fmt.Fprintln(os.Stderr, "error: failed to scan:", err)
return subcommands.ExitFailure
}
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)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
fmt.Println("✅", metadata.Name) fmt.Println("✅", metadata.Name)
@@ -62,78 +51,3 @@ func (p *RunCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
fmt.Println("done.") fmt.Println("done.")
return subcommands.ExitSuccess return subcommands.ExitSuccess
} }
// archiveIfChanged will archive srcDir into destTarGz only if any file
// in srcDir has a modification time > the last run time stored in stateFile.
// After archiving, it updates stateFile to the current time.
func archiveIfChanged(gameID, srcDir, destTarGz, stateFile string) error {
pg := progressbar.New(-1)
destroyPg := func() {
pg.Finish()
pg.Clear()
pg.Close()
}
defer destroyPg()
pg.Describe("Scanning " + gameID + "...")
// load last run time
var lastRun time.Time
data, err := os.ReadFile(stateFile)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to reading state file: %w", err)
}
if err == nil {
lastRun, err = time.Parse(time.RFC3339, string(data))
if err != nil {
return fmt.Errorf("parsing state file timestamp: %w", err)
}
}
// check for changes
changed := false
err = filepath.Walk(srcDir, func(path string, info os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
if info.ModTime().After(lastRun) {
changed = true
return io.EOF // early exit
}
return nil
})
if err != nil && err != io.EOF {
return fmt.Errorf("failed to scanning source directory: %w", err)
}
if !changed {
pg.Finish()
return nil
}
// make a backup
pg.Describe("Backup current data...")
if err := repository.MakeArchive(gameID); err != nil {
return fmt.Errorf("failed to archive data: %w", err)
}
// create archive
pg.Describe("Archiving new data...")
f, err := os.Create(destTarGz)
if err != nil {
return fmt.Errorf("failed to creating archive file: %w", err)
}
defer f.Close()
if err := archive.Tar(f, srcDir); err != nil {
return fmt.Errorf("failed archiving files: %w", err)
}
now := time.Now().UTC().Format(time.RFC3339)
if err := os.WriteFile(stateFile, []byte(now), 0644); err != nil {
return fmt.Errorf("updating state file: %w", err)
}
return nil
}

View File

@@ -0,0 +1,51 @@
package show
import (
"cloudsave/pkg/data"
"context"
"flag"
"fmt"
"os"
"github.com/google/subcommands"
)
type (
ShowCmd struct {
Service *data.Service
}
)
func (*ShowCmd) Name() string { return "show" }
func (*ShowCmd) Synopsis() string { return "show metadata about game" }
func (*ShowCmd) Usage() string {
return `Usage: cloudsave show <GAME_ID>
Show metdata about a game
`
}
func (p *ShowCmd) SetFlags(f *flag.FlagSet) {
}
func (p *ShowCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if f.NArg() != 1 {
fmt.Fprintln(os.Stderr, "error: missing game ID")
return subcommands.ExitUsageError
}
gameID := f.Arg(0)
g, err := p.Service.One(gameID)
if err != nil {
fmt.Fprintf(os.Stderr, "error: failed to apply: %s", err)
return subcommands.ExitFailure
}
fmt.Println(g.Name)
fmt.Println("------")
fmt.Println("Version: ", g.Version)
fmt.Println("Path: ", g.Path)
fmt.Println("MD5: ", g.MD5)
return subcommands.ExitSuccess
}

View File

@@ -3,6 +3,7 @@ package sync
import ( import (
"cloudsave/cmd/cli/tools/prompt" "cloudsave/cmd/cli/tools/prompt"
"cloudsave/cmd/cli/tools/prompt/credentials" "cloudsave/cmd/cli/tools/prompt/credentials"
"cloudsave/pkg/data"
"cloudsave/pkg/remote" "cloudsave/pkg/remote"
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
"cloudsave/pkg/repository" "cloudsave/pkg/repository"
@@ -12,7 +13,6 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"os" "os"
"path/filepath"
"time" "time"
"github.com/google/subcommands" "github.com/google/subcommands"
@@ -21,6 +21,7 @@ import (
type ( type (
SyncCmd struct { SyncCmd struct {
Service *data.Service
} }
) )
@@ -37,7 +38,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 := repository.All() games, err := p.Service.AllGames()
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
@@ -77,13 +78,13 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
if !exists { if !exists {
pg.Describe(fmt.Sprintf("[%s] Pushing data...", g.Name)) pg.Describe(fmt.Sprintf("[%s] Pushing data...", g.Name))
if err := push(g, cli); err != nil { if err := p.push(g, cli); err != nil {
destroyPg() destroyPg()
fmt.Fprintln(os.Stderr, "failed to push:", err) fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
pg.Describe(fmt.Sprintf("[%s] Pushing backup...", g.Name)) pg.Describe(fmt.Sprintf("[%s] Pushing backup...", g.Name))
if err := pushBackup(g, cli); err != nil { if err := p.pushBackup(g, cli); err != nil {
destroyPg() destroyPg()
slog.Warn("failed to push backup files", "err", err) slog.Warn("failed to push backup files", "err", err)
} }
@@ -92,12 +93,6 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
} }
pg.Describe(fmt.Sprintf("[%s] Fetching metadata...", g.Name)) pg.Describe(fmt.Sprintf("[%s] Fetching metadata...", g.Name))
hlocal, err := repository.Hash(r.GameID)
if err != nil {
destroyPg()
slog.Error(err.Error())
continue
}
hremote, err := cli.Hash(r.GameID) hremote, err := cli.Hash(r.GameID)
if err != nil { if err != nil {
@@ -106,13 +101,6 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
vlocal, err := repository.Version(r.GameID)
if err != nil {
destroyPg()
slog.Error(err.Error())
continue
}
remoteMetadata, err := cli.Metadata(r.GameID) remoteMetadata, err := cli.Metadata(r.GameID)
if err != nil { if err != nil {
destroyPg() destroyPg()
@@ -121,20 +109,20 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
} }
pg.Describe(fmt.Sprintf("[%s] Pulling backup...", g.Name)) pg.Describe(fmt.Sprintf("[%s] Pulling backup...", g.Name))
if err := pullBackup(g, cli); err != nil { if err := p.pullBackup(g, cli); err != nil {
slog.Warn("failed to pull backup files", "err", err) slog.Warn("failed to pull backup files", "err", err)
} }
pg.Describe(fmt.Sprintf("[%s] Pushing backup...", g.Name)) pg.Describe(fmt.Sprintf("[%s] Pushing backup...", g.Name))
if err := pushBackup(g, cli); err != nil { if err := p.pushBackup(g, cli); err != nil {
slog.Warn("failed to push backup files", "err", err) slog.Warn("failed to push backup files", "err", err)
} }
if hlocal == hremote { if g.MD5 == hremote {
destroyPg() destroyPg()
if vlocal != remoteMetadata.Version { if g.Version != 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 := repository.SetVersion(r.GameID, remoteMetadata.Version); err != nil { if err := p.Service.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
} }
@@ -143,9 +131,9 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
if vlocal > remoteMetadata.Version { if g.Version > remoteMetadata.Version {
pg.Describe(fmt.Sprintf("[%s] Pushing data...", g.Name)) pg.Describe(fmt.Sprintf("[%s] Pushing data...", g.Name))
if err := push(g, cli); err != nil { if err := p.push(g, cli); err != nil {
destroyPg() destroyPg()
fmt.Fprintln(os.Stderr, "failed to push:", err) fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
@@ -155,22 +143,21 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
if vlocal < remoteMetadata.Version { if g.Version < remoteMetadata.Version {
destroyPg() destroyPg()
if err := pull(r.GameID, cli); err != nil { if err := p.pull(r.GameID, cli); err != nil {
destroyPg() destroyPg()
fmt.Fprintln(os.Stderr, "failed to push:", err) fmt.Fprintln(os.Stderr, "failed to push:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }
if err := repository.SetVersion(r.GameID, remoteMetadata.Version); err != nil {
g.Version = remoteMetadata.Version
g.Date = remoteMetadata.Date
if err := p.Service.UpdateMetadata(g.ID, g); err != nil {
destroyPg() destroyPg()
fmt.Fprintln(os.Stderr, "error: failed to synchronize version number:", err) fmt.Fprintln(os.Stderr, "failed to push:", err)
continue return subcommands.ExitFailure
}
if err := repository.SetDate(r.GameID, remoteMetadata.Date); err != nil {
destroyPg()
fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err)
continue
} }
fmt.Println(g.Name + ": pulled") fmt.Println(g.Name + ": pulled")
continue continue
@@ -178,8 +165,8 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
destroyPg() destroyPg()
if vlocal == remoteMetadata.Version { if g.Version == remoteMetadata.Version {
if err := conflict(r.GameID, g, remoteMetadata, cli); err != nil { if err := p.conflict(r.GameID, g, remoteMetadata, cli); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to resolve conflict:", err) fmt.Fprintln(os.Stderr, "error: failed to resolve conflict:", err)
continue continue
} }
@@ -191,8 +178,8 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
return subcommands.ExitSuccess return subcommands.ExitSuccess
} }
func conflict(gameID string, m, remoteMetadata repository.Metadata, cli *client.Client) error { func (p *SyncCmd) conflict(gameID string, m, remoteMetadata repository.Metadata, cli *client.Client) error {
g, err := repository.One(gameID) g, err := p.Service.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)
@@ -211,35 +198,33 @@ func conflict(gameID string, m, remoteMetadata repository.Metadata, cli *client.
switch res { switch res {
case prompt.My: case prompt.My:
{ {
if err := push(m, cli); err != nil { if err := p.push(m, cli); err != nil {
return fmt.Errorf("failed to push: %w", err) return fmt.Errorf("failed to push: %w", err)
} }
} }
case prompt.Their: case prompt.Their:
{ {
if err := pull(gameID, cli); err != nil { if err := p.pull(gameID, cli); err != nil {
return fmt.Errorf("failed to push: %w", err) return fmt.Errorf("failed to push: %w", err)
} }
if err := repository.SetVersion(gameID, remoteMetadata.Version); err != nil { g.Version = remoteMetadata.Version
return fmt.Errorf("failed to synchronize version number: %w", err) g.Date = remoteMetadata.Date
}
if err := repository.SetDate(gameID, remoteMetadata.Date); err != nil { if err := p.Service.UpdateMetadata(g.ID, g); err != nil {
return fmt.Errorf("failed to synchronize date: %w", err) return fmt.Errorf("failed to push: %w", err)
} }
} }
} }
return nil return nil
} }
func push(m repository.Metadata, cli *client.Client) error { func (p *SyncCmd) push(m repository.Metadata, cli *client.Client) error {
archivePath := filepath.Join(repository.DatastorePath(), m.ID, "data.tar.gz") return p.Service.PushArchive(m.ID, "", cli)
return cli.PushSave(archivePath, m)
} }
func pushBackup(m repository.Metadata, cli *client.Client) error { func (p *SyncCmd) pushBackup(m repository.Metadata, cli *client.Client) error {
bs, err := repository.Archives(m.ID) bs, err := p.Service.AllBackups(m.ID)
if err != nil { if err != nil {
return err return err
} }
@@ -262,7 +247,7 @@ func pushBackup(m repository.Metadata, cli *client.Client) error {
return nil return nil
} }
func pullBackup(m repository.Metadata, cli *client.Client) error { func (p *SyncCmd) pullBackup(m repository.Metadata, cli *client.Client) error {
bs, err := cli.ListArchives(m.ID) bs, err := cli.ListArchives(m.ID)
if err != nil { if err != nil {
return err return err
@@ -274,20 +259,13 @@ func pullBackup(m repository.Metadata, cli *client.Client) error {
return err return err
} }
linfo, err := repository.Archive(m.ID, uuid) linfo, err := p.Service.Backup(m.ID, uuid)
if err != nil { if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
}
path := filepath.Join(repository.DatastorePath(), m.ID, "hist", uuid)
if err := os.MkdirAll(path, 0740); err != nil {
return err return err
} }
if rinfo.MD5 != linfo.MD5 { if linfo.MD5 != rinfo.MD5 {
if err := cli.PullBackup(m.ID, uuid, filepath.Join(path, "data.tar.gz")); err != nil { if err := p.Service.PullBackup(m.ID, uuid, cli); err != nil {
return err return err
} }
} }
@@ -295,10 +273,8 @@ func pullBackup(m repository.Metadata, cli *client.Client) error {
return nil return nil
} }
func pull(gameID string, cli *client.Client) error { func (p *SyncCmd) pull(gameID string, cli *client.Client) error {
archivePath := filepath.Join(repository.DatastorePath(), gameID, "data.tar.gz") return p.Service.PullArchive(gameID, "", cli)
return cli.Pull(gameID, archivePath)
} }
func connect(remoteCred map[string]map[string]string, r remote.Remote) (*client.Client, error) { func connect(remoteCred map[string]map[string]string, r remote.Remote) (*client.Client, error) {

View File

@@ -2,34 +2,60 @@ package main
import ( import (
"cloudsave/cmd/cli/commands/add" "cloudsave/cmd/cli/commands/add"
"cloudsave/cmd/cli/commands/apply"
"cloudsave/cmd/cli/commands/list" "cloudsave/cmd/cli/commands/list"
"cloudsave/cmd/cli/commands/pull" "cloudsave/cmd/cli/commands/pull"
"cloudsave/cmd/cli/commands/remote" "cloudsave/cmd/cli/commands/remote"
"cloudsave/cmd/cli/commands/remove" "cloudsave/cmd/cli/commands/remove"
"cloudsave/cmd/cli/commands/run" "cloudsave/cmd/cli/commands/run"
"cloudsave/cmd/cli/commands/show"
"cloudsave/cmd/cli/commands/sync" "cloudsave/cmd/cli/commands/sync"
"cloudsave/cmd/cli/commands/version" "cloudsave/cmd/cli/commands/version"
"cloudsave/pkg/data"
"cloudsave/pkg/repository"
"context" "context"
"flag" "flag"
"os" "os"
"path/filepath"
"github.com/google/subcommands" "github.com/google/subcommands"
) )
func main() { func main() {
roaming, err := os.UserConfigDir()
if err != nil {
panic("failed to get user config path: " + err.Error())
}
datastorepath := filepath.Join(roaming, "cloudsave", "data")
err = os.MkdirAll(datastorepath, 0740)
if err != nil {
panic("cannot make the datastore:" + err.Error())
}
repo, err := repository.NewLazyRepository(datastorepath)
if err != nil {
panic("cannot make the datastore:" + err.Error())
}
s := data.NewService(repo)
subcommands.Register(subcommands.HelpCommand(), "help") subcommands.Register(subcommands.HelpCommand(), "help")
subcommands.Register(subcommands.FlagsCommand(), "help") subcommands.Register(subcommands.FlagsCommand(), "help")
subcommands.Register(subcommands.CommandsCommand(), "help") subcommands.Register(subcommands.CommandsCommand(), "help")
subcommands.Register(&version.VersionCmd{}, "help") subcommands.Register(&version.VersionCmd{}, "help")
subcommands.Register(&add.AddCmd{}, "management") subcommands.Register(&add.AddCmd{Service: s}, "management")
subcommands.Register(&run.RunCmd{}, "management") subcommands.Register(&run.RunCmd{Service: s}, "management")
subcommands.Register(&list.ListCmd{}, "management") subcommands.Register(&list.ListCmd{Service: s}, "management")
subcommands.Register(&remove.RemoveCmd{}, "management") subcommands.Register(&remove.RemoveCmd{Service: s}, "management")
subcommands.Register(&show.ShowCmd{Service: s}, "management")
subcommands.Register(&remote.RemoteCmd{}, "remote") subcommands.Register(&apply.ApplyCmd{Service: s}, "restore")
subcommands.Register(&sync.SyncCmd{}, "remote")
subcommands.Register(&pull.PullCmd{}, "remote") subcommands.Register(&remote.RemoteCmd{Service: s}, "remote")
subcommands.Register(&sync.SyncCmd{Service: s}, "remote")
subcommands.Register(&pull.PullCmd{Service: s}, "remote")
flag.Parse() flag.Parse()
ctx := context.Background() ctx := context.Background()

View File

@@ -1,7 +1,7 @@
package api package api
import ( import (
"cloudsave/cmd/server/data" "cloudsave/pkg/data"
"cloudsave/pkg/repository" "cloudsave/pkg/repository"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -20,16 +20,18 @@ import (
type ( type (
HTTPServer struct { HTTPServer struct {
Server *http.Server Server *http.Server
Service *data.Service
documentRoot string documentRoot string
} }
) )
// NewServer start the http server // NewServer start the http server
func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServer { func NewServer(documentRoot string, srv *data.Service, creds map[string]string, port int) *HTTPServer {
if !filepath.IsAbs(documentRoot) { if !filepath.IsAbs(documentRoot) {
panic("the document root is not an absolute path") panic("the document root is not an absolute path")
} }
s := &HTTPServer{ s := &HTTPServer{
Service: srv,
documentRoot: documentRoot, documentRoot: documentRoot,
} }
router := chi.NewRouter() router := chi.NewRouter()
@@ -195,14 +197,14 @@ func (s HTTPServer) upload(w http.ResponseWriter, r *http.Request) {
defer file.Close() defer file.Close()
//TODO make a transaction //TODO make a transaction
if err := data.UpdateMetadata(id, s.documentRoot, m); err != nil { if err := s.Service.UpdateMetadata(id, m); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to write metadata to disk:", err) fmt.Fprintln(os.Stderr, "error: failed to write metadata to disk:", err)
internalServerError(w, r) internalServerError(w, r)
return return
} }
if err := data.Write(id, s.documentRoot, file); err != nil { if err := s.Service.Copy(id, file); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to write file to disk:", err) fmt.Fprintln(os.Stderr, "error: failed to write data to disk:", err)
internalServerError(w, r) internalServerError(w, r)
return return
} }
@@ -213,20 +215,9 @@ func (s HTTPServer) upload(w http.ResponseWriter, r *http.Request) {
func (s HTTPServer) allHist(w http.ResponseWriter, r *http.Request) { func (s HTTPServer) allHist(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
path := filepath.Join(s.documentRoot, "data", gameID, "hist")
datastore := make([]string, 0) datastore := make([]string, 0)
if _, err := os.Stat(path); err != nil { ds, err := s.Service.AllBackups(gameID)
if errors.Is(err, os.ErrNotExist) {
ok(datastore, w, r)
return
}
fmt.Fprintln(os.Stderr, "failed to open datastore (", s.documentRoot, "):", err)
internalServerError(w, r)
return
}
ds, err := os.ReadDir(path)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "failed to open datastore (", s.documentRoot, "):", err) fmt.Fprintln(os.Stderr, "failed to open datastore (", s.documentRoot, "):", err)
internalServerError(w, r) internalServerError(w, r)
@@ -234,7 +225,7 @@ func (s HTTPServer) allHist(w http.ResponseWriter, r *http.Request) {
} }
for _, d := range ds { for _, d := range ds {
datastore = append(datastore, d.Name()) datastore = append(datastore, d.UUID)
} }
ok(datastore, w, r) ok(datastore, w, r)
@@ -268,8 +259,8 @@ func (s HTTPServer) histUpload(w http.ResponseWriter, r *http.Request) {
} }
defer file.Close() defer file.Close()
if err := data.WriteHist(gameID, s.documentRoot, uuid, file); err != nil { if err := s.Service.CopyBackup(gameID, uuid, file); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to write file to disk:", err) fmt.Fprintln(os.Stderr, "error: failed to write data to disk:", err)
internalServerError(w, r) internalServerError(w, r)
return return
} }
@@ -324,10 +315,10 @@ func (s HTTPServer) histExists(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
uuid := chi.URLParam(r, "uuid") uuid := chi.URLParam(r, "uuid")
finfo, err := data.ArchiveInfo(gameID, s.documentRoot, uuid) finfo, err := s.Service.Backup(gameID, uuid)
if err != nil { if err != nil {
if errors.Is(err, data.ErrBackupNotExists) { if errors.Is(err, repository.ErrNotFound) {
notFound("backup not found", w, r) notFound("not found", w, r)
return return
} }
fmt.Fprintln(os.Stderr, "error: failed to read data:", err) fmt.Fprintln(os.Stderr, "error: failed to read data:", err)
@@ -341,10 +332,10 @@ func (s HTTPServer) histExists(w http.ResponseWriter, r *http.Request) {
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")
sum, err := data.Hash(id, s.documentRoot) m, err := s.Service.One(id)
if err != nil { if err != nil {
if errors.Is(err, data.ErrNotExists) { if errors.Is(err, repository.ErrNotFound) {
notFound("id not found", w, r) notFound("not found", w, r)
return return
} }
fmt.Fprintln(os.Stderr, "error: an error occured while calculating the hash:", err) fmt.Fprintln(os.Stderr, "error: an error occured while calculating the hash:", err)
@@ -352,7 +343,7 @@ func (s HTTPServer) hash(w http.ResponseWriter, r *http.Request) {
return return
} }
ok(sum, w, r) ok(m.MD5, w, r)
} }
func (s HTTPServer) metadata(w http.ResponseWriter, r *http.Request) { func (s HTTPServer) metadata(w http.ResponseWriter, r *http.Request) {

View File

@@ -1,243 +0,0 @@
package data
import (
"cloudsave/pkg/repository"
"cloudsave/pkg/tools/hash"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sync"
)
type (
cache map[string]cachedInfo
cachedInfo struct {
MD5 string
Version int
}
)
var (
ErrBackupNotExists error = errors.New("backup not found")
ErrNotExists error = errors.New("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")
finalFilePath := filepath.Join(dataFolderPath, "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
}
hashCache.Remove(gameID)
return nil
}
func WriteHist(gameID, documentRoot, uuid string, r io.Reader) error {
dataFolderPath := filepath.Join(documentRoot, "data", gameID, "hist", uuid)
partPath := filepath.Join(dataFolderPath, "data.tar.gz.part")
finalFilePath := filepath.Join(dataFolderPath, "data.tar.gz")
if err := makeDataFolder(gameID, documentRoot); err != nil {
return err
}
if err := os.MkdirAll(dataFolderPath, 0740); 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 {
return err
}
path := filepath.Join(documentRoot, "data", gameID, "metadata.json")
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0740)
if err != nil {
return err
}
defer f.Close()
e := json.NewEncoder(f)
return e.Encode(m)
}
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 {
if errors.Is(err, os.ErrNotExist) {
return repository.Backup{}, ErrBackupNotExists
}
return repository.Backup{}, 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{
MD5: h,
})
return repository.Backup{
CreatedAt: finfo.ModTime(),
UUID: uuid,
MD5: h,
}, nil
}
func Hash(gameID, documentRoot string) (string, error) {
path := filepath.Clean(filepath.Join(documentRoot, "data", gameID))
sdir, err := os.Stat(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", ErrNotExists
}
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_RDONLY, 0)
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
}
if err := os.MkdirAll(filepath.Join(documentRoot, "data", gameID, "hist"), 0740); err != nil {
return err
}
return nil
}

View File

@@ -4,6 +4,8 @@ import (
"cloudsave/cmd/server/api" "cloudsave/cmd/server/api"
"cloudsave/cmd/server/security/htpasswd" "cloudsave/cmd/server/security/htpasswd"
"cloudsave/pkg/constants" "cloudsave/pkg/constants"
"cloudsave/pkg/data"
"cloudsave/pkg/repository"
"flag" "flag"
"fmt" "fmt"
"path/filepath" "path/filepath"
@@ -16,16 +18,36 @@ func run() {
var documentRoot string var documentRoot string
var port int var port int
var noCache bool
flag.StringVar(&documentRoot, "document-root", defaultDocumentRoot, "Define the path to the document root") flag.StringVar(&documentRoot, "document-root", defaultDocumentRoot, "Define the path to the document root")
flag.IntVar(&port, "port", 8080, "Define the port of the server") flag.IntVar(&port, "port", 8080, "Define the port of the server")
flag.BoolVar(&noCache, "no-cache", false, "Disable the cache")
flag.Parse() flag.Parse()
h, err := htpasswd.Open(filepath.Join(documentRoot, ".htpasswd")) h, err := htpasswd.Open(filepath.Join(documentRoot, ".htpasswd"))
if err != nil { if err != nil {
fatal("failed to load .htpasswd: "+err.Error(), 1) fatal("failed to load .htpasswd: "+err.Error(), 1)
} }
var repo repository.Repository
if noCache {
r, err := repository.NewEagerRepository(filepath.Join(documentRoot, "data"))
if err != nil {
fatal("failed to load datastore: "+err.Error(), 1)
}
if err := r.Preload(); err != nil {
fatal("failed to load datastore: "+err.Error(), 1)
}
repo = r
} else {
repo, err = repository.NewLazyRepository(filepath.Join(documentRoot, "data"))
if err != nil {
fatal("failed to load datastore: "+err.Error(), 1)
}
}
server := api.NewServer(documentRoot, h.Content(), port) s := data.NewService(repo)
server := api.NewServer(documentRoot, s, h.Content(), port)
fmt.Println("starting server at :" + strconv.Itoa(port)) fmt.Println("starting server at :" + strconv.Itoa(port))
if err := server.Server.ListenAndServe(); err != nil { if err := server.Server.ListenAndServe(); err != nil {

View File

@@ -12,6 +12,7 @@ import (
"net/http" "net/http"
"runtime" "runtime"
"slices" "slices"
"sync"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
@@ -160,38 +161,59 @@ func (s *HTTPServer) detailled(w http.ResponseWriter, r *http.Request) {
return return
} }
save, err := cli.Metadata(id) var wg sync.WaitGroup
if err != nil { var err1, err2, err3 error
if errors.Is(err, client.ErrUnauthorized) { var save repository.Metadata
var h string
var ids []string
wg.Add(1)
go func() {
save, err1 = cli.Metadata(id)
wg.Done()
}()
wg.Add(1)
go func() {
h, err2 = cli.Hash(id)
wg.Done()
}()
wg.Add(1)
go func() {
ids, err3 = cli.ListArchives(id)
wg.Done()
}()
wg.Wait()
if err1 != nil || err2 != nil || err3 != nil {
if errors.Is(err1, client.ErrUnauthorized) {
unauthorized("Unable to access resources", w, r) unauthorized("Unable to access resources", w, r)
return return
} }
slog.Error("unable to connect to the remote", "err", err) slog.Error("unable to connect to the remote", "err", err1)
return return
} }
h, err := cli.Hash(id) wg = sync.WaitGroup{}
if err != nil {
slog.Error("unable to connect to the remote", "err", err)
return
}
ids, err := cli.ListArchives(id)
if err != nil {
slog.Error("unable to connect to the remote", "err", err)
return
}
var bm []repository.Backup var bm []repository.Backup
for _, i := range ids { for _, i := range ids {
b, err := cli.ArchiveInfo(id, i) wg.Add(1)
if err != nil { go func() {
slog.Error("unable to connect to the remote", "err", err) defer wg.Done()
return b, err := cli.ArchiveInfo(id, i)
} if err != nil {
bm = append(bm, b) slog.Error("unable to connect to the remote", "err", err)
return
}
bm = append(bm, b)
}()
} }
wg.Wait()
payload := DetaillePayload{ payload := DetaillePayload{
Save: save, Save: save,
Hash: h, Hash: h,

View File

@@ -1,5 +1,5 @@
package constants package constants
const Version = "0.0.3" const Version = "0.0.4a"
const ApiVersion = 1 const ApiVersion = 1

387
pkg/data/data.go Normal file
View File

@@ -0,0 +1,387 @@
package data
import (
"cloudsave/pkg/remote/client"
"cloudsave/pkg/repository"
"cloudsave/pkg/tools/archive"
"fmt"
"io"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
)
type (
Service struct {
repo repository.Repository
}
)
func NewService(repo repository.Repository) *Service {
return &Service{
repo: repo,
}
}
func (s *Service) Add(name, path, remote string) (string, error) {
gameID := repository.NewGameIdentifier(uuid.NewString())
if err := s.repo.Mkdir(gameID); err != nil {
return "", fmt.Errorf("failed to add game reference: %w", err)
}
m := repository.Metadata{
ID: gameID.Key(),
Name: name,
Path: path,
Version: 0,
Date: time.Now(),
}
if err := s.repo.WriteMetadata(gameID, m); err != nil {
return "", fmt.Errorf("failed to add game reference: %w", err)
}
return gameID.Key(), nil
}
func (s *Service) One(gameID string) (repository.Metadata, error) {
id := repository.NewGameIdentifier(gameID)
m, err := s.repo.Metadata(id)
if err != nil {
return repository.Metadata{}, fmt.Errorf("failed to get metadata: %w", err)
}
return m, nil
}
func (s *Service) Backup(gameID, backupID string) (repository.Backup, error) {
id := repository.NewBackupIdentifier(gameID, backupID)
if err := s.repo.Mkdir(id); err != nil {
return repository.Backup{}, fmt.Errorf("failed to make game dir: %w", err)
}
return s.repo.Backup(id)
}
func (s *Service) UpdateMetadata(gameID string, m repository.Metadata) error {
id := repository.NewGameIdentifier(gameID)
if err := s.repo.Mkdir(id); err != nil {
return fmt.Errorf("failed to make game dir: %w", err)
}
if err := s.repo.WriteMetadata(id, m); err != nil {
return fmt.Errorf("failed to write metadate: %w", err)
}
return nil
}
func (s *Service) Scan(gameID string) error {
id := repository.NewGameIdentifier(gameID)
lastRun, err := s.repo.LastScan(id)
if err != nil {
return fmt.Errorf("failed to get last scan time: %w", err)
}
m, err := s.repo.Metadata(id)
if err != nil {
return fmt.Errorf("failed to get game metadata: %w", err)
}
if !IsDirectoryChanged(m.Path, lastRun) {
return nil
}
f, err := s.repo.WriteBlob(id)
if err != nil {
return fmt.Errorf("failed to get datastore stream: %w", err)
}
if v, ok := f.(io.Closer); ok {
defer v.Close()
}
if err := archive.Tar(f, m.Path); err != nil {
return fmt.Errorf("failed to make archive: %w", err)
}
if err := s.repo.ResetLastScan(id); err != nil {
return fmt.Errorf("failed to reset scan date: %w", err)
}
m.Date = time.Now()
m.Version += 1
if err := s.repo.WriteMetadata(id, m); err != nil {
return fmt.Errorf("failed to update metadata: %w", err)
}
return nil
}
func (s *Service) MakeBackup(gameID string) error {
var id repository.Identifier = repository.NewGameIdentifier(gameID)
src, err := s.repo.ReadBlob(id)
if err != nil {
return err
}
if v, ok := src.(io.Closer); ok {
defer v.Close()
}
id = repository.NewBackupIdentifier(gameID, uuid.NewString())
if err := s.repo.Mkdir(id); err != nil {
return err
}
dst, err := s.repo.WriteBlob(id)
if err != nil {
return err
}
if v, ok := dst.(io.Closer); ok {
defer v.Close()
}
if _, err := io.Copy(dst, src); err != nil {
return err
}
return nil
}
func (s *Service) AllGames() ([]repository.Metadata, error) {
ids, err := s.repo.All()
if err != nil {
return nil, fmt.Errorf("failed to get the list of ids: %w", err)
}
var ms []repository.Metadata
for _, id := range ids {
m, err := s.repo.Metadata(repository.NewGameIdentifier(id))
if err != nil {
return nil, fmt.Errorf("failed to open metadata: %w", err)
}
ms = append(ms, m)
}
return ms, nil
}
func (s *Service) AllBackups(gameID string) ([]repository.Backup, error) {
ids, err := s.repo.AllHist(repository.NewGameIdentifier(gameID))
if err != nil {
return nil, fmt.Errorf("failed to get the list of ids: %w", err)
}
var bs []repository.Backup
for _, id := range ids {
b, err := s.repo.Backup(repository.NewBackupIdentifier(gameID, id))
if err != nil {
return nil, fmt.Errorf("failed to open metadata: %w", err)
}
bs = append(bs, b)
}
return bs, nil
}
func (l Service) PullArchive(gameID, backupID string, cli *client.Client) error {
if len(backupID) > 0 {
path := l.repo.DataPath(repository.NewBackupIdentifier(gameID, backupID))
return cli.PullBackup(gameID, backupID, filepath.Join(path, "data.tar.gz"))
}
path := l.repo.DataPath(repository.NewGameIdentifier(gameID))
return cli.Pull(gameID, filepath.Join(path, "data.tar.gz"))
}
func (l Service) PushArchive(gameID, backupID string, cli *client.Client) error {
m, err := l.repo.Metadata(repository.NewGameIdentifier(gameID))
if err != nil {
return err
}
if len(backupID) > 0 {
path := l.repo.DataPath(repository.NewBackupIdentifier(gameID, backupID))
return cli.PushSave(filepath.Join(path, "data.taz.gz"), m)
}
path := l.repo.DataPath(repository.NewGameIdentifier(gameID))
return cli.PushSave(filepath.Join(path, "data.tar.gz"), m)
}
func (l Service) PullCurrent(id, path string, cli *client.Client) error {
gameID := repository.NewGameIdentifier(id)
if err := l.repo.Mkdir(gameID); err != nil {
return err
}
m, err := cli.Metadata(id)
if err != nil {
return fmt.Errorf("failed to get metadata from the server: %w", err)
}
if err := l.repo.WriteMetadata(gameID, m); err != nil {
return fmt.Errorf("failed to write metadata: %w", err)
}
archivePath := filepath.Join(l.repo.DataPath(gameID), "data.tar.gz")
if err := cli.Pull(id, archivePath); err != nil {
return fmt.Errorf("failed to pull from the server: %w", err)
}
f, err := l.repo.ReadBlob(gameID)
if err != nil {
return fmt.Errorf("failed to open blob from local repository: %w", err)
}
if err := os.MkdirAll(path, 0740); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
if err := archive.Untar(f, path); err != nil {
return fmt.Errorf("failed to untar archive: %w", err)
}
if err := l.repo.ResetLastScan(gameID); err != nil {
return fmt.Errorf("failed to create .last_run file: %w", err)
}
return nil
}
func (l Service) PullBackup(gameID, backupID string, cli *client.Client) error {
id := repository.NewBackupIdentifier(gameID, backupID)
archivePath := filepath.Join(l.repo.DataPath(id), "data.tar.gz")
if err := cli.PullBackup(gameID, backupID, archivePath); err != nil {
return fmt.Errorf("failed to pull backup: %w", err)
}
return nil
}
func (l Service) RemoveGame(gameID string) error {
return l.repo.Remove(repository.NewGameIdentifier(gameID))
}
func (l Service) SetVersion(gameID string, value int) error {
id := repository.NewGameIdentifier(gameID)
m, err := l.repo.Metadata(id)
if err != nil {
return fmt.Errorf("failed to get metadata from the server: %w", err)
}
m.Version = value
if err := l.repo.WriteMetadata(id, m); err != nil {
return fmt.Errorf("failed to write metadata: %w", err)
}
return nil
}
func IsDirectoryChanged(path string, lastRun time.Time) bool {
changed := false
_ = filepath.Walk(path, func(path string, info os.FileInfo, walkErr error) error {
if walkErr != nil {
return nil
}
if info.ModTime().After(lastRun) {
changed = true
return io.EOF // early exit
}
return nil
})
return changed
}
func (l Service) Copy(id string, src io.Reader) error {
dst, err := l.repo.WriteBlob(repository.NewGameIdentifier(id))
if err != nil {
return err
}
if v, ok := dst.(io.Closer); ok {
defer v.Close()
}
if _, err := io.Copy(dst, src); err != nil {
return err
}
return nil
}
func (l Service) CopyBackup(gameID, backupID string, src io.Reader) error {
id := repository.NewBackupIdentifier(gameID, backupID)
if err := l.repo.Mkdir(id); err != nil {
return err
}
dst, err := l.repo.WriteBlob(id)
if err != nil {
return err
}
if v, ok := dst.(io.Closer); ok {
defer v.Close()
}
if _, err := io.Copy(dst, src); err != nil {
return err
}
return nil
}
func (l Service) ApplyCurrent(gameID string) error {
id := repository.NewGameIdentifier(gameID)
path := l.repo.DataPath(id)
g, err := l.repo.Metadata(id)
if err != nil {
return err
}
return l.apply(filepath.Join(path, "data.tar.gz"), g.Path)
}
func (l Service) ApplyBackup(gameID, backupID string) error {
id := repository.NewGameIdentifier(gameID)
fullID := repository.NewBackupIdentifier(gameID, backupID)
path := l.repo.DataPath(fullID)
g, err := l.repo.Metadata(id)
if err != nil {
return err
}
return l.apply(filepath.Join(path, "data.tar.gz"), g.Path)
}
func (l Service) apply(src, dst string) error {
if err := os.RemoveAll(dst); err != nil {
return fmt.Errorf("failed to remove old save: %w", err)
}
f, err := os.OpenFile(src, os.O_RDONLY, 0)
if err != nil {
return fmt.Errorf("failed to open archive: %w", err)
}
defer f.Close()
return archive.Untar(f, dst)
}

View File

@@ -276,15 +276,16 @@ func (c *Client) PullBackup(gameID, uuid, archivePath string) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to open file: %w", err) return fmt.Errorf("failed to open file: %w", err)
} }
defer f.Close()
res, err := cli.Do(req) res, err := cli.Do(req)
if err != nil { if err != nil {
f.Close()
return fmt.Errorf("cannot connect to remote: %w", err) return fmt.Errorf("cannot connect to remote: %w", err)
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
f.Close()
return fmt.Errorf("cannot connect to remote: server return code: %s", res.Status) return fmt.Errorf("cannot connect to remote: server return code: %s", res.Status)
} }
@@ -295,8 +296,10 @@ func (c *Client) PullBackup(gameID, uuid, archivePath string) error {
defer bar.Close() defer bar.Close()
if _, err := io.Copy(io.MultiWriter(f, bar), res.Body); err != nil { if _, err := io.Copy(io.MultiWriter(f, bar), res.Body); err != nil {
f.Close()
return fmt.Errorf("an error occured while copying the file from the remote: %w", err) return fmt.Errorf("an error occured while copying the file from the remote: %w", err)
} }
f.Close()
if err := os.Rename(archivePath+".part", archivePath); err != nil { if err := os.Rename(archivePath+".part", archivePath); err != nil {
return fmt.Errorf("failed to move temporary data: %w", err) return fmt.Errorf("failed to move temporary data: %w", err)

View File

@@ -2,7 +2,6 @@ package repository
import ( import (
"cloudsave/pkg/tools/hash" "cloudsave/pkg/tools/hash"
"cloudsave/pkg/tools/id"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -10,8 +9,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"github.com/google/uuid"
) )
type ( type (
@@ -21,6 +18,12 @@ type (
Path string `json:"path"` Path string `json:"path"`
Version int `json:"version"` Version int `json:"version"`
Date time.Time `json:"date"` Date time.Time `json:"date"`
MD5 string `json:"-"`
}
Remote struct {
URL string `json:"url"`
GameID string `json:"-"`
} }
Backup struct { Backup struct {
@@ -29,354 +32,460 @@ type (
UUID string `json:"uuid"` UUID string `json:"uuid"`
ArchivePath string `json:"-"` ArchivePath string `json:"-"`
} }
Data struct {
Metadata Metadata
Remote *Remote
DataPath string
Backup map[string]Backup
}
GameIdentifier struct {
gameID string
}
BackupIdentifier struct {
gameID string
backupID string
}
Identifier interface {
Key() string
}
LazyRepository struct {
dataRoot string
}
EagerRepository struct {
Repository
data map[string]Data
}
Repository interface {
Mkdir(id Identifier) error
All() ([]string, error)
AllHist(gameID GameIdentifier) ([]string, error)
WriteBlob(ID Identifier) (io.Writer, error)
WriteMetadata(gameID GameIdentifier, m Metadata) error
Metadata(gameID GameIdentifier) (Metadata, error)
LastScan(gameID GameIdentifier) (time.Time, error)
ReadBlob(gameID Identifier) (io.Reader, error)
Backup(id BackupIdentifier) (Backup, error)
Remote(id GameIdentifier) (*Remote, error)
SetRemote(gameID GameIdentifier, url string) error
ResetLastScan(id GameIdentifier) error
DataPath(id Identifier) string
Remove(gameID GameIdentifier) error
}
) )
var ( var (
roaming string ErrNotFound error = errors.New("not found")
datastorepath string
) )
func init() { func NewGameIdentifier(gameID string) GameIdentifier {
var err error return GameIdentifier{
roaming, err = os.UserConfigDir() gameID: gameID,
if err != nil {
panic("failed to get user config path: " + err.Error())
} }
}
func (bi GameIdentifier) Key() string {
return bi.gameID
}
datastorepath = filepath.Join(roaming, "cloudsave", "data") func NewBackupIdentifier(gameID, backupID string) BackupIdentifier {
err = os.MkdirAll(datastorepath, 0740) return BackupIdentifier{
if err != nil { gameID: gameID,
panic("cannot make the datastore:" + err.Error()) backupID: backupID,
} }
} }
func Add(name, path string) (Metadata, error) { func (bi BackupIdentifier) Key() string {
m := Metadata{ return bi.gameID + ":" + bi.backupID
ID: id.New(),
Name: name,
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 Metadata{}, 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 Metadata{}, fmt.Errorf("cannot write into the metadata file in the datastore: %w", err)
}
return m, nil
} }
func Register(m Metadata, path string) error { func NewLazyRepository(dataRootPath string) (*LazyRepository, error) {
m.Path = path if m, err := os.Stat(dataRootPath); err != nil {
if errors.Is(err, os.ErrNotExist) {
err := os.MkdirAll(filepath.Join(datastorepath, m.ID), 0740) if err := os.MkdirAll(dataRootPath, 0740); err != nil {
if err != nil { return nil, fmt.Errorf("failed to make the directory: %w", err)
panic("cannot make directory for the game:" + err.Error()) }
} else {
return nil, fmt.Errorf("failed to open datastore: %w", err)
}
} else {
if !m.IsDir() {
return nil, fmt.Errorf("failed to open datastore: not a directory")
}
} }
f, err := os.OpenFile(filepath.Join(datastorepath, m.ID, "metadata.json"), os.O_CREATE|os.O_WRONLY, 0740) return &LazyRepository{
if err != nil { dataRoot: dataRootPath,
return fmt.Errorf("cannot open the metadata file in the datastore: %w", err) }, nil
} }
defer f.Close()
e := json.NewEncoder(f) func (l *LazyRepository) Mkdir(id Identifier) error {
err = e.Encode(m) return os.MkdirAll(l.DataPath(id), 0740)
}
func (l *LazyRepository) All() ([]string, error) {
dir, err := os.ReadDir(l.dataRoot)
if err != nil { if err != nil {
return fmt.Errorf("cannot write into the metadata file in the datastore: %w", err) return nil, fmt.Errorf("failed to open directory: %w", err)
}
var res []string
for _, d := range dir {
res = append(res, d.Name())
}
return res, nil
}
func (l *LazyRepository) AllHist(id GameIdentifier) ([]string, error) {
path := l.DataPath(id)
dir, err := os.ReadDir(filepath.Join(path, "hist"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, fmt.Errorf("failed to open directory: %w", err)
}
var res []string
for _, d := range dir {
res = append(res, d.Name())
}
return res, nil
}
func (l *LazyRepository) WriteBlob(ID Identifier) (io.Writer, error) {
path := l.DataPath(ID)
dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return nil, fmt.Errorf("failed to open destination file: %w", err)
}
return dst, nil
}
func (l *LazyRepository) WriteMetadata(id GameIdentifier, m Metadata) error {
path := l.DataPath(id)
dst, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return fmt.Errorf("failed to open destination file: %w", err)
}
defer dst.Close()
e := json.NewEncoder(dst)
if err := e.Encode(m); err != nil {
return fmt.Errorf("failed to encode data: %w", err)
} }
return nil return nil
} }
func All() ([]Metadata, error) { func (l *LazyRepository) Metadata(id GameIdentifier) (Metadata, error) {
ds, err := os.ReadDir(datastorepath) path := l.DataPath(id)
if err != nil {
return nil, fmt.Errorf("cannot open the datastore: %w", err)
}
var datastore []Metadata src, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_RDONLY, 0)
for _, d := range ds { if err != nil {
content, err := os.ReadFile(filepath.Join(datastorepath, d.Name(), "metadata.json")) if errors.Is(err, os.ErrNotExist) {
if err != nil { return Metadata{}, ErrNotFound
continue
} }
return Metadata{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err)
var m Metadata
err = json.Unmarshal(content, &m)
if err != nil {
return nil, fmt.Errorf("corrupted datastore: failed to parse %s/metadata.json: %w", d.Name(), err)
}
datastore = append(datastore, m)
}
return datastore, nil
}
func One(gameID string) (Metadata, error) {
_, err := os.ReadDir(datastorepath)
if err != nil {
return Metadata{}, fmt.Errorf("cannot open the datastore: %w", err)
}
content, err := os.ReadFile(filepath.Join(datastorepath, gameID, "metadata.json"))
if err != nil {
return Metadata{}, fmt.Errorf("game not found: %w", err)
} }
var m Metadata var m Metadata
err = json.Unmarshal(content, &m) d := json.NewDecoder(src)
if err := d.Decode(&m); err != nil {
return Metadata{}, fmt.Errorf("corrupted datastore: failed to parse metadata: %w", err)
}
if _, err := os.Stat(filepath.Join(path, "data.tar.gz")); err != nil {
if errors.Is(err, os.ErrNotExist) {
return m, nil
}
return Metadata{}, fmt.Errorf("failed to open archive: %w", err)
}
m.MD5, err = hash.FileMD5(filepath.Join(path, "data.tar.gz"))
if err != nil { if err != nil {
return Metadata{}, fmt.Errorf("corrupted datastore: failed to parse %s/metadata.json: %w", gameID, err) return Metadata{}, fmt.Errorf("failed to calculate md5: %w", err)
} }
return m, nil return m, nil
} }
func MakeArchive(gameID string) error { func (l *LazyRepository) Backup(id BackupIdentifier) (Backup, error) {
path := filepath.Join(datastorepath, gameID, "data.tar.gz") path := l.DataPath(id)
// open old fs, err := os.Stat(filepath.Join(path, "data.tar.gz"))
f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return nil return Backup{}, ErrNotFound
} }
return fmt.Errorf("failed to open old file: %w", err) return Backup{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err)
}
defer f.Close()
histDirPath := filepath.Join(datastorepath, gameID, "hist", uuid.NewString())
if err := os.MkdirAll(histDirPath, 0740); err != nil {
return fmt.Errorf("failed to make directory: %w", err)
} }
// open new h, err := hash.FileMD5(filepath.Join(path, "data.tar.gz"))
nf, err := os.OpenFile(filepath.Join(histDirPath, "data.tar.gz"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil { if err != nil {
return fmt.Errorf("failed to open new file: %w", err) return Backup{}, fmt.Errorf("corrupted datastore: failed to open metadata: %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 return Backup{
} CreatedAt: fs.ModTime(),
func RestoreArchive(gameID, uuid string) error {
histDirPath := filepath.Join(datastorepath, gameID, "hist", uuid)
if err := os.MkdirAll(histDirPath, 0740); err != nil {
return fmt.Errorf("failed to make directory: %w", err)
}
// open old
nf, err := os.OpenFile(filepath.Join(histDirPath, "data.tar.gz"), os.O_RDONLY, 0)
if err != nil {
return fmt.Errorf("failed to open new file: %w", err)
}
defer nf.Close()
path := filepath.Join(datastorepath, gameID, "data.tar.gz")
// open new
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("failed to open old file: %w", err)
}
defer f.Close()
// copy
if _, err := io.Copy(f, nf); err != nil {
return fmt.Errorf("failed to copy data: %w", err)
}
return nil
}
func Archive(gameID, uuid string) (Backup, error) {
histDirPath := filepath.Join(datastorepath, gameID, "hist", uuid)
if err := os.MkdirAll(histDirPath, 0740); err != nil {
return Backup{}, fmt.Errorf("failed to make 'hist' directory")
}
finfo, err := os.Stat(histDirPath)
if err != nil {
return Backup{}, fmt.Errorf("corrupted datastore: %w", err)
}
archivePath := filepath.Join(histDirPath, "data.tar.gz")
h, err := hash.FileMD5(archivePath)
if err != nil {
return Backup{}, fmt.Errorf("failed to calculate md5 hash: %w", err)
}
b := Backup{
CreatedAt: finfo.ModTime(),
UUID: filepath.Base(finfo.Name()),
MD5: h, MD5: h,
ArchivePath: archivePath, UUID: id.backupID,
} ArchivePath: filepath.Join(path, "data.tar.gz"),
}, nil
return b, nil
} }
func Archives(gameID string) ([]Backup, error) { func (l *LazyRepository) LastScan(id GameIdentifier) (time.Time, error) {
histDirPath := filepath.Join(datastorepath, gameID, "hist") path := l.DataPath(id)
if err := os.MkdirAll(histDirPath, 0740); err != nil {
return nil, fmt.Errorf("failed to make 'hist' directory")
}
d, err := os.ReadDir(histDirPath) data, err := os.ReadFile(filepath.Join(path, ".last_run"))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open 'hist' directory") if errors.Is(err, os.ErrNotExist) {
return time.Time{}, nil
}
return time.Time{}, fmt.Errorf("failed to reading state file: %w", err)
} }
var res []Backup lastRun, err := time.Parse(time.RFC3339, string(data))
for _, f := range d { if err != nil {
finfo, err := f.Info() return time.Time{}, fmt.Errorf("parsing state file timestamp: %w", err)
}
return lastRun, nil
}
func (l *LazyRepository) ResetLastScan(id GameIdentifier) error {
path := l.DataPath(id)
f, err := os.OpenFile(filepath.Join(path, ".last_run"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer f.Close()
data := time.Now().Format(time.RFC3339)
if _, err := f.WriteString(data); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
func (l *LazyRepository) ReadBlob(id Identifier) (io.Reader, error) {
path := l.DataPath(id)
dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_RDONLY, 0)
if err != nil {
return nil, fmt.Errorf("failed to open blob: %w", err)
}
return dst, nil
}
func (l *LazyRepository) SetRemote(id GameIdentifier, url string) error {
path := l.DataPath(id)
src, err := os.OpenFile(filepath.Join(path, "remote.json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return fmt.Errorf("failed to open remote description: %w", err)
}
defer src.Close()
var r Remote
r.URL = url
e := json.NewEncoder(src)
if err := e.Encode(r); err != nil {
return fmt.Errorf("failed to marshall remote description: %w", err)
}
return nil
}
func (l *LazyRepository) Remote(id GameIdentifier) (*Remote, error) {
path := l.DataPath(id)
src, err := os.OpenFile(filepath.Join(path, "remote.json"), os.O_RDONLY, 0)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, fmt.Errorf("failed to open remote description: %w", err)
}
defer src.Close()
var r Remote
e := json.NewDecoder(src)
if err := e.Decode(&r); err != nil {
return nil, fmt.Errorf("failed to marshall remote description: %w", err)
}
return &r, nil
}
func (l *LazyRepository) Remove(id GameIdentifier) error {
path := l.DataPath(id)
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("failed to remove game folder from the datastore: %w", err)
}
return nil
}
func (r *LazyRepository) DataPath(id Identifier) string {
switch identifier := id.(type) {
case GameIdentifier:
return filepath.Join(r.dataRoot, identifier.gameID)
case BackupIdentifier:
return filepath.Join(r.dataRoot, identifier.gameID, "hist", identifier.backupID)
}
panic("identifier type not supported")
}
func NewEagerRepository(dataRootPath string) (*EagerRepository, error) {
r, err := NewLazyRepository(dataRootPath)
if err != nil {
return nil, err
}
return &EagerRepository{
Repository: r,
data: make(map[string]Data),
}, nil
}
func (r *EagerRepository) Preload() error {
games, err := r.Repository.All()
if err != nil {
return fmt.Errorf("failed to load all data: %w", err)
}
for _, g := range games {
backup, err := r.Repository.AllHist(NewGameIdentifier(g))
if err != nil { if err != nil {
return nil, fmt.Errorf("corrupted datastore: %w", err) return fmt.Errorf("[%s] failed to load hist data: %w", g, err)
} }
path := filepath.Join(histDirPath, finfo.Name())
archivePath := filepath.Join(path, "data.tar.gz")
h, err := hash.FileMD5(archivePath) remote, err := r.Repository.Remote(NewGameIdentifier(g))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to calculate md5 hash: %w", err) return fmt.Errorf("[%s] failed to load remote metadata: %w", g, err)
} }
b := Backup{ m, err := r.Repository.Metadata(NewGameIdentifier(g))
CreatedAt: finfo.ModTime(), if err != nil {
UUID: filepath.Base(finfo.Name()), return fmt.Errorf("[%s] failed to load metadata: %w", g, err)
MD5: h,
ArchivePath: archivePath,
} }
res = append(res, b) backups := make(map[string]Backup)
for _, b := range backup {
info, err := r.Repository.Backup(NewBackupIdentifier(g, b))
if err != nil {
return fmt.Errorf("[%s] failed to get backup information: %w", g, err)
}
backups[b] = info
}
r.data[g] = Data{
Metadata: m,
Remote: remote,
DataPath: r.DataPath(NewGameIdentifier(g)),
Backup: backups,
}
}
return nil
}
func (r *EagerRepository) All() ([]string, error) {
var res []string
for _, g := range r.data {
res = append(res, g.Metadata.ID)
}
return res, nil
}
func (r *EagerRepository) AllHist(id GameIdentifier) ([]string, error) {
var res []string
if d, ok := r.data[id.gameID]; ok {
for _, b := range d.Backup {
res = append(res, b.UUID)
}
} }
return res, nil return res, nil
} }
func DatastorePath() string { func (r *EagerRepository) WriteMetadata(id GameIdentifier, m Metadata) error {
return datastorepath err := r.Repository.WriteMetadata(id, m)
}
func Remove(gameID string) error {
err := os.RemoveAll(filepath.Join(datastorepath, gameID))
if err != nil {
return err
}
return nil
}
func Hash(gameID string) (string, error) {
path := filepath.Join(datastorepath, gameID, "data.tar.gz")
return hash.FileMD5(path)
}
func Version(gameID string) (int, error) {
path := filepath.Join(datastorepath, gameID, "metadata.json")
f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
return 0, err
}
defer f.Close()
var metadata Metadata
d := json.NewDecoder(f)
err = d.Decode(&metadata)
if err != nil {
return 0, err
}
return metadata.Version, nil
}
func SetVersion(gameID string, version int) error {
path := filepath.Join(datastorepath, gameID, "metadata.json")
f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil { if err != nil {
return err return err
} }
var metadata Metadata d := r.data[id.gameID]
d := json.NewDecoder(f) d.Metadata = m
err = d.Decode(&metadata) r.data[id.gameID] = d
if err != nil {
f.Close()
return err
}
f.Close()
metadata.Version = version
f, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0740)
if err != nil {
return err
}
defer f.Close()
e := json.NewEncoder(f)
err = e.Encode(metadata)
if err != nil {
return err
}
return nil return nil
} }
func SetDate(gameID string, dt time.Time) error { func (r *EagerRepository) Metadata(id GameIdentifier) (Metadata, error) {
path := filepath.Join(datastorepath, gameID, "metadata.json") if d, ok := r.data[id.gameID]; ok {
return d.Metadata, nil
}
return Metadata{}, ErrNotFound
}
f, err := os.OpenFile(path, os.O_RDONLY, 0) func (r *EagerRepository) Backup(id BackupIdentifier) (Backup, error) {
if d, ok := r.data[id.gameID]; ok {
if b, ok := d.Backup[id.backupID]; ok {
return b, nil
}
}
return Backup{}, ErrNotFound
}
func (r *EagerRepository) SetRemote(id GameIdentifier, url string) error {
err := r.Repository.SetRemote(id, url)
if err != nil { if err != nil {
return err return err
} }
var metadata Metadata d := r.data[id.gameID]
d := json.NewDecoder(f) d.Remote = &Remote{
err = d.Decode(&metadata) URL: url,
if err != nil { GameID: d.Metadata.ID,
f.Close()
return err
}
f.Close()
metadata.Date = dt
f, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0740)
if err != nil {
return err
}
defer f.Close()
e := json.NewEncoder(f)
err = e.Encode(metadata)
if err != nil {
return err
} }
r.data[id.gameID] = d
return nil return nil
} }
func (r *EagerRepository) Remove(id GameIdentifier) error {
if err := r.Repository.Remove(id); err != nil {
return err
}
delete(r.data, id.gameID)
return nil
}