22 Commits

Author SHA1 Message Date
62911f2405 fix access right
Some checks failed
CloudSave/pipeline/head There was a failure building this commit
CloudSave/pipeline/pr-main There was a failure building this commit
2025-09-07 19:02:26 +02:00
3db9974aa8 hook test
All checks were successful
CloudSave/pipeline/head This commit looks good
CloudSave/pipeline/pr-main This commit looks good
2025-09-07 12:47:59 +02:00
8b4c599657 better compile script 2025-09-07 12:43:19 +02:00
5ba8642904 fix timeing
All checks were successful
CloudSave/pipeline/head This commit looks good
CloudSave/pipeline/pr-main This commit looks good
2025-09-07 01:50:41 +02:00
ddcfe2a698 fix build script 2025-09-07 01:41:44 +02:00
1cf8b986fa version
All checks were successful
CloudSave/pipeline/head This commit looks good
2025-09-07 01:33:18 +02:00
46f312078d fix sec
Some checks failed
CloudSave/pipeline/head Something is wrong with the build of this commit
2025-09-07 01:31:14 +02:00
af11e843a4 fixing sec issues
Some checks failed
CloudSave/pipeline/head There was a failure building this commit
2025-09-07 01:14:19 +02:00
b3232e79d5 add gosec
Some checks failed
CloudSave/pipeline/head There was a failure building this commit
2025-09-07 00:31:37 +02:00
0a33d1b68d wip 0.0.5
All checks were successful
CloudSave/pipeline/head This commit looks good
2025-09-02 22:32:07 +02:00
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
35 changed files with 935 additions and 352 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

@@ -35,7 +35,7 @@ fi
## SERVER ## SERVER
platforms=("linux/amd64" "linux/arm64" "linux/riscv64" "linux/ppc64le") platforms=("linux/amd64" "linux/arm64" "linux/riscv64" "linux/ppc64le" "windows/amd64")
for platform in "${platforms[@]}"; do for platform in "${platforms[@]}"; do
echo "* Compiling server for $platform..." echo "* Compiling server for $platform..."
@@ -47,11 +47,25 @@ 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
if [ $? -ne 0 ]; then
exit 1
fi
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
if [ $? -ne 0 ]; then
exit 1
fi
rm build/cloudsave_server$EXT rm build/cloudsave_server$EXT
if [ $? -ne 0 ]; then
exit 1
fi
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
if [ $? -ne 0 ]; then
exit 1
fi
fi fi
done done
@@ -70,10 +84,24 @@ for platform in "${platforms[@]}"; do
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_web$EXT -a ./cmd/web CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} go build -o build/cloudsave_web$EXT -a ./cmd/web
if [ $? -ne 0 ]; then
exit 1
fi
tar -czf build/web_${platform_split[0]}_${platform_split[1]}.tar.gz build/cloudsave_web$EXT tar -czf build/web_${platform_split[0]}_${platform_split[1]}.tar.gz build/cloudsave_web$EXT
if [ $? -ne 0 ]; then
exit 1
fi
rm build/cloudsave_web$EXT rm build/cloudsave_web$EXT
if [ $? -ne 0 ]; then
exit 1
fi
else else
CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} go build -o build/cloudsave_web_${platform_split[0]}_${platform_split[1]}$EXT -a ./cmd/web CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} go build -o build/cloudsave_web_${platform_split[0]}_${platform_split[1]}$EXT -a ./cmd/web
if [ $? -ne 0 ]; then
exit 1
fi
fi fi
done done
@@ -92,9 +120,23 @@ for platform in "${platforms[@]}"; do
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$EXT -a ./cmd/cli CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} go build -o build/cloudsave$EXT -a ./cmd/cli
if [ $? -ne 0 ]; then
exit 1
fi
tar -czf build/cli_${platform_split[0]}_${platform_split[1]}.tar.gz build/cloudsave$EXT tar -czf build/cli_${platform_split[0]}_${platform_split[1]}.tar.gz build/cloudsave$EXT
if [ $? -ne 0 ]; then
exit 1
fi
rm build/cloudsave$EXT rm build/cloudsave$EXT
if [ $? -ne 0 ]; then
exit 1
fi
else else
CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} go build -o build/cloudsave_${platform_split[0]}_${platform_split[1]}$EXT -a ./cmd/cli CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} go build -o build/cloudsave_${platform_split[0]}_${platform_split[1]}$EXT -a ./cmd/cli
if [ $? -ne 0 ]; then
exit 1
fi
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

@@ -43,7 +43,7 @@ func (p *ListCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
return subcommands.ExitUsageError return subcommands.ExitUsageError
} }
username, password, err := credentials.Read() username, password, err := credentials.Read(f.Arg(0))
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error: 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
@@ -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

@@ -0,0 +1,60 @@
package login
import (
"cloudsave/cmd/cli/tools/prompt/credentials"
"cloudsave/pkg/remote/client"
"context"
"flag"
"fmt"
"os"
"github.com/google/subcommands"
)
type (
LoginCmd struct {
}
)
func (*LoginCmd) Name() string { return "login" }
func (*LoginCmd) Synopsis() string { return "save IN PLAIN TEXT your credentials" }
func (*LoginCmd) Usage() string {
return `Usage: cloudsave login <SERVER_HOSTNAME>
Warning: this command saves the login into a plain text json file
Options:
`
}
func (p *LoginCmd) SetFlags(f *flag.FlagSet) {
}
func (p *LoginCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if f.NArg() != 1 {
fmt.Fprintf(os.Stderr, "error: this command take 1 argument")
return subcommands.ExitUsageError
}
server := f.Arg(0)
username, password, err := credentials.Read(server)
if err != nil {
fmt.Fprintf(os.Stderr, "error: failed to read std output: %s", err)
return subcommands.ExitFailure
}
cli := client.New(server, username, password)
if _, err := cli.Version(); err != nil {
fmt.Fprintf(os.Stderr, "error: failed to login: %s", err)
return subcommands.ExitFailure
}
if err := credentials.Login(username, password, server); err != nil {
fmt.Fprintf(os.Stderr, "error: failed to save login: %s", err)
return subcommands.ExitFailure
}
fmt.Println("login information saved!")
return subcommands.ExitSuccess
}

View File

@@ -0,0 +1,43 @@
package logout
import (
"cloudsave/cmd/cli/tools/prompt/credentials"
"context"
"flag"
"fmt"
"os"
"github.com/google/subcommands"
)
type (
LogoutCmd struct {
}
)
func (*LogoutCmd) Name() string { return "logout" }
func (*LogoutCmd) Synopsis() string { return "logout from a server" }
func (*LogoutCmd) Usage() string {
return `Usage: cloudsave logout <SERVER_HOSTNAME>
Options:
`
}
func (p *LogoutCmd) SetFlags(f *flag.FlagSet) {
}
func (p *LogoutCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if f.NArg() != 1 {
fmt.Fprintf(os.Stderr, "error: this command take 1 argument")
return subcommands.ExitUsageError
}
if err := credentials.Logout(f.Arg(0)); err != nil {
fmt.Fprintf(os.Stderr, "error: failed to logout: %s", err)
return subcommands.ExitFailure
}
fmt.Println("bye!")
return subcommands.ExitSuccess
}

View File

@@ -41,7 +41,7 @@ func (p *PullCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
gameID := f.Arg(1) gameID := f.Arg(1)
path := f.Arg(2) path := f.Arg(2)
username, password, err := credentials.Read() username, password, err := credentials.Read(url)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error: 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

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)
@@ -63,10 +63,15 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
pg := progressbar.New(-1) pg := progressbar.New(-1)
destroyPg := func() { destroyPg := func() {
pg.Finish() if err := pg.Finish(); err != nil {
pg.Clear() slog.Error("failed to finish progressbar", "err", err)
pg.Close() }
if err := pg.Clear(); err != nil {
slog.Error("failed to clear progressbar", "err", err)
}
if err := pg.Close(); err != nil {
slog.Error("failed to close progressbar", "err", err)
}
} }
pg.Describe(fmt.Sprintf("[%s] Checking status...", g.Name)) pg.Describe(fmt.Sprintf("[%s] Checking status...", g.Name))
@@ -88,19 +93,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 +117,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 +126,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 +138,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 +158,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 +185,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 +204,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 +272,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,7 +287,10 @@ func connect(remoteCred map[string]map[string]string, r remote.Remote) (*client.
return cli, nil return cli, nil
} }
username, password, err := credentials.Read() fmt.Println()
fmt.Println("Connexion to", r.URL)
fmt.Println("============")
username, password, err := credentials.Read(r.URL)
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

@@ -42,7 +42,7 @@ func (p *VersionCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{
return subcommands.ExitUsageError return subcommands.ExitUsageError
} }
username, password, err := credentials.Read() username, password, err := credentials.Read(f.Arg(0))
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to read std output: %s", err) fmt.Fprintf(os.Stderr, "failed to read std output: %s", err)
return subcommands.ExitFailure return subcommands.ExitFailure

View File

@@ -4,6 +4,8 @@ import (
"cloudsave/cmd/cli/commands/add" "cloudsave/cmd/cli/commands/add"
"cloudsave/cmd/cli/commands/apply" "cloudsave/cmd/cli/commands/apply"
"cloudsave/cmd/cli/commands/list" "cloudsave/cmd/cli/commands/list"
"cloudsave/cmd/cli/commands/login"
"cloudsave/cmd/cli/commands/logout"
"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"
@@ -56,6 +58,8 @@ func main() {
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")
subcommands.Register(&pull.PullCmd{Service: s}, "remote") subcommands.Register(&pull.PullCmd{Service: s}, "remote")
subcommands.Register(&login.LoginCmd{}, "remote")
subcommands.Register(&logout.LogoutCmd{}, "remote")
flag.Parse() flag.Parse()
ctx := context.Background() ctx := context.Background()

View File

@@ -2,14 +2,48 @@ package credentials
import ( import (
"bufio" "bufio"
"encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"golang.org/x/term" "golang.org/x/term"
) )
func Read() (string, string, error) { type (
credential struct {
Username string `json:"username"`
Password string `json:"password"`
}
credentialsStore struct {
Store map[string]credential `json:"store"`
}
)
var (
datastorePath string
)
func init() {
roaming, err := os.UserConfigDir()
if err != nil {
panic("failed to get user config path: " + err.Error())
}
datastorePath = filepath.Join(roaming, "cloudsave")
}
func Read(server string) (string, string, error) {
var err error
store, err := load()
if err == nil {
if c, ok := store[server]; ok {
return c.Username, c.Password, nil
}
}
fmt.Print("Enter username: ") fmt.Print("Enter username: ")
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
username, _ := reader.ReadString('\n') username, _ := reader.ReadString('\n')
@@ -24,3 +58,66 @@ func Read() (string, string, error) {
return username, string(password), nil return username, string(password), nil
} }
func Login(username, password, server string) error {
store, err := load()
if err != nil {
return err
}
store[server] = credential{
Username: username,
Password: password,
}
return save(store)
}
func Logout(server string) error {
store, err := load()
if err != nil {
return err
}
delete(store, server)
return save(store)
}
func save(store map[string]credential) error {
c := credentialsStore{
Store: store,
}
f, err := os.OpenFile(filepath.Clean(filepath.Join(datastorePath, "credential.json")), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return fmt.Errorf("failed to open datastore: %w", err)
}
defer f.Close()
e := json.NewEncoder(f)
if err := e.Encode(c); err != nil {
return fmt.Errorf("failed to encode data: %w", err)
}
return nil
}
func load() (map[string]credential, error) {
f, err := os.OpenFile(filepath.Clean(filepath.Join(datastorePath, "credential.json")), os.O_RDONLY, 0)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return make(map[string]credential), nil
}
return nil, fmt.Errorf("failed to open datastore: %w", err)
}
defer f.Close()
var c credentialsStore
d := json.NewDecoder(f)
if err := d.Decode(&c); err != nil {
return nil, fmt.Errorf("failed to decode data: %w", err)
}
return c.Store, nil
}

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"
@@ -22,6 +21,7 @@ type (
Server *http.Server Server *http.Server
Service *data.Service Service *data.Service
documentRoot string documentRoot string
creds map[string]string
} }
) )
@@ -33,10 +33,11 @@ func NewServer(documentRoot string, srv *data.Service, creds map[string]string,
s := &HTTPServer{ s := &HTTPServer{
Service: srv, Service: srv,
documentRoot: documentRoot, documentRoot: documentRoot,
creds: creds,
} }
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)
@@ -47,7 +48,7 @@ func NewServer(documentRoot string, srv *data.Service, creds map[string]string,
router.Use(middleware.Compress(5, "application/gzip")) router.Use(middleware.Compress(5, "application/gzip"))
router.Use(middleware.Heartbeat("/heartbeat")) router.Use(middleware.Heartbeat("/heartbeat"))
router.Route("/api", func(routerAPI chi.Router) { router.Route("/api", func(routerAPI chi.Router) {
routerAPI.Use(BasicAuth("cloudsave", creds)) routerAPI.Use(s.BasicAuth("cloudsave"))
routerAPI.Route("/v1", func(r chi.Router) { routerAPI.Route("/v1", func(r chi.Router) {
// Get information about the server // Get information about the server
r.Get("/version", s.Information) r.Get("/version", s.Information)
@@ -61,7 +62,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)
@@ -74,50 +74,25 @@ func NewServer(documentRoot string, srv *data.Service, creds map[string]string,
}) })
}) })
s.Server = &http.Server{ s.Server = &http.Server{
Addr: fmt.Sprintf(":%d", port), Addr: fmt.Sprintf(":%d", port),
Handler: router, Handler: router,
ReadHeaderTimeout: 2 * time.Second,
} }
return s return s
} }
func (s *HTTPServer) SetCredentials(creds map[string]string) {
s.creds = creds
}
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 +100,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 +171,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 +228,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 +248,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 +290,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

@@ -20,7 +20,7 @@ func recoverMiddleware(next http.Handler) http.Handler {
} }
// BasicAuth implements a simple middleware handler for adding basic http auth to a route. // BasicAuth implements a simple middleware handler for adding basic http auth to a route.
func BasicAuth(realm string, creds map[string]string) func(next http.Handler) http.Handler { func (s *HTTPServer) BasicAuth(realm string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth() user, pass, ok := r.BasicAuth()
@@ -29,7 +29,7 @@ func BasicAuth(realm string, creds map[string]string) func(next http.Handler) ht
return return
} }
credPass := creds[user] credPass := s.creds[user]
if err := bcrypt.CompareHashAndPassword([]byte(credPass), []byte(pass)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(credPass), []byte(pass)); err != nil {
basicAuthFailed(w, r, realm) basicAuthFailed(w, r, realm)
return return

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

@@ -5,12 +5,30 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"os/signal"
"syscall"
) )
const defaultDocumentRoot string = "/var/lib/cloudsave" const defaultDocumentRoot string = "/var/lib/cloudsave"
var (
updateChan chan struct{}
)
func main() { func main() {
run() updateChan = make(chan struct{})
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, syscall.SIGHUP)
go func() {
for {
<-sigc
updateChan <- struct{}{}
}
}()
run(updateChan)
} }
func fatal(message string, exitCode int) { func fatal(message string, exitCode int) {

View File

@@ -2,16 +2,54 @@ 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
var (
updateChan chan struct{}
)
func main() { func main() {
run() updateChan = make(chan struct{})
go systray.Run(onReady, onExit)
run(updateChan)
} }
func fatal(message string, exitCode int) { 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)
mReload := systray.AddMenuItem("Reload", "Reload the server data")
mQuit := systray.AddMenuItem("Quit", "Quit the server")
go func() {
<-mQuit.ClickedCh
os.Exit(0)
}()
go func() {
for {
<-mReload.ClickedCh
updateChan <- struct{}{}
}
}()
}
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,28 +8,38 @@ import (
"cloudsave/pkg/repository" "cloudsave/pkg/repository"
"flag" "flag"
"fmt" "fmt"
"log/slog"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
) )
func run() { func run(updateChan <-chan struct{}) {
fmt.Printf("CloudSave server -- v%s.%s.%s\n\n", constants.Version, runtime.GOOS, runtime.GOARCH) fmt.Printf("CloudSave server -- v%s.%s.%s\n\n", constants.Version, runtime.GOOS, runtime.GOARCH)
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)
@@ -37,19 +47,39 @@ func run() {
if err := r.Preload(); err != nil { if err := r.Preload(); err != nil {
fatal("failed to load datastore: "+err.Error(), 1) fatal("failed to load datastore: "+err.Error(), 1)
} }
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)) go func() {
for {
<-updateChan
if r, ok := repo.(*repository.EagerRepository); ok {
if err := r.Reload(); err != nil {
fatal("failed to reload data: "+err.Error(), 1)
}
}
h, err := htpasswd.Open(filepath.Join(documentRoot, ".htpasswd"))
if err != nil {
fatal("failed to load .htpasswd: "+err.Error(), 1)
}
slog.Info("users loaded: " + strconv.Itoa(len(h.Content())) + " user(s) loaded")
server.SetCredentials(h.Content())
}
}()
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

@@ -2,6 +2,7 @@ package htpasswd
import ( import (
"os" "os"
"path/filepath"
"strings" "strings"
) )
@@ -12,7 +13,7 @@ type (
) )
func Open(path string) (File, error) { func Open(path string) (File, error) {
c, err := os.ReadFile(path) c, err := os.ReadFile(filepath.Clean(path))
if err != nil { if err != nil {
return File{}, err return File{}, err
} }
@@ -26,7 +27,7 @@ func Open(path string) (File, error) {
if len(kv) != 2 { if len(kv) != 2 {
continue continue
} }
f.data[kv[0]] = kv[1] f.data[kv[0]] = kv[1]
} }
return f, nil return f, nil

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath"
) )
type ( type (
@@ -22,7 +23,7 @@ type (
) )
func Load(path string) (Configuration, error) { func Load(path string) (Configuration, error) {
f, err := os.OpenFile(path, os.O_RDONLY, 0) f, err := os.OpenFile(filepath.Clean(path), os.O_RDONLY, 0)
if err != nil { if err != nil {
return Configuration{}, fmt.Errorf("failed to open configuration file: %w", err) return Configuration{}, fmt.Errorf("failed to open configuration file: %w", err)
} }

View File

@@ -13,6 +13,7 @@ import (
"runtime" "runtime"
"slices" "slices"
"sync" "sync"
"time"
"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"
@@ -74,13 +75,19 @@ var (
// NewServer start the http server // NewServer start the http server
func NewServer(c config.Configuration) *HTTPServer { func NewServer(c config.Configuration) *HTTPServer {
dashboardTemplate := template.New("dashboard") dashboardTemplate := template.New("dashboard")
dashboardTemplate.Parse(DashboardHTMLPage) if _, err := dashboardTemplate.Parse(DashboardHTMLPage); err != nil {
panic("failed to load template 'dashboard': " + err.Error())
}
detailledTemplate := template.New("detailled") detailledTemplate := template.New("detailled")
detailledTemplate.Parse(DetailledHTMLPage) if _, err := detailledTemplate.Parse(DetailledHTMLPage); err != nil {
panic("failed to load template 'detailled': " + err.Error())
}
systemTemplate := template.New("system") systemTemplate := template.New("system")
systemTemplate.Parse(SystemHTMLPage) if _, err := systemTemplate.Parse(SystemHTMLPage); err != nil {
panic("failed to load template 'system': " + err.Error())
}
s := &HTTPServer{ s := &HTTPServer{
Config: c, Config: c,
@@ -99,8 +106,9 @@ func NewServer(c config.Configuration) *HTTPServer {
routerAPI.Get("/system", s.system) routerAPI.Get("/system", s.system)
}) })
s.Server = &http.Server{ s.Server = &http.Server{
Addr: fmt.Sprintf(":%d", c.Server.Port), Addr: fmt.Sprintf(":%d", c.Server.Port),
Handler: router, Handler: router,
ReadHeaderTimeout: 2 * time.Second,
} }
return s return s
} }
@@ -162,9 +170,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 +182,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 +206,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 +217,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 />

41
go.mod
View File

@@ -3,6 +3,8 @@ module cloudsave
go 1.24 go 1.24
require ( require (
fyne.io/fyne/v2 v2.6.3
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 +14,46 @@ require (
) )
require ( require (
fyne.io/systray v1.11.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.1.0 // indirect
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-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rymdport/portal v0.4.1 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

101
go.sum
View File

@@ -1,30 +1,127 @@
fyne.io/fyne/v2 v2.6.3 h1:cvtM2KHeRuH+WhtHiA63z5wJVBkQ9+Ay0UMl9PxFHyA=
fyne.io/fyne/v2 v2.6.3/go.mod h1:NGSurpRElVoI1G3h+ab2df3O5KLGh1CGbsMMcX0bPIs=
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
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-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
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/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
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/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
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/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
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/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
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/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
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=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=

22
jenkinsfile Normal file
View File

@@ -0,0 +1,22 @@
pipeline {
agent any
stages {
stage('Audit') {
steps {
sh '''
go install github.com/securego/gosec/v2/cmd/gosec@v2.22.8
go install honnef.co/go/tools/cmd/staticcheck@v0.6.1
/var/lib/jenkins/go/bin/staticcheck ./...
/var/lib/jenkins/go/bin/gosec -exclude="G401,G501,G103" ./...
'''
}
}
stage('Build') {
steps {
sh './build.sh'
}
}
}
}

View File

@@ -1,5 +1,5 @@
package constants package constants
const Version = "0.0.4" const Version = "0.0.4e"
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,12 +384,23 @@ 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)
} }
f, err := os.OpenFile(src, os.O_RDONLY, 0) f, err := os.OpenFile(filepath.Clean(src), os.O_RDONLY, 0)
if err != nil { if err != nil {
return fmt.Errorf("failed to open archive: %w", err) return fmt.Errorf("failed to open archive: %w", err)
} }

View File

@@ -9,10 +9,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strconv" "strconv"
"time" "time"
@@ -49,7 +51,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 +106,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 +132,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 +169,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 {
@@ -224,10 +222,15 @@ func (c *Client) Pull(gameID, archivePath string) error {
req.SetBasicAuth(c.username, c.password) req.SetBasicAuth(c.username, c.password)
f, err := os.OpenFile(archivePath+".part", os.O_CREATE|os.O_WRONLY, 0740) f, err := os.OpenFile(filepath.Clean(archivePath+".part"), os.O_CREATE|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)
} }
defer func() {
if err := os.Rename(filepath.Clean(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 +253,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
@@ -272,20 +277,24 @@ func (c *Client) PullBackup(gameID, uuid, archivePath string) error {
req.SetBasicAuth(c.username, c.password) req.SetBasicAuth(c.username, c.password)
f, err := os.OpenFile(archivePath+".part", os.O_CREATE|os.O_WRONLY, 0740) f, err := os.OpenFile(filepath.Clean(archivePath+".part"), os.O_CREATE|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)
} }
res, err := cli.Do(req) res, err := cli.Do(req)
if err != nil { if err != nil {
f.Close() if err := f.Close(); err != nil {
slog.Error("failed to close file", "err", err)
}
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() if err := f.Close(); err != nil {
slog.Error("failed to close file", "err", err)
}
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)
} }
@@ -296,10 +305,14 @@ 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() if err := f.Close(); err != nil {
slog.Error("failed to close file", "err", err)
}
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 := f.Close(); err != nil {
slog.Error("failed to close file", "err", err)
}
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)
@@ -346,6 +359,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 +372,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)
} }
@@ -405,7 +423,7 @@ func (c *Client) get(url string) (obj.HTTPObject, error) {
} }
func (c *Client) push(u, archivePath string, m repository.Metadata) error { func (c *Client) push(u, archivePath string, m repository.Metadata) error {
f, err := os.OpenFile(archivePath, os.O_RDONLY, 0) f, err := os.OpenFile(filepath.Clean(archivePath), os.O_RDONLY, 0)
if err != nil { if err != nil {
return fmt.Errorf("failed to open file: %w", err) return fmt.Errorf("failed to open file: %w", err)
} }
@@ -423,9 +441,15 @@ func (c *Client) push(u, archivePath string, m repository.Metadata) error {
return fmt.Errorf("failed to copy data: %w", err) return fmt.Errorf("failed to copy data: %w", err)
} }
writer.WriteField("name", m.Name) if err := writer.WriteField("name", m.Name); err != nil {
writer.WriteField("version", strconv.Itoa(m.Version)) return err
writer.WriteField("date", m.Date.Format(time.RFC3339)) }
if err := writer.WriteField("version", strconv.Itoa(m.Version)); err != nil {
return err
}
if err := writer.WriteField("date", m.Date.Format(time.RFC3339)); err != nil {
return err
}
if err := writer.Close(); err != nil { if err := writer.Close(); err != nil {
return err return err

View File

@@ -39,7 +39,7 @@ func init() {
} }
func One(gameID string) (Remote, error) { func One(gameID string) (Remote, error) {
content, err := os.ReadFile(filepath.Join(datastorepath, gameID, "remote.json")) content, err := os.ReadFile(filepath.Clean(filepath.Join(datastorepath, gameID, "remote.json")))
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return Remote{}, ErrNoRemote return Remote{}, ErrNoRemote
@@ -62,7 +62,7 @@ func Set(gameID, url string) error {
URL: url, URL: url,
} }
f, err := os.OpenFile(filepath.Join(datastorepath, gameID, "remote.json"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0740) f, err := os.OpenFile(filepath.Join(filepath.Join(datastorepath, gameID, "remote.json")), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0740)
if err != nil { if err != nil {
return err return err
} }

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
} }
@@ -74,7 +77,7 @@ type (
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)
@@ -114,7 +117,7 @@ func (bi BackupIdentifier) Key() string {
func NewLazyRepository(dataRootPath string) (*LazyRepository, error) { func NewLazyRepository(dataRootPath string) (*LazyRepository, error) {
if m, err := os.Stat(dataRootPath); err != nil { if m, err := os.Stat(dataRootPath); err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
if err := os.MkdirAll(dataRootPath, 0740); err != nil { if err := os.MkdirAll(dataRootPath, 0750); err != nil {
return nil, fmt.Errorf("failed to make the directory: %w", err) return nil, fmt.Errorf("failed to make the directory: %w", err)
} }
} else { } else {
@@ -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", "0750")
return os.MkdirAll(path, 0750)
}
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,7 +181,8 @@ 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)
dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) slog.Debug("loading write buffer...", "id", ID)
dst, err := os.OpenFile(filepath.Clean(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,9 +191,11 @@ 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)
dst, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) slog.Debug("writing metadata", "id", id, "metadata", m)
dst, err := os.OpenFile(filepath.Clean(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,7 +212,8 @@ 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)
src, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_RDONLY, 0) slog.Debug("loading metadata", "id", id)
src, err := os.OpenFile(filepath.Clean(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) {
return Metadata{}, ErrNotFound return Metadata{}, ErrNotFound
@@ -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)
@@ -248,7 +272,7 @@ func (l *LazyRepository) Backup(id BackupIdentifier) (Backup, error) {
func (l *LazyRepository) LastScan(id GameIdentifier) (time.Time, error) { func (l *LazyRepository) LastScan(id GameIdentifier) (time.Time, error) {
path := l.DataPath(id) path := l.DataPath(id)
data, err := os.ReadFile(filepath.Join(path, ".last_run")) data, err := os.ReadFile(filepath.Clean(filepath.Join(path, ".last_run")))
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return time.Time{}, nil return time.Time{}, nil
@@ -267,7 +291,8 @@ 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)
f, err := os.OpenFile(filepath.Join(path, ".last_run"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) slog.Debug("resetting last scan datetime for", "id", id)
f, err := os.OpenFile(filepath.Clean(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)
dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_RDONLY, 0) slog.Debug("loading read buffer...", "id", id)
dst, err := os.OpenFile(filepath.Clean(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)
} }
@@ -296,7 +325,7 @@ func (l *LazyRepository) ReadBlob(id Identifier) (io.Reader, error) {
func (l *LazyRepository) SetRemote(id GameIdentifier, url string) error { func (l *LazyRepository) SetRemote(id GameIdentifier, url string) error {
path := l.DataPath(id) path := l.DataPath(id)
src, err := os.OpenFile(filepath.Join(path, "remote.json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) src, err := os.OpenFile(filepath.Clean(filepath.Join(path, "remote.json")), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil { if err != nil {
return fmt.Errorf("failed to open remote description: %w", err) return fmt.Errorf("failed to open remote description: %w", err)
} }
@@ -316,7 +345,7 @@ func (l *LazyRepository) SetRemote(id GameIdentifier, url string) error {
func (l *LazyRepository) Remote(id GameIdentifier) (*Remote, error) { func (l *LazyRepository) Remote(id GameIdentifier) (*Remote, error) {
path := l.DataPath(id) path := l.DataPath(id)
src, err := os.OpenFile(filepath.Join(path, "remote.json"), os.O_RDONLY, 0) src, err := os.OpenFile(filepath.Clean(filepath.Join(path, "remote.json")), os.O_RDONLY, 0)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return nil, nil return nil, nil
@@ -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,10 @@ func NewEagerRepository(dataRootPath string) (*EagerRepository, error) {
} }
func (r *EagerRepository) Preload() error { func (r *EagerRepository) Preload() error {
r.mu.Lock()
defer r.mu.Unlock()
slog.Info("loading data from datastore to memory...")
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)
@@ -410,7 +444,25 @@ func (r *EagerRepository) Preload() error {
return nil return nil
} }
func (r *EagerRepository) ClearCache() {
r.mu.Lock()
defer r.mu.Unlock()
slog.Info("clearing cache...")
for k := range r.data {
delete(r.data, k)
}
}
func (r *EagerRepository) Reload() error {
r.ClearCache()
return r.Preload()
}
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 +472,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 +485,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 +501,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 +511,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 +523,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 +542,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 +552,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
}

View File

@@ -5,10 +5,17 @@ import (
"compress/gzip" "compress/gzip"
"fmt" "fmt"
"io" "io"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
) )
const (
// Tune these to your apps needs
maxCompressedUpload = 500 << 20 // 500 MiB compressed
maxUncompressedOutput = 1000 << 20 // 100 MiB after inflate
)
func Untar(file io.Reader, path string) error { func Untar(file io.Reader, path string) error {
gzr, err := gzip.NewReader(file) gzr, err := gzip.NewReader(file)
if err != nil { if err != nil {
@@ -37,7 +44,7 @@ func Untar(file io.Reader, path string) error {
} }
// the target location where the dir/file should be created // the target location where the dir/file should be created
target := filepath.Join(path, header.Name) target := filepath.Clean(filepath.Join(path, filepath.Clean(header.Name)))
// the following switch could also be done using fi.Mode(), not sure if there // the following switch could also be done using fi.Mode(), not sure if there
// a benefit of using one vs. the other. // a benefit of using one vs. the other.
@@ -49,26 +56,35 @@ func Untar(file io.Reader, path string) error {
// if its a dir and it doesn't exist create it // if its a dir and it doesn't exist create it
case tar.TypeDir: case tar.TypeDir:
if _, err := os.Stat(target); err != nil { if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil { if err := os.MkdirAll(target, 0740); err != nil {
return err return err
} }
} }
// if it's a file create it // if it's a file create it
case tar.TypeReg: case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, header.FileInfo().Mode())
if err != nil { if err != nil {
return err return err
} }
limited := &io.LimitedReader{R: gzr, N: maxUncompressedOutput}
// copy over contents // copy over contents
if _, err := io.Copy(f, tr); err != nil { if _, err := io.Copy(f, limited); err != nil {
return err return err
} }
// manually close here after each file operation; defering would cause each file close // manually close here after each file operation; defering would cause each file close
// to wait until all operations have completed. // to wait until all operations have completed.
f.Close() if err := f.Close(); err != nil {
slog.Error("failed to close file", "err", err)
}
if limited.N == 0 {
// Limit exhausted → likely bomb
return fmt.Errorf("payload too large after decompression")
}
} }
} }
} }
@@ -106,7 +122,7 @@ func Tar(file io.Writer, root string) error {
return nil return nil
} }
file, err := os.Open(path) file, err := os.Open(filepath.Clean(path))
if err != nil { if err != nil {
return fmt.Errorf("failed to open file: %w", err) return fmt.Errorf("failed to open file: %w", err)
} }

View File

@@ -5,10 +5,11 @@ import (
"encoding/hex" "encoding/hex"
"io" "io"
"os" "os"
"path/filepath"
) )
func FileMD5(fp string) (string, error) { func FileMD5(fp string) (string, error) {
f, err := os.OpenFile(fp, os.O_RDONLY, 0) f, err := os.OpenFile(filepath.Clean(fp), os.O_RDONLY, 0)
if err != nil { if err != nil {
return "", err return "", err
} }