705 lines
18 KiB
Go
705 lines
18 KiB
Go
// Package fs implements galaxy/storage.Storage using the filesystem.
|
|
package fs
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"galaxy/model/client"
|
|
"galaxy/model/order"
|
|
"galaxy/model/report"
|
|
"galaxy/util"
|
|
)
|
|
|
|
const (
|
|
// stateFileName is the file name under the storage root where [client.State] is stored.
|
|
stateFileName = "state.dat"
|
|
|
|
// gameDataFileSuffix is the extension for per-turn [client.GameData] files.
|
|
gameDataFileSuffix = ".dat"
|
|
|
|
defaultFilePerm = 0o644
|
|
oldFileSuffix = ".old"
|
|
newFileSuffix = ".new"
|
|
)
|
|
|
|
// StateFilePath returns the path to the persisted [client.State] file under root.
|
|
func StateFilePath(root string) string {
|
|
return filepath.Join(root, stateFileName)
|
|
}
|
|
|
|
// GameDataFilePath returns the legacy per-game data file path under root.
|
|
//
|
|
// The storage implementation keeps turn data in per-turn files under a game directory
|
|
// and does not use this helper internally.
|
|
func GameDataFilePath(root string, id fmt.Stringer) string {
|
|
return filepath.Join(root, id.String()) + gameDataFileSuffix
|
|
}
|
|
|
|
type pathLock struct {
|
|
mu sync.Mutex
|
|
refs int
|
|
}
|
|
|
|
type fsStorage struct {
|
|
storageRoot string
|
|
|
|
locksMu sync.Mutex
|
|
locks map[string]*pathLock
|
|
|
|
readFileFn func(string) ([]byte, error)
|
|
writeFileFn func(string, []byte, os.FileMode) error
|
|
renameFileFn func(string, string) error
|
|
removeFileFn func(string) error
|
|
}
|
|
|
|
type storedOrder struct {
|
|
UpdatedAt int `json:"updatedAt"`
|
|
Commands []json.RawMessage `json:"cmd"`
|
|
}
|
|
|
|
type storedGameData struct {
|
|
Turn uint `json:"turn"`
|
|
Report report.Report `json:"report"`
|
|
Order *storedOrder `json:"order,omitempty"`
|
|
}
|
|
|
|
// NewFS returns a filesystem-backed implementation of galaxy/storage.Storage rooted at storageRoot.
|
|
// storageRoot must already exist, be a directory, and be writable by the current user.
|
|
func NewFS(storageRoot string) (*fsStorage, error) {
|
|
fmt.Println("using fs root:", storageRoot)
|
|
absRoot, err := filepath.Abs(storageRoot)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new fs storage: resolve absolute path for %q: %w", storageRoot, err)
|
|
}
|
|
if ok, err := util.PathExists(absRoot, true); err != nil {
|
|
return nil, fmt.Errorf("new fs storage: check path %q exists: %w", absRoot, err)
|
|
} else if !ok {
|
|
return nil, fmt.Errorf("new fs storage: path %q does not exist", absRoot)
|
|
}
|
|
if ok, err := util.Writable(absRoot); err != nil {
|
|
return nil, fmt.Errorf("new fs storage: check path %q writable: %w", absRoot, err)
|
|
} else if !ok {
|
|
return nil, fmt.Errorf("new fs storage: path %q is not writable", absRoot)
|
|
}
|
|
|
|
return &fsStorage{
|
|
storageRoot: absRoot,
|
|
locks: make(map[string]*pathLock),
|
|
readFileFn: os.ReadFile,
|
|
writeFileFn: os.WriteFile,
|
|
renameFileFn: os.Rename,
|
|
removeFileFn: os.Remove,
|
|
}, nil
|
|
}
|
|
|
|
func (s *fsStorage) StateExistsAsync(callback func(bool, error)) {
|
|
go func() {
|
|
exists, err := s.FileExists(stateFileName)
|
|
if callback != nil {
|
|
callback(exists, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *fsStorage) LoadStateAsync(callback func(client.State, error)) {
|
|
go func() {
|
|
state, err := s.loadStateSync()
|
|
if callback != nil {
|
|
callback(state, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *fsStorage) SaveStateAsync(state client.State, callback func(error)) {
|
|
go func() {
|
|
err := s.saveStateSync(state)
|
|
if callback != nil {
|
|
callback(err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *fsStorage) LoadReportAsync(id client.GameID, turn uint, callback func(report.Report, error)) {
|
|
go func() {
|
|
rep, err := s.loadReportSync(id, turn)
|
|
if callback != nil {
|
|
callback(rep, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *fsStorage) SaveReportAsync(id client.GameID, turn uint, rep report.Report, callback func(error)) {
|
|
go func() {
|
|
err := s.saveReportSync(id, turn, rep)
|
|
if callback != nil {
|
|
callback(err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *fsStorage) LoadOrderAsync(id client.GameID, turn uint, callback func(order.Order, error)) {
|
|
go func() {
|
|
o, err := s.loadOrderSync(id, turn)
|
|
if callback != nil {
|
|
callback(o, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *fsStorage) SaveOrderAsync(id client.GameID, turn uint, o order.Order, callback func(error)) {
|
|
go func() {
|
|
err := s.saveOrderSync(id, turn, o)
|
|
if callback != nil {
|
|
callback(err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *fsStorage) FileExists(path string) (bool, error) {
|
|
absPath, err := s.resolvePath(path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var exists bool
|
|
err = s.withPathLock(absPath, func() error {
|
|
var opErr error
|
|
exists, opErr = s.fileExistsUnlocked(absPath)
|
|
return opErr
|
|
})
|
|
return exists, err
|
|
}
|
|
|
|
func (s *fsStorage) ReadFile(path string) ([]byte, error) {
|
|
absPath, err := s.resolvePath(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var data []byte
|
|
err = s.withPathLock(absPath, func() error {
|
|
var opErr error
|
|
data, opErr = s.readFileUnlocked(absPath)
|
|
return opErr
|
|
})
|
|
return data, err
|
|
}
|
|
|
|
func (s *fsStorage) WriteFile(path string, data []byte) error {
|
|
absPath, err := s.resolvePath(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.withPathLock(absPath, func() error {
|
|
return s.writeFileUnlocked(absPath, data)
|
|
})
|
|
}
|
|
|
|
func (s *fsStorage) DeleteFile(path string) error {
|
|
absPath, err := s.resolvePath(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.withPathLock(absPath, func() error {
|
|
exists, err := s.fileExistsUnlocked(absPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return fmt.Errorf("delete file %q: %w", absPath, os.ErrNotExist)
|
|
}
|
|
if err := s.removeFileFn(absPath); err != nil {
|
|
return fmt.Errorf("delete file %q: %w", absPath, err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *fsStorage) ListFiles() ([]string, error) {
|
|
files := make([]string, 0)
|
|
err := filepath.WalkDir(s.storageRoot, func(path string, d os.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
if strings.HasSuffix(d.Name(), newFileSuffix) || strings.HasSuffix(d.Name(), oldFileSuffix) {
|
|
return nil
|
|
}
|
|
|
|
relPath, err := filepath.Rel(s.storageRoot, path)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve relative path for %q: %w", path, err)
|
|
}
|
|
files = append(files, filepath.Clean(relPath))
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list files under %q: %w", s.storageRoot, err)
|
|
}
|
|
|
|
slices.Sort(files)
|
|
return files, nil
|
|
}
|
|
|
|
func (s *fsStorage) loadStateSync() (client.State, error) {
|
|
data, err := s.ReadFile(stateFileName)
|
|
if err != nil {
|
|
return client.State{}, err
|
|
}
|
|
return unmarshalState(data)
|
|
}
|
|
|
|
func (s *fsStorage) saveStateSync(state client.State) error {
|
|
data, err := marshalState(state)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.WriteFile(stateFileName, data)
|
|
}
|
|
|
|
func (s *fsStorage) loadReportSync(id client.GameID, turn uint) (report.Report, error) {
|
|
gameData, err := s.loadGameDataSync(id, turn)
|
|
if err != nil {
|
|
return report.Report{}, err
|
|
}
|
|
return gameData.Report, nil
|
|
}
|
|
|
|
func (s *fsStorage) saveReportSync(id client.GameID, turn uint, rep report.Report) error {
|
|
absPath, err := s.resolvePath(gameTurnFilePath(id, turn))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.withPathLock(absPath, func() error {
|
|
gameData, err := s.loadGameDataUnlocked(absPath)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
gameData = client.GameData{Turn: turn}
|
|
}
|
|
|
|
gameData.Turn = turn
|
|
gameData.Report = rep
|
|
return s.writeGameDataUnlocked(absPath, gameData)
|
|
})
|
|
}
|
|
|
|
func (s *fsStorage) loadOrderSync(id client.GameID, turn uint) (order.Order, error) {
|
|
gameData, err := s.loadGameDataSync(id, turn)
|
|
if err != nil {
|
|
return order.Order{}, err
|
|
}
|
|
if gameData.Order == nil {
|
|
return order.Order{}, fmt.Errorf("load order for game %q turn %d: %w", id, turn, os.ErrNotExist)
|
|
}
|
|
return *gameData.Order, nil
|
|
}
|
|
|
|
func (s *fsStorage) saveOrderSync(id client.GameID, turn uint, o order.Order) error {
|
|
absPath, err := s.resolvePath(gameTurnFilePath(id, turn))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.withPathLock(absPath, func() error {
|
|
gameData, err := s.loadGameDataUnlocked(absPath)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("save order for game %q turn %d: %w", id, turn, os.ErrNotExist)
|
|
}
|
|
return err
|
|
}
|
|
|
|
gameData.Turn = turn
|
|
gameData.Order = &o
|
|
return s.writeGameDataUnlocked(absPath, gameData)
|
|
})
|
|
}
|
|
|
|
func (s *fsStorage) loadGameDataSync(id client.GameID, turn uint) (client.GameData, error) {
|
|
absPath, err := s.resolvePath(gameTurnFilePath(id, turn))
|
|
if err != nil {
|
|
return client.GameData{}, err
|
|
}
|
|
|
|
var gameData client.GameData
|
|
err = s.withPathLock(absPath, func() error {
|
|
var opErr error
|
|
gameData, opErr = s.loadGameDataUnlocked(absPath)
|
|
return opErr
|
|
})
|
|
return gameData, err
|
|
}
|
|
|
|
func (s *fsStorage) loadGameDataUnlocked(absPath string) (client.GameData, error) {
|
|
data, err := s.readFileUnlocked(absPath)
|
|
if err != nil {
|
|
return client.GameData{}, err
|
|
}
|
|
return unmarshalGameData(data)
|
|
}
|
|
|
|
func (s *fsStorage) writeGameDataUnlocked(absPath string, gameData client.GameData) error {
|
|
data, err := marshalGameData(gameData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.writeFileUnlocked(absPath, data)
|
|
}
|
|
|
|
func marshalState(state client.State) ([]byte, error) {
|
|
return marshalJSON(state)
|
|
}
|
|
|
|
func unmarshalState(data []byte) (client.State, error) {
|
|
var state client.State
|
|
if err := unmarshalJSON(data, &state); err != nil {
|
|
return client.State{}, err
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func marshalGameData(gameData client.GameData) ([]byte, error) {
|
|
stored, err := makeStoredGameData(gameData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return marshalJSON(stored)
|
|
}
|
|
|
|
func unmarshalGameData(data []byte) (client.GameData, error) {
|
|
var stored storedGameData
|
|
if err := unmarshalJSON(data, &stored); err != nil {
|
|
return client.GameData{}, err
|
|
}
|
|
return stored.toGameData()
|
|
}
|
|
|
|
func marshalJSON(value any) ([]byte, error) {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal json: %w", err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func unmarshalJSON(data []byte, target any) error {
|
|
if err := json.Unmarshal(data, target); err != nil {
|
|
return fmt.Errorf("unmarshal json: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func makeStoredGameData(gameData client.GameData) (storedGameData, error) {
|
|
stored := storedGameData{
|
|
Turn: gameData.Turn,
|
|
Report: gameData.Report,
|
|
}
|
|
if gameData.Order == nil {
|
|
return stored, nil
|
|
}
|
|
|
|
storedOrder, err := makeStoredOrder(*gameData.Order)
|
|
if err != nil {
|
|
return storedGameData{}, err
|
|
}
|
|
stored.Order = &storedOrder
|
|
return stored, nil
|
|
}
|
|
|
|
func (d storedGameData) toGameData() (client.GameData, error) {
|
|
gameData := client.GameData{
|
|
Turn: d.Turn,
|
|
Report: d.Report,
|
|
}
|
|
if d.Order == nil {
|
|
return gameData, nil
|
|
}
|
|
|
|
o, err := d.Order.toOrder()
|
|
if err != nil {
|
|
return client.GameData{}, err
|
|
}
|
|
gameData.Order = o
|
|
return gameData, nil
|
|
}
|
|
|
|
func makeStoredOrder(o order.Order) (storedOrder, error) {
|
|
result := storedOrder{
|
|
UpdatedAt: o.UpdatedAt,
|
|
Commands: make([]json.RawMessage, len(o.Commands)),
|
|
}
|
|
for i := range o.Commands {
|
|
data, err := marshalJSON(o.Commands[i])
|
|
if err != nil {
|
|
return storedOrder{}, fmt.Errorf("marshal order command %d: %w", i, err)
|
|
}
|
|
result.Commands[i] = data
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (o *storedOrder) toOrder() (*order.Order, error) {
|
|
if o == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
result := &order.Order{
|
|
UpdatedAt: o.UpdatedAt,
|
|
Commands: make([]order.DecodableCommand, len(o.Commands)),
|
|
}
|
|
for i := range o.Commands {
|
|
cmd, err := parseOrderCommand(o.Commands[i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", i, err)
|
|
}
|
|
result.Commands[i] = cmd
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func parseOrderCommand(data json.RawMessage) (order.DecodableCommand, error) {
|
|
meta := new(order.CommandMeta)
|
|
if err := json.Unmarshal(data, meta); err != nil {
|
|
return nil, fmt.Errorf("decode order command metadata: %w", err)
|
|
}
|
|
|
|
switch meta.CmdType {
|
|
case order.CommandTypeRaceQuit:
|
|
return decodeOrderCommand(data, new(order.CommandRaceQuit))
|
|
case order.CommandTypeRaceVote:
|
|
return decodeOrderCommand(data, new(order.CommandRaceVote))
|
|
case order.CommandTypeRaceRelation:
|
|
return decodeOrderCommand(data, new(order.CommandRaceRelation))
|
|
case order.CommandTypeShipClassCreate:
|
|
return decodeOrderCommand(data, new(order.CommandShipClassCreate))
|
|
case order.CommandTypeShipClassMerge:
|
|
return decodeOrderCommand(data, new(order.CommandShipClassMerge))
|
|
case order.CommandTypeShipClassRemove:
|
|
return decodeOrderCommand(data, new(order.CommandShipClassRemove))
|
|
case order.CommandTypeShipGroupBreak:
|
|
return decodeOrderCommand(data, new(order.CommandShipGroupBreak))
|
|
case order.CommandTypeShipGroupLoad:
|
|
return decodeOrderCommand(data, new(order.CommandShipGroupLoad))
|
|
case order.CommandTypeShipGroupUnload:
|
|
return decodeOrderCommand(data, new(order.CommandShipGroupUnload))
|
|
case order.CommandTypeShipGroupSend:
|
|
return decodeOrderCommand(data, new(order.CommandShipGroupSend))
|
|
case order.CommandTypeShipGroupUpgrade:
|
|
return decodeOrderCommand(data, new(order.CommandShipGroupUpgrade))
|
|
case order.CommandTypeShipGroupMerge:
|
|
return decodeOrderCommand(data, new(order.CommandShipGroupMerge))
|
|
case order.CommandTypeShipGroupDismantle:
|
|
return decodeOrderCommand(data, new(order.CommandShipGroupDismantle))
|
|
case order.CommandTypeShipGroupTransfer:
|
|
return decodeOrderCommand(data, new(order.CommandShipGroupTransfer))
|
|
case order.CommandTypeShipGroupJoinFleet:
|
|
return decodeOrderCommand(data, new(order.CommandShipGroupJoinFleet))
|
|
case order.CommandTypeFleetMerge:
|
|
return decodeOrderCommand(data, new(order.CommandFleetMerge))
|
|
case order.CommandTypeFleetSend:
|
|
return decodeOrderCommand(data, new(order.CommandFleetSend))
|
|
case order.CommandTypeScienceCreate:
|
|
return decodeOrderCommand(data, new(order.CommandScienceCreate))
|
|
case order.CommandTypeScienceRemove:
|
|
return decodeOrderCommand(data, new(order.CommandScienceRemove))
|
|
case order.CommandTypePlanetRename:
|
|
return decodeOrderCommand(data, new(order.CommandPlanetRename))
|
|
case order.CommandTypePlanetProduce:
|
|
return decodeOrderCommand(data, new(order.CommandPlanetProduce))
|
|
case order.CommandTypePlanetRouteSet:
|
|
return decodeOrderCommand(data, new(order.CommandPlanetRouteSet))
|
|
case order.CommandTypePlanetRouteRemove:
|
|
return decodeOrderCommand(data, new(order.CommandPlanetRouteRemove))
|
|
default:
|
|
return nil, fmt.Errorf("unknown order command type %q", meta.CmdType)
|
|
}
|
|
}
|
|
|
|
func decodeOrderCommand[T order.DecodableCommand](data json.RawMessage, target T) (T, error) {
|
|
if err := json.Unmarshal(data, target); err != nil {
|
|
return target, err
|
|
}
|
|
return target, nil
|
|
}
|
|
|
|
func (s *fsStorage) resolvePath(path string) (string, error) {
|
|
relPath, err := normalizeRelativePath(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(s.storageRoot, relPath), nil
|
|
}
|
|
|
|
func normalizeRelativePath(path string) (string, error) {
|
|
path = strings.ReplaceAll(path, "\\", string(filepath.Separator))
|
|
path = strings.TrimLeft(path, string(filepath.Separator))
|
|
path = filepath.Clean(path)
|
|
|
|
switch {
|
|
case path == "." || path == "":
|
|
return "", errors.New("path must not be empty")
|
|
case filepath.IsAbs(path):
|
|
return "", fmt.Errorf("path %q must be relative", path)
|
|
case filepath.VolumeName(path) != "":
|
|
return "", fmt.Errorf("path %q must not include a volume name", path)
|
|
case path == "..":
|
|
return "", fmt.Errorf("path %q escapes storage root", path)
|
|
case strings.HasPrefix(path, ".."+string(filepath.Separator)):
|
|
return "", fmt.Errorf("path %q escapes storage root", path)
|
|
}
|
|
|
|
return path, nil
|
|
}
|
|
|
|
func gameTurnFilePath(id fmt.Stringer, turn uint) string {
|
|
return filepath.Join(id.String(), strconv.FormatUint(uint64(turn), 10)+gameDataFileSuffix)
|
|
}
|
|
|
|
func (s *fsStorage) withPathLock(absPath string, fn func() error) error {
|
|
lock := s.acquirePathLock(absPath)
|
|
defer s.releasePathLock(absPath)
|
|
|
|
lock.mu.Lock()
|
|
defer lock.mu.Unlock()
|
|
|
|
return fn()
|
|
}
|
|
|
|
func (s *fsStorage) acquirePathLock(absPath string) *pathLock {
|
|
s.locksMu.Lock()
|
|
defer s.locksMu.Unlock()
|
|
|
|
lock, ok := s.locks[absPath]
|
|
if !ok {
|
|
lock = &pathLock{}
|
|
s.locks[absPath] = lock
|
|
}
|
|
lock.refs++
|
|
return lock
|
|
}
|
|
|
|
func (s *fsStorage) releasePathLock(absPath string) {
|
|
s.locksMu.Lock()
|
|
defer s.locksMu.Unlock()
|
|
|
|
lock, ok := s.locks[absPath]
|
|
if !ok {
|
|
return
|
|
}
|
|
lock.refs--
|
|
if lock.refs == 0 {
|
|
delete(s.locks, absPath)
|
|
}
|
|
}
|
|
|
|
func (s *fsStorage) fileExistsUnlocked(absPath string) (bool, error) {
|
|
ok, err := util.FileExists(absPath)
|
|
if err != nil {
|
|
return false, fmt.Errorf("check file %q exists: %w", absPath, err)
|
|
}
|
|
return ok, nil
|
|
}
|
|
|
|
func (s *fsStorage) readFileUnlocked(absPath string) ([]byte, error) {
|
|
data, err := s.readFileFn(absPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read file %q: %w", absPath, err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func (s *fsStorage) writeFileUnlocked(absPath string, data []byte) error {
|
|
if err := s.ensureParentDir(absPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
targetExists, err := s.fileExistsUnlocked(absPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldPath := absPath + oldFileSuffix
|
|
oldExists, err := s.fileExistsUnlocked(oldPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if oldExists {
|
|
return fmt.Errorf("write file %q: old file already exists at %q", absPath, oldPath)
|
|
}
|
|
|
|
newPath := absPath + newFileSuffix
|
|
newExists, err := s.fileExistsUnlocked(newPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if newExists {
|
|
return fmt.Errorf("write file %q: new file already exists at %q", absPath, newPath)
|
|
}
|
|
|
|
if err := s.writeFileFn(newPath, data, defaultFilePerm); err != nil {
|
|
return fmt.Errorf("write new file %q: %w", newPath, err)
|
|
}
|
|
|
|
if targetExists {
|
|
if err := s.renameFileFn(absPath, oldPath); err != nil {
|
|
return errors.Join(
|
|
fmt.Errorf("rename file %q to %q: %w", absPath, oldPath, err),
|
|
s.cleanupTempFile(newPath),
|
|
)
|
|
}
|
|
}
|
|
|
|
if err := s.renameFileFn(newPath, absPath); err != nil {
|
|
var restoreErr error
|
|
if targetExists {
|
|
restoreErr = s.renameFileFn(oldPath, absPath)
|
|
if restoreErr != nil {
|
|
restoreErr = fmt.Errorf("restore file %q from %q: %w", absPath, oldPath, restoreErr)
|
|
}
|
|
}
|
|
return errors.Join(
|
|
fmt.Errorf("rename new file %q to %q: %w", newPath, absPath, err),
|
|
restoreErr,
|
|
s.cleanupTempFile(newPath),
|
|
)
|
|
}
|
|
|
|
if !targetExists {
|
|
return nil
|
|
}
|
|
if err := s.removeFileFn(oldPath); err != nil {
|
|
return fmt.Errorf("remove old file %q: %w", oldPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *fsStorage) cleanupTempFile(path string) error {
|
|
if err := s.removeFileFn(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("remove temp file %q: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *fsStorage) ensureParentDir(absPath string) error {
|
|
parentDir := filepath.Dir(absPath)
|
|
if err := os.MkdirAll(parentDir, os.ModePerm); err != nil {
|
|
return fmt.Errorf("create parent directory %q: %w", parentDir, err)
|
|
}
|
|
return nil
|
|
}
|