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(), }) }) }