Files
cloudsave/pkg/repository/repository.go
2025-08-09 22:19:57 +02:00

322 lines
7.1 KiB
Go

package repository
import (
"cloudsave/pkg/tools/hash"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"time"
)
type (
Metadata struct {
ID string `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
Version int `json:"version"`
Date time.Time `json:"date"`
MD5 string `json:"-"`
}
Remote struct {
URL string `json:"url"`
GameID string `json:"-"`
}
Backup struct {
CreatedAt time.Time `json:"created_at"`
MD5 string `json:"md5"`
UUID string `json:"uuid"`
ArchivePath string `json:"-"`
}
Data struct {
Metadata Metadata
DataPath string
Backup map[string]Data
}
GameIdentifier struct {
gameID string
}
BackupIdentifier struct {
gameID string
backupID string
}
Identifier interface {
Key() string
}
LazyRepository struct {
dataRoot string
}
EagerRepository struct {
LazyRepository
data map[string]Data
}
Repository interface {
Mkdir(id Identifier) error
All() ([]string, error)
AllHist(gameID GameIdentifier) ([]string, error)
WriteBlob(ID Identifier) (io.Writer, error)
WriteMetadata(gameID GameIdentifier, m Metadata) error
Metadata(gameID GameIdentifier) (Metadata, error)
LastScan(gameID GameIdentifier) (time.Time, error)
ReadBlob(gameID Identifier) (io.Reader, error)
Backup(id BackupIdentifier) (Backup, error)
SetRemote(gameID GameIdentifier, url string) error
ResetLastScan(id GameIdentifier) error
DataPath(id Identifier) string
Remove(gameID GameIdentifier) error
}
)
var (
roaming string
datastorepath string
)
func NewGameIdentifier(gameID string) GameIdentifier {
return GameIdentifier{
gameID: gameID,
}
}
func (bi GameIdentifier) Key() string {
return bi.gameID
}
func NewBackupIdentifier(gameID, backupID string) BackupIdentifier {
return BackupIdentifier{
gameID: gameID,
backupID: backupID,
}
}
func (bi BackupIdentifier) Key() string {
return bi.gameID + ":" + bi.backupID
}
func NewLazyRepository(dataRootPath string) (Repository, error) {
m, err := os.Stat(dataRootPath)
if err != nil {
return nil, fmt.Errorf("failed to open datastore: %w", err)
}
if !m.IsDir() {
return nil, fmt.Errorf("failed to open datastore: not a directory")
}
return &LazyRepository{
dataRoot: dataRootPath,
}, nil
}
func (l LazyRepository) Mkdir(id Identifier) error {
return os.MkdirAll(l.DataPath(id), 0740)
}
func (l LazyRepository) All() ([]string, error) {
dir, err := os.ReadDir(l.dataRoot)
if err != nil {
return nil, fmt.Errorf("failed to open directory: %w", err)
}
var res []string
for _, d := range dir {
res = append(res, d.Name())
}
return res, nil
}
func (l LazyRepository) AllHist(id GameIdentifier) ([]string, error) {
path := l.DataPath(id)
dir, err := os.ReadDir(filepath.Join(path, "hist"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, fmt.Errorf("failed to open directory: %w", err)
}
var res []string
for _, d := range dir {
res = append(res, d.Name())
}
return res, nil
}
func (l LazyRepository) WriteBlob(ID Identifier) (io.Writer, error) {
path := l.DataPath(ID)
dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return nil, fmt.Errorf("failed to open destination file: %w", err)
}
return dst, nil
}
func (l LazyRepository) WriteMetadata(id GameIdentifier, m Metadata) error {
path := l.DataPath(id)
dst, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return fmt.Errorf("failed to open destination file: %w", err)
}
defer dst.Close()
e := json.NewEncoder(dst)
if err := e.Encode(m); err != nil {
return fmt.Errorf("failed to encode data: %w", err)
}
return nil
}
func (l LazyRepository) Metadata(id GameIdentifier) (Metadata, error) {
path := l.DataPath(id)
src, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_RDONLY, 0)
if err != nil {
return Metadata{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err)
}
var m Metadata
d := json.NewDecoder(src)
if err := d.Decode(&m); err != nil {
return Metadata{}, fmt.Errorf("corrupted datastore: failed to parse metadata: %w", err)
}
m.MD5, err = hash.FileMD5(filepath.Join(path, "data.tar.gz"))
if err != nil {
return Metadata{}, fmt.Errorf("failed to calculate md5: %w", err)
}
return m, nil
}
func (l LazyRepository) Backup(id BackupIdentifier) (Backup, error) {
path := l.DataPath(id)
fs, err := os.Stat(filepath.Join(path, "data.tar.gz"))
if err != nil {
return Backup{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err)
}
h, err := hash.FileMD5(filepath.Join(path, "data.tar.gz"))
if err != nil {
return Backup{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err)
}
return Backup{
CreatedAt: fs.ModTime(),
MD5: h,
UUID: id.backupID,
ArchivePath: filepath.Join(path, "data.tar.gz"),
}, nil
}
func (l LazyRepository) LastScan(id GameIdentifier) (time.Time, error) {
path := l.DataPath(id)
data, err := os.ReadFile(filepath.Join(path, ".last_run"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return time.Time{}, nil
}
return time.Time{}, fmt.Errorf("failed to reading state file: %w", err)
}
lastRun, err := time.Parse(time.RFC3339, string(data))
if err != nil {
return time.Time{}, fmt.Errorf("parsing state file timestamp: %w", err)
}
return lastRun, nil
}
func (l LazyRepository) ResetLastScan(id GameIdentifier) error {
path := l.DataPath(id)
f, err := os.OpenFile(filepath.Join(path, ".last_run"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer f.Close()
data := time.Now().Format(time.RFC3339)
if _, err := f.WriteString(data); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
func (l LazyRepository) ReadBlob(id Identifier) (io.Reader, error) {
path := l.DataPath(id)
dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_RDONLY, 0)
if err != nil {
return nil, fmt.Errorf("failed to open blob: %w", err)
}
return dst, nil
}
func (l LazyRepository) SetRemote(id GameIdentifier, url string) error {
path := l.DataPath(id)
src, err := os.OpenFile(filepath.Join(path, "remote.json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
if err != nil {
return fmt.Errorf("failed to open remote description: %w", err)
}
defer src.Close()
var r Remote
d := json.NewEncoder(src)
if err := d.Encode(r); err != nil {
return fmt.Errorf("failed to marshall remote description: %w", err)
}
return nil
}
func (l LazyRepository) Remove(id GameIdentifier) error {
path := l.DataPath(id)
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("failed to remove game folder from the datastore: %w", err)
}
return nil
}
func (r LazyRepository) DataPath(id Identifier) string {
switch identifier := id.(type) {
case GameIdentifier:
return filepath.Join(r.dataRoot, identifier.gameID)
case BackupIdentifier:
return filepath.Join(r.dataRoot, identifier.gameID, "hist", identifier.backupID)
}
panic("identifier type not supported")
}