15 Commits

Author SHA1 Message Date
8518503d40 version 2025-09-02 19:30:43 +02:00
fdc019a200 fixes 2025-09-02 19:21:03 +02:00
7bf88d9d8c Add jenkinsfile
All checks were successful
CloudSave/pipeline/head This commit looks good
2025-08-29 14:37:05 +02:00
7ec9432d7b change version 2025-08-25 22:14:19 +02:00
044d49a9dc fix error when add 2025-08-25 22:13:23 +02:00
9a14571c31 fixes (the last) 2025-08-18 22:10:27 +02:00
0f2c0e511f fixes 2025-08-18 21:28:01 +02:00
0d92b6b8a0 multiple fix again 2025-08-18 20:52:06 +02:00
2ff191fecf more fix 2025-08-18 20:04:32 +02:00
b2425d310b fix things 2025-08-18 19:38:42 +02:00
97cd8f065f fix things 2025-08-18 19:38:34 +02:00
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
24 changed files with 359 additions and 255 deletions

11
.vscode/launch.json vendored
View File

@@ -4,6 +4,15 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "web",
"type": "go",
"request": "launch",
"mode": "auto",
"args": ["-config", "${workspaceFolder}/env/config.json"],
"console": "integratedTerminal",
"program": "${workspaceFolder}/cmd/web"
},
{ {
"name": "server", "name": "server",
"type": "go", "type": "go",
@@ -18,7 +27,7 @@
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"args": ["run"], "args": ["sync"],
"console": "integratedTerminal", "console": "integratedTerminal",
"program": "${workspaceFolder}/cmd/cli" "program": "${workspaceFolder}/cmd/cli"
} }

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]"
@@ -47,11 +47,11 @@ for platform in "${platforms[@]}"; do
fi fi
if [ "$MAKE_PACKAGE" == "true" ]; then if [ "$MAKE_PACKAGE" == "true" ]; then
CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} go build -o build/cloudsave_server$EXT -a ./cmd/server CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} GORISCV64=rva22u64 GOAMD64=v3 GOARM64=v8.2 go build -o build/cloudsave_server$EXT -a ./cmd/server
tar -czf build/server_${platform_split[0]}_${platform_split[1]}.tar.gz build/cloudsave_server$EXT tar -czf build/server_${platform_split[0]}_${platform_split[1]}.tar.gz build/cloudsave_server$EXT
rm build/cloudsave_server$EXT rm build/cloudsave_server$EXT
else else
CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} go build -o build/cloudsave_server_${platform_split[0]}_${platform_split[1]}$EXT -a ./cmd/server CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} GORISCV64=rva22u64 GOAMD64=v3 GOARM64=v8.2 go build -o build/cloudsave_server_${platform_split[0]}_${platform_split[1]}$EXT -a ./cmd/server
fi fi
done done

View File

@@ -56,7 +56,7 @@ func (p *AddCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
return subcommands.ExitFailure return subcommands.ExitFailure
} }
if err := p.Service.Scan(gameID); err != nil { if _, err := p.Service.Scan(gameID); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to scan:", err) fmt.Fprintln(os.Stderr, "error: failed to scan:", err)
return subcommands.ExitFailure return subcommands.ExitFailure
} }

View File

@@ -11,24 +11,24 @@ import (
) )
type ( type (
ListCmd struct { ApplyCmd struct {
Service *data.Service 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() < 1 { 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

View File

@@ -71,7 +71,9 @@ func (p *ListCmd) 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 := p.Service.AllBackups(g.ID) bk, err := p.Service.AllBackups(g.ID)
if err != nil { if err != nil {
@@ -108,7 +110,9 @@ func (p *ListCmd) server(url, username, password string, includeBackup bool) err
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

@@ -78,7 +78,7 @@ func (p *RemoteCmd) print() 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

@@ -37,15 +37,16 @@ func (p *RunCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
} }
for _, metadata := range datastore { for _, metadata := range datastore {
if err := p.Service.MakeBackup(metadata.ID); err != nil { changed, err := p.Service.Scan(metadata.ID)
fmt.Fprintln(os.Stderr, "error: failed to make backup:", err) if err != nil {
return subcommands.ExitFailure fmt.Println("❌", metadata.Name, ":", err.Error())
continue
} }
if err := p.Service.Scan(metadata.ID); err != nil { if changed {
fmt.Fprintln(os.Stderr, "error: failed to scan:", err) fmt.Println("✅", metadata.Name, ": backed up")
return subcommands.ExitFailure } else {
fmt.Println("🆗", metadata.Name, ": up to date")
} }
fmt.Println("✅", metadata.Name)
} }
fmt.Println("done.") fmt.Println("done.")

View File

@@ -49,7 +49,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
r, err := remote.One(g.ID) r, err := remote.One(g.ID)
if err != nil { if err != nil {
if errors.Is(err, remote.ErrNoRemote) { if errors.Is(err, remote.ErrNoRemote) {
fmt.Println(g.Name + ": no remote configured") fmt.Println("⬛", g.Name+": no remote configured")
continue continue
} }
fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err) fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err)
@@ -66,7 +66,6 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
pg.Finish() pg.Finish()
pg.Clear() pg.Clear()
pg.Close() pg.Close()
} }
pg.Describe(fmt.Sprintf("[%s] Checking status...", g.Name)) pg.Describe(fmt.Sprintf("[%s] Checking status...", g.Name))
@@ -88,19 +87,13 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
destroyPg() destroyPg()
slog.Warn("failed to push backup files", "err", err) slog.Warn("failed to push backup files", "err", err)
} }
fmt.Println(g.Name + ": pushed") destroyPg()
fmt.Println("⬆️", g.Name+": pushed")
continue continue
} }
pg.Describe(fmt.Sprintf("[%s] Fetching metadata...", g.Name)) pg.Describe(fmt.Sprintf("[%s] Fetching metadata...", g.Name))
hremote, err := cli.Hash(r.GameID)
if err != nil {
destroyPg()
fmt.Fprintln(os.Stderr, "error: failed to get the file hash from the remote:", err)
continue
}
remoteMetadata, err := cli.Metadata(r.GameID) remoteMetadata, err := cli.Metadata(r.GameID)
if err != nil { if err != nil {
destroyPg() destroyPg()
@@ -118,7 +111,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
slog.Warn("failed to push backup files", "err", err) slog.Warn("failed to push backup files", "err", err)
} }
if g.MD5 == hremote { if g.MD5 == remoteMetadata.MD5 {
destroyPg() destroyPg()
if g.Version != 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")
@@ -127,7 +120,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
continue continue
} }
} }
fmt.Println(g.Name + ": already up-to-date") fmt.Println("🆗", g.Name+": already up-to-date")
continue continue
} }
@@ -139,13 +132,13 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
return subcommands.ExitFailure return subcommands.ExitFailure
} }
destroyPg() destroyPg()
fmt.Println(g.Name + ": pushed") fmt.Println("⬆️", g.Name+": pushed")
continue continue
} }
if g.Version < remoteMetadata.Version { if g.Version < remoteMetadata.Version {
destroyPg() destroyPg()
if err := p.pull(r.GameID, cli); err != nil { if err := p.pull(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
@@ -159,7 +152,7 @@ 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
} }
fmt.Println(g.Name + ": pulled") fmt.Println("⬇️", g.Name+": pulled")
continue continue
} }
@@ -186,7 +179,7 @@ func (p *SyncCmd) conflict(gameID string, m, remoteMetadata repository.Metadata,
return nil return nil
} }
fmt.Println() fmt.Println()
fmt.Println("--- /!\\ CONFLICT ---") fmt.Println("--- ⚠️ CONFLICT ---")
fmt.Println(g.Name, "(", g.Path, ")") fmt.Println(g.Name, "(", g.Path, ")")
fmt.Println("----") fmt.Println("----")
fmt.Println("Your version:", g.Date.Format(time.RFC1123)) fmt.Println("Your version:", g.Date.Format(time.RFC1123))
@@ -205,7 +198,7 @@ func (p *SyncCmd) conflict(gameID string, m, remoteMetadata repository.Metadata,
case prompt.Their: case prompt.Their:
{ {
if err := p.pull(gameID, cli); err != nil { if err := p.pull(g, cli); err != nil {
return fmt.Errorf("failed to push: %w", err) return fmt.Errorf("failed to push: %w", err)
} }
g.Version = remoteMetadata.Version g.Version = remoteMetadata.Version
@@ -273,8 +266,11 @@ func (p *SyncCmd) pullBackup(m repository.Metadata, cli *client.Client) error {
return nil return nil
} }
func (p *SyncCmd) pull(gameID string, cli *client.Client) error { func (p *SyncCmd) pull(g repository.Metadata, cli *client.Client) error {
return p.Service.PullArchive(gameID, "", cli) if err := p.Service.PullArchive(g.ID, "", cli); err != nil {
return err
}
return p.Service.ApplyCurrent(g.ID)
} }
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) {
@@ -285,6 +281,9 @@ func connect(remoteCred map[string]map[string]string, r remote.Remote) (*client.
return cli, nil return cli, nil
} }
fmt.Println()
fmt.Println("Connexion to", r.URL)
fmt.Println("============")
username, password, err := credentials.Read() username, password, err := credentials.Read()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read std output: %w", err) return nil, fmt.Errorf("failed to read std output: %w", err)

View File

@@ -51,7 +51,7 @@ func main() {
subcommands.Register(&remove.RemoveCmd{Service: s}, "management") subcommands.Register(&remove.RemoveCmd{Service: s}, "management")
subcommands.Register(&show.ShowCmd{Service: s}, "management") subcommands.Register(&show.ShowCmd{Service: s}, "management")
subcommands.Register(&apply.ListCmd{Service: s}, "restore") subcommands.Register(&apply.ApplyCmd{Service: s}, "restore")
subcommands.Register(&remote.RemoteCmd{Service: s}, "remote") subcommands.Register(&remote.RemoteCmd{Service: s}, "remote")
subcommands.Register(&sync.SyncCmd{Service: s}, "remote") subcommands.Register(&sync.SyncCmd{Service: s}, "remote")

View File

@@ -3,7 +3,6 @@ package api
import ( import (
"cloudsave/pkg/data" "cloudsave/pkg/data"
"cloudsave/pkg/repository" "cloudsave/pkg/repository"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -36,7 +35,7 @@ func NewServer(documentRoot string, srv *data.Service, creds map[string]string,
} }
router := chi.NewRouter() router := chi.NewRouter()
router.NotFound(func(writer http.ResponseWriter, request *http.Request) { router.NotFound(func(writer http.ResponseWriter, request *http.Request) {
notFound("This route does not exist", writer, request) notFound("id not found", writer, request)
}) })
router.MethodNotAllowed(func(writer http.ResponseWriter, request *http.Request) { router.MethodNotAllowed(func(writer http.ResponseWriter, request *http.Request) {
methodNotAllowed(writer, request) methodNotAllowed(writer, request)
@@ -61,7 +60,6 @@ func NewServer(documentRoot string, srv *data.Service, creds map[string]string,
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.Get("/{id}/data", s.download) saveRouter.Get("/{id}/data", s.download)
saveRouter.Get("/{id}/hash", s.hash)
saveRouter.Get("/{id}/metadata", s.metadata) saveRouter.Get("/{id}/metadata", s.metadata)
saveRouter.Get("/{id}/hist", s.allHist) saveRouter.Get("/{id}/hist", s.allHist)
@@ -81,43 +79,13 @@ func NewServer(documentRoot string, srv *data.Service, creds map[string]string,
} }
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") datastore, err := s.Service.AllGames()
datastore := make([]repository.Metadata, 0)
if _, err := os.Stat(path); err != nil {
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) slog.Error(err.Error())
internalServerError(w, r) internalServerError(w, r)
return return
} }
for _, d := range ds {
content, err := os.ReadFile(filepath.Join(path, d.Name(), "metadata.json"))
if err != nil {
slog.Error("error: failed to load metadata.json", "err", err)
continue
}
var m repository.Metadata
err = json.Unmarshal(content, &m)
if err != nil {
fmt.Fprintf(os.Stderr, "corrupted datastore: failed to parse %s/metadata.json: %s", d.Name(), err)
internalServerError(w, r)
}
datastore = append(datastore, m)
}
ok(datastore, w, r) ok(datastore, w, r)
} }
@@ -125,32 +93,19 @@ func (s HTTPServer) download(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))
sdir, err := os.Stat(path) fi, err := os.Stat(filepath.Join(path, "data.tar.gz"))
if err != nil { if err != nil {
notFound("id not found", w, r) notFound("id not found", w, r)
return return
} }
if !sdir.IsDir() { f, err := s.Service.Repository().ReadBlob(repository.NewGameIdentifier(id))
notFound("id not found", w, r)
return
}
path = filepath.Join(path, "data.tar.gz")
f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil { if err != nil {
notFound("id not found", w, r) slog.Error(err.Error())
return
}
defer f.Close()
// Get file info to set headers
fi, err := f.Stat()
if err != nil || fi.IsDir() {
internalServerError(w, r) internalServerError(w, r)
return return
} }
defer f.Close()
// Set headers // Set headers
w.Header().Set("Content-Disposition", "attachment; filename=\"data.tar.gz\"") w.Header().Set("Content-Disposition", "attachment; filename=\"data.tar.gz\"")
@@ -209,6 +164,12 @@ func (s HTTPServer) upload(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := s.Service.ReloadCache(id); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to reload data from the disk:", err)
internalServerError(w, r)
return
}
// Respond success // Respond success
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
} }
@@ -260,7 +221,13 @@ func (s HTTPServer) histUpload(w http.ResponseWriter, r *http.Request) {
defer file.Close() defer file.Close()
if err := s.Service.CopyBackup(gameID, uuid, file); err != nil { if err := s.Service.CopyBackup(gameID, uuid, file); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to write data to disk:", err) fmt.Fprintln(os.Stderr, "error: failed to write data to the disk:", err)
internalServerError(w, r)
return
}
if err := s.Service.ReloadCache(gameID); err != nil {
fmt.Fprintln(os.Stderr, "error: failed to reload data from the disk:", err)
internalServerError(w, r) internalServerError(w, r)
return return
} }
@@ -274,32 +241,19 @@ func (s HTTPServer) histDownload(w http.ResponseWriter, r *http.Request) {
uuid := chi.URLParam(r, "uuid") uuid := chi.URLParam(r, "uuid")
path := filepath.Clean(filepath.Join(s.documentRoot, "data", id, "hist", uuid)) path := filepath.Clean(filepath.Join(s.documentRoot, "data", id, "hist", uuid))
sdir, err := os.Stat(path) fi, err := os.Stat(filepath.Join(path, "data.tar.gz"))
if err != nil { if err != nil {
notFound("id not found", w, r) notFound("id not found", w, r)
return return
} }
if !sdir.IsDir() { f, err := s.Service.Repository().ReadBlob(repository.NewBackupIdentifier(id, uuid))
notFound("id not found", w, r)
return
}
path = filepath.Join(path, "data.tar.gz")
f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil { if err != nil {
notFound("id not found", w, r) slog.Error(err.Error())
return
}
defer f.Close()
// Get file info to set headers
fi, err := f.Stat()
if err != nil || fi.IsDir() {
internalServerError(w, r) internalServerError(w, r)
return return
} }
defer f.Close()
// Set headers // Set headers
w.Header().Set("Content-Disposition", "attachment; filename=\"data.tar.gz\"") w.Header().Set("Content-Disposition", "attachment; filename=\"data.tar.gz\"")
@@ -329,56 +283,18 @@ func (s HTTPServer) histExists(w http.ResponseWriter, r *http.Request) {
ok(finfo, w, r) ok(finfo, w, r)
} }
func (s HTTPServer) hash(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
m, err := s.Service.One(id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
notFound("not found", w, r)
return
}
fmt.Fprintln(os.Stderr, "error: an error occured while calculating the hash:", err)
internalServerError(w, r)
return
}
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) {
id := chi.URLParam(r, "id") id := chi.URLParam(r, "id")
path := filepath.Clean(filepath.Join(s.documentRoot, "data", id)) metadata, err := s.Service.One(id)
sdir, err := os.Stat(path)
if err != nil { if err != nil {
notFound("id not found", w, r) if errors.Is(err, repository.ErrNotFound) {
return notFound("id not found", w, r)
} return
}
if !sdir.IsDir() { slog.Error(err.Error())
notFound("id not found", w, r)
return
}
path = filepath.Join(path, "metadata.json")
f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
notFound("id not found", w, r)
return
}
defer f.Close()
var metadata repository.Metadata
d := json.NewDecoder(f)
err = d.Decode(&metadata)
if err != nil {
fmt.Fprintln(os.Stderr, "error: an error occured while reading data:", err)
internalServerError(w, r) internalServerError(w, r)
return return
} }
ok(metadata, w, r) ok(metadata, w, r)
} }

View File

@@ -3,13 +3,13 @@ package api
import ( import (
"cloudsave/pkg/remote/obj" "cloudsave/pkg/remote/obj"
"encoding/json" "encoding/json"
"log" "log/slog"
"net/http" "net/http"
"time" "time"
) )
func internalServerError(w http.ResponseWriter, r *http.Request) { func internalServerError(w http.ResponseWriter, r *http.Request) {
e := obj.HTTPError{ payload := obj.HTTPError{
HTTPCore: obj.HTTPCore{ HTTPCore: obj.HTTPCore{
Status: http.StatusInternalServerError, Status: http.StatusInternalServerError,
Path: r.RequestURI, Path: r.RequestURI,
@@ -19,20 +19,16 @@ func internalServerError(w http.ResponseWriter, r *http.Request) {
Message: "The server encountered an unexpected condition that prevented it from fulfilling the request.", Message: "The server encountered an unexpected condition that prevented it from fulfilling the request.",
} }
payload, err := json.Marshal(e)
if err != nil {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write(payload) e := json.NewEncoder(w)
if err != nil { if err := e.Encode(payload); err != nil {
log.Println(err) slog.Error(err.Error())
} }
} }
func notFound(message string, w http.ResponseWriter, r *http.Request) { func notFound(message string, w http.ResponseWriter, r *http.Request) {
e := obj.HTTPError{ payload := obj.HTTPError{
HTTPCore: obj.HTTPCore{ HTTPCore: obj.HTTPCore{
Status: http.StatusNotFound, Status: http.StatusNotFound,
Path: r.RequestURI, Path: r.RequestURI,
@@ -42,20 +38,16 @@ func notFound(message string, w http.ResponseWriter, r *http.Request) {
Message: message, Message: message,
} }
payload, err := json.Marshal(e)
if err != nil {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
_, err = w.Write(payload) e := json.NewEncoder(w)
if err != nil { if err := e.Encode(payload); err != nil {
log.Println(err) slog.Error(err.Error())
} }
} }
func methodNotAllowed(w http.ResponseWriter, r *http.Request) { func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
e := obj.HTTPError{ payload := obj.HTTPError{
HTTPCore: obj.HTTPCore{ HTTPCore: obj.HTTPCore{
Status: http.StatusMethodNotAllowed, Status: http.StatusMethodNotAllowed,
Path: r.RequestURI, Path: r.RequestURI,
@@ -65,20 +57,16 @@ func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
Message: "The server knows the request method, but the target resource doesn't support this method", Message: "The server knows the request method, but the target resource doesn't support this method",
} }
payload, err := json.Marshal(e)
if err != nil {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
_, err = w.Write(payload) e := json.NewEncoder(w)
if err != nil { if err := e.Encode(payload); err != nil {
log.Println(err) slog.Error(err.Error())
} }
} }
func unauthorized(w http.ResponseWriter, r *http.Request) { func unauthorized(w http.ResponseWriter, r *http.Request) {
e := obj.HTTPError{ payload := obj.HTTPError{
HTTPCore: obj.HTTPCore{ HTTPCore: obj.HTTPCore{
Status: http.StatusUnauthorized, Status: http.StatusUnauthorized,
Path: r.RequestURI, Path: r.RequestURI,
@@ -88,21 +76,17 @@ func unauthorized(w http.ResponseWriter, r *http.Request) {
Message: "The request has not been completed because it lacks valid authentication credentials for the requested resource.", Message: "The request has not been completed because it lacks valid authentication credentials for the requested resource.",
} }
payload, err := json.Marshal(e)
if err != nil {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.Header().Add("WWW-Authenticate", "Custom realm=\"loginUserHandler via /api/login\"") w.Header().Add("WWW-Authenticate", "Custom realm=\"loginUserHandler via /api/login\"")
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
_, err = w.Write(payload) e := json.NewEncoder(w)
if err != nil { if err := e.Encode(payload); err != nil {
log.Println(err) slog.Error(err.Error())
} }
} }
func ok(o interface{}, w http.ResponseWriter, r *http.Request) { func ok(o interface{}, w http.ResponseWriter, r *http.Request) {
e := obj.HTTPObject{ payload := obj.HTTPObject{
HTTPCore: obj.HTTPCore{ HTTPCore: obj.HTTPCore{
Status: http.StatusOK, Status: http.StatusOK,
Path: r.RequestURI, Path: r.RequestURI,
@@ -110,20 +94,15 @@ func ok(o interface{}, w http.ResponseWriter, r *http.Request) {
}, },
Data: o, Data: o,
} }
payload, err := json.Marshal(e)
if err != nil {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
_, err = w.Write(payload) e := json.NewEncoder(w)
if err != nil { if err := e.Encode(payload); err != nil {
log.Println(err) slog.Error(err.Error())
} }
} }
func badRequest(message string, w http.ResponseWriter, r *http.Request) { func badRequest(message string, w http.ResponseWriter, r *http.Request) {
e := obj.HTTPError{ payload := obj.HTTPError{
HTTPCore: obj.HTTPCore{ HTTPCore: obj.HTTPCore{
Status: http.StatusBadRequest, Status: http.StatusBadRequest,
Path: r.RequestURI, Path: r.RequestURI,
@@ -133,14 +112,10 @@ func badRequest(message string, w http.ResponseWriter, r *http.Request) {
Message: message, Message: message,
} }
payload, err := json.Marshal(e)
if err != nil {
log.Println(err)
}
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
_, err = w.Write(payload) e := json.NewEncoder(w)
if err != nil { if err := e.Encode(payload); err != nil {
log.Println(err) slog.Error(err.Error())
} }
} }

View File

@@ -2,12 +2,19 @@ package main
import ( import (
"cloudsave/pkg/tools/windows" "cloudsave/pkg/tools/windows"
_ "embed"
"os" "os"
"github.com/getlantern/systray"
) )
const defaultDocumentRoot string = "C:/ProgramData/CloudSave" const defaultDocumentRoot string = "C:\\ProgramData\\CloudSave"
//go:embed res/icon.ico
var icon []byte
func main() { func main() {
go systray.Run(onReady, onExit)
run() run()
} }
@@ -15,3 +22,20 @@ func fatal(message string, exitCode int) {
windows.MessageBox(windows.NULL, message, "CloudSave", windows.MB_OK) windows.MessageBox(windows.NULL, message, "CloudSave", windows.MB_OK)
os.Exit(exitCode) os.Exit(exitCode)
} }
func onReady() {
systray.SetTitle("CloudSave")
systray.SetTooltip("CloudSave")
systray.SetIcon(icon)
mQuit := systray.AddMenuItem("Quit", "Quit")
go func() {
<-mQuit.ClickedCh
os.Exit(0)
}()
}
func onExit() {
// clean up here
}

BIN
cmd/server/res/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -8,6 +8,7 @@ import (
"cloudsave/pkg/repository" "cloudsave/pkg/repository"
"flag" "flag"
"fmt" "fmt"
"log/slog"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
@@ -18,18 +19,27 @@ func run() {
var documentRoot string var documentRoot string
var port int var port int
var noCache bool var noCache, verbose 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.BoolVar(&noCache, "no-cache", false, "Disable the cache")
flag.BoolVar(&verbose, "verbose", false, "Show more logs")
flag.Parse() flag.Parse()
if verbose {
slog.SetLogLoggerLevel(slog.LevelDebug)
}
slog.Info("loading .htpasswd")
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)
} }
slog.Info("users loaded: " + strconv.Itoa(len(h.Content())) + " user(s) loaded")
var repo repository.Repository var repo repository.Repository
if noCache { if !noCache {
slog.Info("loading eager repository...")
r, err := repository.NewEagerRepository(filepath.Join(documentRoot, "data")) r, err := repository.NewEagerRepository(filepath.Join(documentRoot, "data"))
if err != nil { if err != nil {
fatal("failed to load datastore: "+err.Error(), 1) fatal("failed to load datastore: "+err.Error(), 1)
@@ -39,17 +49,19 @@ func run() {
} }
repo = r repo = r
} else { } else {
slog.Info("loading lazy repository...")
repo, err = repository.NewLazyRepository(filepath.Join(documentRoot, "data")) repo, err = repository.NewLazyRepository(filepath.Join(documentRoot, "data"))
if err != nil { if err != nil {
fatal("failed to load datastore: "+err.Error(), 1) fatal("failed to load datastore: "+err.Error(), 1)
} }
} }
slog.Info("repository loaded")
s := data.NewService(repo) s := data.NewService(repo)
server := api.NewServer(documentRoot, s, h.Content(), port) server := api.NewServer(documentRoot, s, h.Content(), port)
fmt.Println("starting server at :" + strconv.Itoa(port)) fmt.Println("server started at :" + strconv.Itoa(port))
if err := server.Server.ListenAndServe(); err != nil { if err := server.Server.ListenAndServe(); err != nil {
fatal("failed to start server: "+err.Error(), 1) fatal("failed to start server: "+err.Error(), 1)
} }

View File

@@ -162,9 +162,8 @@ func (s *HTTPServer) detailled(w http.ResponseWriter, r *http.Request) {
} }
var wg sync.WaitGroup var wg sync.WaitGroup
var err1, err2, err3 error var err1, err2 error
var save repository.Metadata var save repository.Metadata
var h string
var ids []string var ids []string
wg.Add(1) wg.Add(1)
@@ -175,24 +174,18 @@ func (s *HTTPServer) detailled(w http.ResponseWriter, r *http.Request) {
wg.Add(1) wg.Add(1)
go func() { go func() {
h, err2 = cli.Hash(id) ids, err2 = cli.ListArchives(id)
wg.Done()
}()
wg.Add(1)
go func() {
ids, err3 = cli.ListArchives(id)
wg.Done() wg.Done()
}() }()
wg.Wait() wg.Wait()
if err1 != nil || err2 != nil || err3 != nil { if err1 != nil || err2 != nil {
if errors.Is(err1, client.ErrUnauthorized) { 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", err1) slog.Error("failed to get metadata: unable to connect to the remote", "err", err1)
return return
} }
@@ -205,7 +198,7 @@ func (s *HTTPServer) detailled(w http.ResponseWriter, r *http.Request) {
defer wg.Done() defer wg.Done()
b, err := cli.ArchiveInfo(id, i) b, err := cli.ArchiveInfo(id, i)
if err != nil { if err != nil {
slog.Error("unable to connect to the remote", "err", err) slog.Error("failed to get backup: unable to connect to the remote", "err", err)
return return
} }
bm = append(bm, b) bm = append(bm, b)
@@ -216,7 +209,6 @@ func (s *HTTPServer) detailled(w http.ResponseWriter, r *http.Request) {
payload := DetaillePayload{ payload := DetaillePayload{
Save: save, Save: save,
Hash: h,
BackupMetadata: bm, BackupMetadata: bm,
Version: constants.Version, Version: constants.Version,
} }

View File

@@ -30,7 +30,7 @@
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item">UUID: {{.Save.ID}}</li> <li class="list-group-item">UUID: {{.Save.ID}}</li>
<li class="list-group-item">Last Upload: {{.Save.Date}}</li> <li class="list-group-item">Last Upload: {{.Save.Date}}</li>
<li class="list-group-item">Hash (MD5): {{.Hash}}</li> <li class="list-group-item">Hash (MD5): {{.Save.MD5}}</li>
</ul> </ul>
<hr /> <hr />

9
go.mod
View File

@@ -3,6 +3,7 @@ module cloudsave
go 1.24 go 1.24
require ( require (
github.com/getlantern/systray v1.2.2
github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/chi/v5 v5.2.1
github.com/google/subcommands v1.2.0 github.com/google/subcommands v1.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
@@ -12,7 +13,15 @@ require (
) )
require ( require (
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect
) )

27
go.sum
View File

@@ -1,30 +1,57 @@
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

11
jenkinsfile Normal file
View File

@@ -0,0 +1,11 @@
pipeline {
agent any
stages {
stage('Build') {
steps {
sh './build.sh'
}
}
}
}

View File

@@ -1,5 +1,5 @@
package constants package constants
const Version = "0.0.5" const Version = "0.0.4d"
const ApiVersion = 1 const ApiVersion = 1

View File

@@ -4,6 +4,7 @@ import (
"cloudsave/pkg/remote/client" "cloudsave/pkg/remote/client"
"cloudsave/pkg/repository" "cloudsave/pkg/repository"
"cloudsave/pkg/tools/archive" "cloudsave/pkg/tools/archive"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -36,7 +37,7 @@ func (s *Service) Add(name, path, remote string) (string, error) {
ID: gameID.Key(), ID: gameID.Key(),
Name: name, Name: name,
Path: path, Path: path,
Version: 1, Version: 0,
Date: time.Now(), Date: time.Now(),
} }
@@ -82,47 +83,51 @@ func (s *Service) UpdateMetadata(gameID string, m repository.Metadata) error {
return nil return nil
} }
func (s *Service) Scan(gameID string) error { func (s *Service) Scan(gameID string) (bool, error) {
id := repository.NewGameIdentifier(gameID) id := repository.NewGameIdentifier(gameID)
lastRun, err := s.repo.LastScan(id) lastRun, err := s.repo.LastScan(id)
if err != nil { if err != nil {
return fmt.Errorf("failed to get last scan time: %w", err) return false, fmt.Errorf("failed to get last scan time: %w", err)
} }
m, err := s.repo.Metadata(id) m, err := s.repo.Metadata(id)
if err != nil { if err != nil {
return fmt.Errorf("failed to get game metadata: %w", err) return false, fmt.Errorf("failed to get game metadata: %w", err)
} }
if !IsDirectoryChanged(m.Path, lastRun) { if !IsDirectoryChanged(m.Path, lastRun) {
return nil return false, nil
}
if err := s.MakeBackup(gameID); err != nil {
return false, fmt.Errorf("failed to make the backup: %w", err)
} }
f, err := s.repo.WriteBlob(id) f, err := s.repo.WriteBlob(id)
if err != nil { if err != nil {
return fmt.Errorf("failed to get datastore stream: %w", err) return false, fmt.Errorf("failed to get datastore stream: %w", err)
} }
if v, ok := f.(io.Closer); ok { if v, ok := f.(io.Closer); ok {
defer v.Close() defer v.Close()
} }
if err := archive.Tar(f, m.Path); err != nil { if err := archive.Tar(f, m.Path); err != nil {
return fmt.Errorf("failed to make archive: %w", err) return false, fmt.Errorf("failed to make archive: %w", err)
} }
if err := s.repo.ResetLastScan(id); err != nil { if err := s.repo.ResetLastScan(id); err != nil {
return fmt.Errorf("failed to reset scan date: %w", err) return false, fmt.Errorf("failed to reset scan date: %w", err)
} }
m.Date = time.Now() m.Date = time.Now()
m.Version += 1 m.Version += 1
if err := s.repo.WriteMetadata(id, m); err != nil { if err := s.repo.WriteMetadata(id, m); err != nil {
return fmt.Errorf("failed to update metadata: %w", err) return false, fmt.Errorf("failed to update metadata: %w", err)
} }
return nil return true, nil
} }
func (s *Service) MakeBackup(gameID string) error { func (s *Service) MakeBackup(gameID string) error {
@@ -130,6 +135,9 @@ func (s *Service) MakeBackup(gameID string) error {
src, err := s.repo.ReadBlob(id) src, err := s.repo.ReadBlob(id)
if err != nil { if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil
}
return err return err
} }
if v, ok := src.(io.Closer); ok { if v, ok := src.(io.Closer); ok {
@@ -244,6 +252,12 @@ func (l Service) PullCurrent(id, path string, cli *client.Client) error {
return fmt.Errorf("failed to open blob from local repository: %w", err) return fmt.Errorf("failed to open blob from local repository: %w", err)
} }
if _, err := os.Stat(path); err == nil {
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("failed to clean the destination directory: %w", err)
}
}
if err := os.MkdirAll(path, 0740); err != nil { if err := os.MkdirAll(path, 0740); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err) return fmt.Errorf("failed to create destination directory: %w", err)
} }
@@ -370,6 +384,17 @@ func (l Service) ApplyBackup(gameID, backupID string) error {
return l.apply(filepath.Join(path, "data.tar.gz"), g.Path) return l.apply(filepath.Join(path, "data.tar.gz"), g.Path)
} }
func (l Service) Repository() repository.Repository {
return l.repo
}
func (l Service) ReloadCache(gameID string) error {
if er, ok := l.repo.(*repository.EagerRepository); ok {
return er.ReloadMetadata(repository.NewGameIdentifier(gameID))
}
return nil
}
func (l Service) apply(src, dst string) error { func (l Service) apply(src, dst string) error {
if err := os.RemoveAll(dst); err != nil { if err := os.RemoveAll(dst); err != nil {
return fmt.Errorf("failed to remove old save: %w", err) return fmt.Errorf("failed to remove old save: %w", err)

View File

@@ -49,7 +49,7 @@ func New(baseURL, username, password string) *Client {
} }
func (c *Client) Exists(gameID string) (bool, error) { func (c *Client) Exists(gameID string) (bool, error) {
u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "hash") u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "metadata")
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -104,22 +104,13 @@ func (c *Client) Version() (Information, error) {
return Information{}, errors.New("invalid payload sent by the server") return Information{}, errors.New("invalid payload sent by the server")
} }
// Deprecated: use c.Metadata instead
func (c *Client) Hash(gameID string) (string, error) { func (c *Client) Hash(gameID string) (string, error) {
u, err := url.JoinPath(c.baseURL, "api", "v1", "games", gameID, "hash") m, err := c.Metadata(gameID)
if err != nil { if err != nil {
return "", err return "", err
} }
return m.MD5, nil
o, err := c.get(u)
if err != nil {
return "", err
}
if h, ok := (o.Data).(string); ok {
return h, nil
}
return "", errors.New("invalid payload sent by the server")
} }
func (c *Client) Metadata(gameID string) (repository.Metadata, error) { func (c *Client) Metadata(gameID string) (repository.Metadata, error) {
@@ -139,6 +130,7 @@ func (c *Client) Metadata(gameID string) (repository.Metadata, error) {
Name: m["name"].(string), Name: m["name"].(string),
Version: int(m["version"].(float64)), Version: int(m["version"].(float64)),
Date: customtime.MustParse(time.RFC3339, m["date"].(string)), Date: customtime.MustParse(time.RFC3339, m["date"].(string)),
MD5: m["md5"].(string),
} }
return gm, nil return gm, nil
} }
@@ -175,6 +167,10 @@ func (c *Client) ListArchives(gameID string) ([]string, error) {
return nil, err return nil, err
} }
if o.Data == nil {
return nil, nil
}
if m, ok := (o.Data).([]any); ok { if m, ok := (o.Data).([]any); ok {
var res []string var res []string
for _, uuid := range m { for _, uuid := range m {
@@ -228,6 +224,11 @@ func (c *Client) Pull(gameID, 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 func() {
if err := os.Rename(archivePath+".part", archivePath); err != nil {
panic(err)
}
}()
defer f.Close() defer f.Close()
res, err := cli.Do(req) res, err := cli.Do(req)
@@ -250,8 +251,10 @@ func (c *Client) Pull(gameID, archivePath string) error {
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)
} }
if err := os.Rename(archivePath+".part", archivePath); err != nil { if err := os.Remove(archivePath); err != nil {
return fmt.Errorf("failed to move temporary data: %w", err) if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to remove the old version of the archive: %w", err)
}
} }
return nil return nil
@@ -346,6 +349,10 @@ func (c *Client) All() ([]repository.Metadata, error) {
return nil, err return nil, err
} }
if o.Data == nil {
return nil, nil
}
if games, ok := (o.Data).([]any); ok { if games, ok := (o.Data).([]any); ok {
var res []repository.Metadata var res []repository.Metadata
for _, g := range games { for _, g := range games {
@@ -355,6 +362,7 @@ func (c *Client) All() ([]repository.Metadata, error) {
Name: v["name"].(string), Name: v["name"].(string),
Version: int(v["version"].(float64)), Version: int(v["version"].(float64)),
Date: customtime.MustParse(time.RFC3339, v["date"].(string)), Date: customtime.MustParse(time.RFC3339, v["date"].(string)),
MD5: v["md5"].(string),
} }
res = append(res, gm) res = append(res, gm)
} }

View File

@@ -6,8 +6,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"time" "time"
) )
@@ -18,7 +20,7 @@ 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:"-"` MD5 string `json:"md5,omitempty"`
} }
Remote struct { Remote struct {
@@ -60,6 +62,7 @@ type (
EagerRepository struct { EagerRepository struct {
Repository Repository
mu sync.RWMutex
data map[string]Data data map[string]Data
} }
@@ -70,11 +73,11 @@ type (
AllHist(gameID GameIdentifier) ([]string, error) AllHist(gameID GameIdentifier) ([]string, error)
WriteBlob(ID Identifier) (io.Writer, error) WriteBlob(ID Identifier) (io.Writer, error)
WriteMetadata(gameID GameIdentifier, m Metadata) error WriteMetadata(gameID GameIdentifier, m Metadata) error
Metadata(gameID GameIdentifier) (Metadata, error) Metadata(gameID GameIdentifier) (Metadata, error)
LastScan(gameID GameIdentifier) (time.Time, error) LastScan(gameID GameIdentifier) (time.Time, error)
ReadBlob(gameID Identifier) (io.Reader, error) ReadBlob(gameID Identifier) (io.ReadSeekCloser, error)
Backup(id BackupIdentifier) (Backup, error) Backup(id BackupIdentifier) (Backup, error)
Remote(id GameIdentifier) (*Remote, error) Remote(id GameIdentifier) (*Remote, error)
@@ -132,10 +135,16 @@ func NewLazyRepository(dataRootPath string) (*LazyRepository, error) {
} }
func (l *LazyRepository) Mkdir(id Identifier) error { func (l *LazyRepository) Mkdir(id Identifier) error {
return os.MkdirAll(l.DataPath(id), 0740) path := l.DataPath(id)
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
slog.Debug("making directory", "path", path, "id", id, "perm", "0740")
return os.MkdirAll(path, 0740)
}
return nil
} }
func (l *LazyRepository) All() ([]string, error) { func (l *LazyRepository) All() ([]string, error) {
slog.Debug("loading all current data...")
dir, err := os.ReadDir(l.dataRoot) dir, err := os.ReadDir(l.dataRoot)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open directory: %w", err) return nil, fmt.Errorf("failed to open directory: %w", err)
@@ -152,6 +161,7 @@ func (l *LazyRepository) All() ([]string, error) {
func (l *LazyRepository) AllHist(id GameIdentifier) ([]string, error) { func (l *LazyRepository) AllHist(id GameIdentifier) ([]string, error) {
path := l.DataPath(id) path := l.DataPath(id)
slog.Debug("loading hist data...", "id", id)
dir, err := os.ReadDir(filepath.Join(path, "hist")) dir, err := os.ReadDir(filepath.Join(path, "hist"))
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
@@ -171,6 +181,7 @@ func (l *LazyRepository) AllHist(id GameIdentifier) ([]string, error) {
func (l *LazyRepository) WriteBlob(ID Identifier) (io.Writer, error) { func (l *LazyRepository) WriteBlob(ID Identifier) (io.Writer, error) {
path := l.DataPath(ID) path := l.DataPath(ID)
slog.Debug("loading write buffer...", "id", ID)
dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open destination file: %w", err) return nil, fmt.Errorf("failed to open destination file: %w", err)
@@ -180,8 +191,10 @@ func (l *LazyRepository) WriteBlob(ID Identifier) (io.Writer, error) {
} }
func (l *LazyRepository) WriteMetadata(id GameIdentifier, m Metadata) error { func (l *LazyRepository) WriteMetadata(id GameIdentifier, m Metadata) error {
m.MD5 = ""
path := l.DataPath(id) path := l.DataPath(id)
slog.Debug("writing metadata", "id", id, "metadata", m)
dst, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) dst, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil { if err != nil {
return fmt.Errorf("failed to open destination file: %w", err) return fmt.Errorf("failed to open destination file: %w", err)
@@ -199,6 +212,7 @@ func (l *LazyRepository) WriteMetadata(id GameIdentifier, m Metadata) error {
func (l *LazyRepository) Metadata(id GameIdentifier) (Metadata, error) { func (l *LazyRepository) Metadata(id GameIdentifier) (Metadata, error) {
path := l.DataPath(id) path := l.DataPath(id)
slog.Debug("loading metadata", "id", id)
src, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_RDONLY, 0) src, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_RDONLY, 0)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
@@ -213,6 +227,14 @@ func (l *LazyRepository) Metadata(id GameIdentifier) (Metadata, error) {
return Metadata{}, fmt.Errorf("corrupted datastore: failed to parse metadata: %w", err) 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)
}
slog.Debug("loading md5 hash", "id", id)
m.MD5, err = hash.FileMD5(filepath.Join(path, "data.tar.gz")) m.MD5, err = hash.FileMD5(filepath.Join(path, "data.tar.gz"))
if err != nil { if err != nil {
return Metadata{}, fmt.Errorf("failed to calculate md5: %w", err) return Metadata{}, fmt.Errorf("failed to calculate md5: %w", err)
@@ -224,6 +246,7 @@ func (l *LazyRepository) Metadata(id GameIdentifier) (Metadata, error) {
func (l *LazyRepository) Backup(id BackupIdentifier) (Backup, error) { func (l *LazyRepository) Backup(id BackupIdentifier) (Backup, error) {
path := l.DataPath(id) path := l.DataPath(id)
slog.Debug("loading hist metadata", "id", id)
fs, err := os.Stat(filepath.Join(path, "data.tar.gz")) fs, err := os.Stat(filepath.Join(path, "data.tar.gz"))
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
@@ -232,6 +255,7 @@ func (l *LazyRepository) Backup(id BackupIdentifier) (Backup, error) {
return Backup{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err) return Backup{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err)
} }
slog.Debug("loading md5 hash", "id", id)
h, err := hash.FileMD5(filepath.Join(path, "data.tar.gz")) h, err := hash.FileMD5(filepath.Join(path, "data.tar.gz"))
if err != nil { if err != nil {
return Backup{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err) return Backup{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err)
@@ -267,6 +291,7 @@ func (l *LazyRepository) LastScan(id GameIdentifier) (time.Time, error) {
func (l *LazyRepository) ResetLastScan(id GameIdentifier) error { func (l *LazyRepository) ResetLastScan(id GameIdentifier) error {
path := l.DataPath(id) path := l.DataPath(id)
slog.Debug("resetting last scan datetime for", "id", id)
f, err := os.OpenFile(filepath.Join(path, ".last_run"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) f, err := os.OpenFile(filepath.Join(path, ".last_run"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil { if err != nil {
return fmt.Errorf("failed to open file: %w", err) return fmt.Errorf("failed to open file: %w", err)
@@ -282,11 +307,15 @@ func (l *LazyRepository) ResetLastScan(id GameIdentifier) error {
return nil return nil
} }
func (l *LazyRepository) ReadBlob(id Identifier) (io.Reader, error) { func (l *LazyRepository) ReadBlob(id Identifier) (io.ReadSeekCloser, error) {
path := l.DataPath(id) path := l.DataPath(id)
slog.Debug("loading read buffer...", "id", id)
dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_RDONLY, 0) dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_RDONLY, 0)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("failed to open blob: %w", ErrNotFound)
}
return nil, fmt.Errorf("failed to open blob: %w", err) return nil, fmt.Errorf("failed to open blob: %w", err)
} }
@@ -337,6 +366,7 @@ func (l *LazyRepository) Remote(id GameIdentifier) (*Remote, error) {
func (l *LazyRepository) Remove(id GameIdentifier) error { func (l *LazyRepository) Remove(id GameIdentifier) error {
path := l.DataPath(id) path := l.DataPath(id)
slog.Debug("removing data", "id", id)
if err := os.RemoveAll(path); err != nil { if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("failed to remove game folder from the datastore: %w", err) return fmt.Errorf("failed to remove game folder from the datastore: %w", err)
} }
@@ -368,6 +398,9 @@ func NewEagerRepository(dataRootPath string) (*EagerRepository, error) {
} }
func (r *EagerRepository) Preload() error { func (r *EagerRepository) Preload() error {
r.mu.Lock()
defer r.mu.Unlock()
games, err := r.Repository.All() games, err := r.Repository.All()
if err != nil { if err != nil {
return fmt.Errorf("failed to load all data: %w", err) return fmt.Errorf("failed to load all data: %w", err)
@@ -411,6 +444,9 @@ func (r *EagerRepository) Preload() error {
} }
func (r *EagerRepository) All() ([]string, error) { func (r *EagerRepository) All() ([]string, error) {
r.mu.RLock()
defer r.mu.RUnlock()
var res []string var res []string
for _, g := range r.data { for _, g := range r.data {
res = append(res, g.Metadata.ID) res = append(res, g.Metadata.ID)
@@ -420,6 +456,9 @@ func (r *EagerRepository) All() ([]string, error) {
} }
func (r *EagerRepository) AllHist(id GameIdentifier) ([]string, error) { func (r *EagerRepository) AllHist(id GameIdentifier) ([]string, error) {
r.mu.RLock()
defer r.mu.RUnlock()
var res []string var res []string
if d, ok := r.data[id.gameID]; ok { if d, ok := r.data[id.gameID]; ok {
for _, b := range d.Backup { for _, b := range d.Backup {
@@ -430,6 +469,9 @@ func (r *EagerRepository) AllHist(id GameIdentifier) ([]string, error) {
} }
func (r *EagerRepository) WriteMetadata(id GameIdentifier, m Metadata) error { func (r *EagerRepository) WriteMetadata(id GameIdentifier, m Metadata) error {
r.mu.Lock()
defer r.mu.Unlock()
err := r.Repository.WriteMetadata(id, m) err := r.Repository.WriteMetadata(id, m)
if err != nil { if err != nil {
return err return err
@@ -443,6 +485,9 @@ func (r *EagerRepository) WriteMetadata(id GameIdentifier, m Metadata) error {
} }
func (r *EagerRepository) Metadata(id GameIdentifier) (Metadata, error) { func (r *EagerRepository) Metadata(id GameIdentifier) (Metadata, error) {
r.mu.RLock()
defer r.mu.RUnlock()
if d, ok := r.data[id.gameID]; ok { if d, ok := r.data[id.gameID]; ok {
return d.Metadata, nil return d.Metadata, nil
} }
@@ -450,6 +495,9 @@ func (r *EagerRepository) Metadata(id GameIdentifier) (Metadata, error) {
} }
func (r *EagerRepository) Backup(id BackupIdentifier) (Backup, error) { func (r *EagerRepository) Backup(id BackupIdentifier) (Backup, error) {
r.mu.RLock()
defer r.mu.RUnlock()
if d, ok := r.data[id.gameID]; ok { if d, ok := r.data[id.gameID]; ok {
if b, ok := d.Backup[id.backupID]; ok { if b, ok := d.Backup[id.backupID]; ok {
return b, nil return b, nil
@@ -459,6 +507,9 @@ func (r *EagerRepository) Backup(id BackupIdentifier) (Backup, error) {
} }
func (r *EagerRepository) SetRemote(id GameIdentifier, url string) error { func (r *EagerRepository) SetRemote(id GameIdentifier, url string) error {
r.mu.Lock()
defer r.mu.Unlock()
err := r.Repository.SetRemote(id, url) err := r.Repository.SetRemote(id, url)
if err != nil { if err != nil {
return err return err
@@ -475,6 +526,9 @@ func (r *EagerRepository) SetRemote(id GameIdentifier, url string) error {
} }
func (r *EagerRepository) Remove(id GameIdentifier) error { func (r *EagerRepository) Remove(id GameIdentifier) error {
r.mu.Lock()
defer r.mu.Unlock()
if err := r.Repository.Remove(id); err != nil { if err := r.Repository.Remove(id); err != nil {
return err return err
} }
@@ -482,3 +536,39 @@ func (r *EagerRepository) Remove(id GameIdentifier) error {
delete(r.data, id.gameID) delete(r.data, id.gameID)
return nil return nil
} }
func (r *EagerRepository) ReloadMetadata(id GameIdentifier) error {
backup, err := r.Repository.AllHist(id)
if err != nil {
return fmt.Errorf("[%s] failed to load hist data: %w", id, err)
}
remote, err := r.Repository.Remote(id)
if err != nil {
return fmt.Errorf("[%s] failed to load remote metadata: %w", id, err)
}
m, err := r.Repository.Metadata(id)
if err != nil {
return fmt.Errorf("[%s] failed to load metadata: %w", id, err)
}
backups := make(map[string]Backup)
for _, b := range backup {
info, err := r.Repository.Backup(NewBackupIdentifier(id.gameID, b))
if err != nil {
return fmt.Errorf("[%s] failed to get backup information: %w", id, err)
}
backups[b] = info
}
r.data[id.gameID] = Data{
Metadata: m,
Remote: remote,
DataPath: r.DataPath(id),
Backup: backups,
}
return nil
}