383 lines
8.2 KiB
Go
383 lines
8.2 KiB
Go
package repository
|
|
|
|
import (
|
|
"cloudsave/pkg/tools/hash"
|
|
"cloudsave/pkg/tools/id"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type (
|
|
Metadata struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Version int `json:"version"`
|
|
Date time.Time `json:"date"`
|
|
}
|
|
|
|
Backup struct {
|
|
CreatedAt time.Time `json:"created_at"`
|
|
MD5 string `json:"md5"`
|
|
UUID string `json:"uuid"`
|
|
ArchivePath string `json:"-"`
|
|
}
|
|
)
|
|
|
|
var (
|
|
roaming string
|
|
datastorepath string
|
|
)
|
|
|
|
func init() {
|
|
var err error
|
|
roaming, err = os.UserConfigDir()
|
|
if err != nil {
|
|
panic("failed to get user config path: " + err.Error())
|
|
}
|
|
|
|
datastorepath = filepath.Join(roaming, "cloudsave", "data")
|
|
err = os.MkdirAll(datastorepath, 0740)
|
|
if err != nil {
|
|
panic("cannot make the datastore:" + err.Error())
|
|
}
|
|
}
|
|
|
|
func Add(name, path string) (Metadata, error) {
|
|
m := Metadata{
|
|
ID: id.New(),
|
|
Name: name,
|
|
Path: path,
|
|
}
|
|
|
|
err := os.MkdirAll(filepath.Join(datastorepath, m.ID), 0740)
|
|
if err != nil {
|
|
panic("cannot make directory for the game:" + err.Error())
|
|
}
|
|
|
|
f, err := os.OpenFile(filepath.Join(datastorepath, m.ID, "metadata.json"), os.O_CREATE|os.O_WRONLY, 0740)
|
|
if err != nil {
|
|
return Metadata{}, fmt.Errorf("cannot open the metadata file in the datastore: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
e := json.NewEncoder(f)
|
|
err = e.Encode(m)
|
|
if err != nil {
|
|
return Metadata{}, fmt.Errorf("cannot write into the metadata file in the datastore: %w", err)
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func Register(m Metadata, path string) error {
|
|
m.Path = path
|
|
|
|
err := os.MkdirAll(filepath.Join(datastorepath, m.ID), 0740)
|
|
if err != nil {
|
|
panic("cannot make directory for the game:" + err.Error())
|
|
}
|
|
|
|
f, err := os.OpenFile(filepath.Join(datastorepath, m.ID, "metadata.json"), os.O_CREATE|os.O_WRONLY, 0740)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot open the metadata file in the datastore: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
e := json.NewEncoder(f)
|
|
err = e.Encode(m)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot write into the metadata file in the datastore: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func All() ([]Metadata, error) {
|
|
ds, err := os.ReadDir(datastorepath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot open the datastore: %w", err)
|
|
}
|
|
|
|
var datastore []Metadata
|
|
for _, d := range ds {
|
|
content, err := os.ReadFile(filepath.Join(datastorepath, d.Name(), "metadata.json"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var m Metadata
|
|
err = json.Unmarshal(content, &m)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("corrupted datastore: failed to parse %s/metadata.json: %w", d.Name(), err)
|
|
}
|
|
|
|
datastore = append(datastore, m)
|
|
}
|
|
return datastore, nil
|
|
}
|
|
|
|
func One(gameID string) (Metadata, error) {
|
|
_, err := os.ReadDir(datastorepath)
|
|
if err != nil {
|
|
return Metadata{}, fmt.Errorf("cannot open the datastore: %w", err)
|
|
}
|
|
|
|
content, err := os.ReadFile(filepath.Join(datastorepath, gameID, "metadata.json"))
|
|
if err != nil {
|
|
return Metadata{}, fmt.Errorf("game not found: %w", err)
|
|
}
|
|
|
|
var m Metadata
|
|
err = json.Unmarshal(content, &m)
|
|
if err != nil {
|
|
return Metadata{}, fmt.Errorf("corrupted datastore: failed to parse %s/metadata.json: %w", gameID, err)
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func MakeArchive(gameID string) error {
|
|
path := filepath.Join(datastorepath, gameID, "data.tar.gz")
|
|
|
|
// open old
|
|
f, err := os.OpenFile(path, os.O_RDONLY, 0)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to open old file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
histDirPath := filepath.Join(datastorepath, gameID, "hist", uuid.NewString())
|
|
if err := os.MkdirAll(histDirPath, 0740); err != nil {
|
|
return fmt.Errorf("failed to make directory: %w", err)
|
|
}
|
|
|
|
// open new
|
|
nf, err := os.OpenFile(filepath.Join(histDirPath, "data.tar.gz"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open new file: %w", err)
|
|
}
|
|
defer nf.Close()
|
|
|
|
// copy
|
|
if _, err := io.Copy(nf, f); err != nil {
|
|
return fmt.Errorf("failed to copy data: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func RestoreArchive(gameID, uuid string) error {
|
|
histDirPath := filepath.Join(datastorepath, gameID, "hist", uuid)
|
|
if err := os.MkdirAll(histDirPath, 0740); err != nil {
|
|
return fmt.Errorf("failed to make directory: %w", err)
|
|
}
|
|
|
|
// open old
|
|
nf, err := os.OpenFile(filepath.Join(histDirPath, "data.tar.gz"), os.O_RDONLY, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open new file: %w", err)
|
|
}
|
|
defer nf.Close()
|
|
|
|
path := filepath.Join(datastorepath, gameID, "data.tar.gz")
|
|
|
|
// open new
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to open old file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
// copy
|
|
if _, err := io.Copy(f, nf); err != nil {
|
|
return fmt.Errorf("failed to copy data: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func Archive(gameID, uuid string) (Backup, error) {
|
|
histDirPath := filepath.Join(datastorepath, gameID, "hist", uuid)
|
|
if err := os.MkdirAll(histDirPath, 0740); err != nil {
|
|
return Backup{}, fmt.Errorf("failed to make 'hist' directory")
|
|
}
|
|
|
|
finfo, err := os.Stat(histDirPath)
|
|
if err != nil {
|
|
return Backup{}, fmt.Errorf("corrupted datastore: %w", err)
|
|
}
|
|
archivePath := filepath.Join(histDirPath, "data.tar.gz")
|
|
|
|
h, err := hash.FileMD5(archivePath)
|
|
if err != nil {
|
|
return Backup{}, fmt.Errorf("failed to calculate md5 hash: %w", err)
|
|
}
|
|
|
|
b := Backup{
|
|
CreatedAt: finfo.ModTime(),
|
|
UUID: filepath.Base(finfo.Name()),
|
|
MD5: h,
|
|
ArchivePath: archivePath,
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func Archives(gameID string) ([]Backup, error) {
|
|
histDirPath := filepath.Join(datastorepath, gameID, "hist")
|
|
if err := os.MkdirAll(histDirPath, 0740); err != nil {
|
|
return nil, fmt.Errorf("failed to make 'hist' directory")
|
|
}
|
|
|
|
d, err := os.ReadDir(histDirPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open 'hist' directory")
|
|
}
|
|
|
|
var res []Backup
|
|
for _, f := range d {
|
|
finfo, err := f.Info()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("corrupted datastore: %w", err)
|
|
}
|
|
path := filepath.Join(histDirPath, finfo.Name())
|
|
archivePath := filepath.Join(path, "data.tar.gz")
|
|
|
|
h, err := hash.FileMD5(archivePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to calculate md5 hash: %w", err)
|
|
}
|
|
|
|
b := Backup{
|
|
CreatedAt: finfo.ModTime(),
|
|
UUID: filepath.Base(finfo.Name()),
|
|
MD5: h,
|
|
ArchivePath: archivePath,
|
|
}
|
|
|
|
res = append(res, b)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func DatastorePath() string {
|
|
return datastorepath
|
|
}
|
|
|
|
func Remove(gameID string) error {
|
|
err := os.RemoveAll(filepath.Join(datastorepath, gameID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Hash(gameID string) (string, error) {
|
|
path := filepath.Join(datastorepath, gameID, "data.tar.gz")
|
|
|
|
return hash.FileMD5(path)
|
|
}
|
|
|
|
func Version(gameID string) (int, error) {
|
|
path := filepath.Join(datastorepath, gameID, "metadata.json")
|
|
|
|
f, err := os.OpenFile(path, os.O_RDONLY, 0)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer f.Close()
|
|
|
|
var metadata Metadata
|
|
d := json.NewDecoder(f)
|
|
err = d.Decode(&metadata)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return metadata.Version, nil
|
|
}
|
|
|
|
func SetVersion(gameID string, version int) error {
|
|
path := filepath.Join(datastorepath, gameID, "metadata.json")
|
|
|
|
f, err := os.OpenFile(path, os.O_RDONLY, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var metadata Metadata
|
|
d := json.NewDecoder(f)
|
|
err = d.Decode(&metadata)
|
|
if err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
|
|
f.Close()
|
|
|
|
metadata.Version = version
|
|
|
|
f, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0740)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
e := json.NewEncoder(f)
|
|
err = e.Encode(metadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SetDate(gameID string, dt time.Time) error {
|
|
path := filepath.Join(datastorepath, gameID, "metadata.json")
|
|
|
|
f, err := os.OpenFile(path, os.O_RDONLY, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var metadata Metadata
|
|
d := json.NewDecoder(f)
|
|
err = d.Decode(&metadata)
|
|
if err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
|
|
f.Close()
|
|
|
|
metadata.Date = dt
|
|
|
|
f, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0740)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
e := json.NewEncoder(f)
|
|
err = e.Encode(metadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|