mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-11 15:53:04 +03:00
194 lines
4.7 KiB
Go
194 lines
4.7 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type filesystemStorage struct {
|
|
root *os.Root
|
|
absoluteRootPath string
|
|
}
|
|
|
|
func NewFilesystemStorage(rootPath string) (FileStorage, error) {
|
|
if err := os.MkdirAll(rootPath, 0700); err != nil {
|
|
return nil, fmt.Errorf("failed to create root directory '%s': %w", rootPath, err)
|
|
}
|
|
root, err := os.OpenRoot(rootPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open root directory '%s': %w", rootPath, err)
|
|
}
|
|
|
|
absoluteRootPath, err := filepath.Abs(rootPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get absolute path of root directory '%s': %w", rootPath, err)
|
|
}
|
|
|
|
return &filesystemStorage{root: root, absoluteRootPath: absoluteRootPath}, err
|
|
}
|
|
|
|
func (s *filesystemStorage) Type() string {
|
|
return TypeFileSystem
|
|
}
|
|
|
|
func (s *filesystemStorage) Save(_ context.Context, path string, data io.Reader) error {
|
|
path = filepath.FromSlash(path)
|
|
|
|
if err := s.root.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
|
return fmt.Errorf("failed to create directories for path '%s': %w", path, err)
|
|
}
|
|
|
|
// Our strategy is to save to a separate file and then rename it to override the original file
|
|
tmpName := path + "." + uuid.NewString() + "-tmp"
|
|
|
|
// Write to the temporary file
|
|
tmpFile, err := s.root.Create(tmpName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open file '%s' for writing: %w", tmpName, err)
|
|
}
|
|
|
|
_, err = io.Copy(tmpFile, data)
|
|
if err != nil {
|
|
tmpFile.Close()
|
|
_ = s.root.Remove(tmpName)
|
|
return fmt.Errorf("failed to write temporary file: %w", err)
|
|
}
|
|
|
|
if err = tmpFile.Close(); err != nil {
|
|
_ = s.root.Remove(tmpName)
|
|
return fmt.Errorf("failed to close temporary file: %w", err)
|
|
}
|
|
|
|
// Rename to the final file, which overrides existing files
|
|
// This is an atomic operation
|
|
if err = s.root.Rename(tmpName, path); err != nil {
|
|
_ = s.root.Remove(tmpName)
|
|
return fmt.Errorf("failed to move temporary file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *filesystemStorage) Open(_ context.Context, path string) (io.ReadCloser, int64, error) {
|
|
path = filepath.FromSlash(path)
|
|
|
|
file, err := s.root.Open(path)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
file.Close()
|
|
return nil, 0, err
|
|
}
|
|
return file, info.Size(), nil
|
|
}
|
|
|
|
func (s *filesystemStorage) Delete(_ context.Context, path string) error {
|
|
path = filepath.FromSlash(path)
|
|
|
|
err := s.root.Remove(path)
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *filesystemStorage) DeleteAll(_ context.Context, path string) error {
|
|
path = filepath.FromSlash(path)
|
|
|
|
// If "/", "." or "" is requested, we delete all contents of the root.
|
|
if path == "" || path == "/" || path == "." {
|
|
dir, err := s.root.Open(".")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open root directory: %w", err)
|
|
}
|
|
defer dir.Close()
|
|
|
|
entries, err := dir.ReadDir(-1)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list root directory: %w", err)
|
|
}
|
|
for _, entry := range entries {
|
|
if err := s.root.RemoveAll(entry.Name()); err != nil {
|
|
return fmt.Errorf("failed to delete '%s': %w", entry.Name(), err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return s.root.RemoveAll(path)
|
|
}
|
|
func (s *filesystemStorage) List(_ context.Context, path string) ([]ObjectInfo, error) {
|
|
path = filepath.FromSlash(path)
|
|
|
|
dir, err := s.root.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer dir.Close()
|
|
|
|
entries, err := dir.ReadDir(-1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
objects := make([]ObjectInfo, 0, len(entries))
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
objects = append(objects, ObjectInfo{
|
|
Path: filepath.Join(path, entry.Name()),
|
|
Size: info.Size(),
|
|
ModTime: info.ModTime(),
|
|
})
|
|
}
|
|
return objects, nil
|
|
}
|
|
func (s *filesystemStorage) Walk(_ context.Context, root string, fn func(ObjectInfo) error) error {
|
|
root = filepath.FromSlash(root)
|
|
|
|
fullPath := filepath.Clean(filepath.Join(s.absoluteRootPath, root))
|
|
|
|
// As we can't use os.Root here, we manually ensure that the fullPath is within the root directory
|
|
sep := string(filepath.Separator)
|
|
if !strings.HasPrefix(fullPath+sep, s.absoluteRootPath+sep) {
|
|
return fmt.Errorf("invalid root path: %s", root)
|
|
}
|
|
|
|
return filepath.WalkDir(fullPath, func(full string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(s.absoluteRootPath, full)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
info, err := d.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fn(ObjectInfo{
|
|
Path: filepath.ToSlash(rel),
|
|
Size: info.Size(),
|
|
ModTime: info.ModTime(),
|
|
})
|
|
})
|
|
}
|