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 }