package fs import ( "encoding" "errors" "fmt" "math/big" "os" "path/filepath" "time" ) const ( defaultPerm = 0o644 lockFile = ".lock" oldFileSuffix = ".old" newFileSuffix = ".new" ) type fs struct { root string lock *os.File } func NewFileStorage(path string) (*fs, error) { absPath, err := filepath.Abs(path) if err != nil { return nil, fmt.Errorf("path %s invalid: %s", path, err) } if ok, err := dirExists(absPath); err != nil { return nil, fmt.Errorf("check dir exist: %s", err) } else if !ok { return nil, errors.New("directory does not exist: " + absPath) } if ok, err := writable(absPath); err != nil { return nil, fmt.Errorf("check dir access: %s", err) } else if !ok { return nil, errors.New("directory should have read-write access: " + absPath) } fs := &fs{ root: path, } return fs, nil } func (f *fs) Lock() (func() error, error) { lockPath := f.lockFilePath() exists, err := fileExists(lockPath) if err != nil { return nil, fmt.Errorf("check lock file exists: %s", err) } if exists { return nil, errors.New("lock file already exists") } fd, err := os.OpenFile(lockPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) if err != nil { return nil, fmt.Errorf("create lock file: %s", err) } f.lock = fd unlock := func() error { if err := f.lock.Close(); err != nil { return fmt.Errorf("close lock file: %s", err) } if err := os.Remove(f.lock.Name()); err != nil { return fmt.Errorf("remove lock file: %s", err) } f.lock = nil return nil } if _, err := f.lock.Write(big.NewInt(time.Now().UnixMilli()).Bytes()); err != nil { return nil, errors.Join(fmt.Errorf("write lock file: %s", err), unlock()) } return unlock, nil } func (f *fs) Exist(path string) (bool, error) { return fileExists(filepath.Join(f.root, path)) } func (f *fs) Write(path string, v encoding.BinaryMarshaler) error { if v == nil { return errors.New("cant't marshal from nil object") } if f.lock == nil { return errors.New("lock must be acquired before write") } targetFilePath := filepath.Join(f.root, path) if targetFilePath == f.lockFilePath() { return errors.New("can't write to the lock file") } data, err := v.MarshalBinary() if err != nil { return fmt.Errorf("marshal data: %s", err) } targetDir := filepath.Dir(targetFilePath) if targetDir != f.root { ok, err := dirExists(targetDir) if err != nil { return fmt.Errorf("check target dir exists: %s", err) } if !ok { err := os.MkdirAll(targetDir, os.ModePerm) if err != nil { return fmt.Errorf("create target dirs: %s", err) } } } oldFilePath := targetFilePath + oldFileSuffix targetExists, err := fileExists(targetFilePath) if err != nil { return fmt.Errorf("check target file exists: %s", err) } if targetExists { oldFileExists, err := fileExists(oldFilePath) if err != nil { return fmt.Errorf("check old file exists: %s", err) } if oldFileExists { return fmt.Errorf("old file exists at: %s", oldFilePath) } } newFilePath := targetFilePath + newFileSuffix newFileExists, err := fileExists(newFilePath) if err != nil { return fmt.Errorf("check new file exists: %s", err) } if newFileExists { return fmt.Errorf("new file exists at: %s", oldFilePath) } err = os.WriteFile(newFilePath, data, defaultPerm) if err != nil { return fmt.Errorf("write data to the new file: %s", err) } if targetExists { if err := os.Rename(targetFilePath, oldFilePath); err != nil { return fmt.Errorf("rename target file to the old file: %s", err) } } if err := os.Rename(newFilePath, targetFilePath); err != nil { return fmt.Errorf("rename new file to the target file: %s", err) } if targetExists { err := os.Remove(oldFilePath) if err != nil { return fmt.Errorf("remove old file: %s", err) } } return nil } func (f *fs) Read(path string, v encoding.BinaryUnmarshaler) error { if v == nil { return errors.New("can't unmarshal to a nil object") } if f.lock != nil { return errors.New("lock must be released before read") } targetFilePath := filepath.Join(f.root, path) if targetFilePath == f.lockFilePath() { return errors.New("can't read from the lock file") } data, err := os.ReadFile(targetFilePath) if err != nil { return fmt.Errorf("reading data file: %s", err) } return v.UnmarshalBinary(data) } func (f *fs) lockFilePath() string { return filepath.Join(f.root, lockFile) }