package storage import ( "context" "fmt" "io" "os" "path/filepath" ) // LocalStorage implements StorageBackend using the local filesystem. type LocalStorage struct { basePath string } // NewLocalStorage creates a LocalStorage rooted at basePath. The path is // resolved to an absolute path and verified to exist. func NewLocalStorage(basePath string) (*LocalStorage, error) { abs, err := filepath.Abs(basePath) if err != nil { return nil, fmt.Errorf("resolve storage path: %w", err) } return &LocalStorage{basePath: abs}, nil } // Save writes the contents of reader to path under the storage root. func (s *LocalStorage) Save(_ context.Context, path string, reader io.Reader) (int64, error) { fullPath := filepath.Join(s.basePath, path) if !isSubPath(s.basePath, fullPath) { return 0, fmt.Errorf("storage: path traversal attempt: %s", path) } if err := os.MkdirAll(filepath.Dir(fullPath), 0750); err != nil { return 0, fmt.Errorf("create parent directory: %w", err) } file, err := os.Create(fullPath) if err != nil { return 0, fmt.Errorf("create file: %w", err) } defer file.Close() written, err := io.Copy(file, reader) if err != nil { // Best-effort cleanup on write failure. os.Remove(fullPath) return 0, fmt.Errorf("write file: %w", err) } return written, nil } // Open returns a reader for the file at path under the storage root. func (s *LocalStorage) Open(_ context.Context, path string) (io.ReadCloser, error) { fullPath := filepath.Join(s.basePath, path) if !isSubPath(s.basePath, fullPath) { return nil, fmt.Errorf("storage: path traversal attempt: %s", path) } file, err := os.Open(fullPath) if err != nil { if os.IsNotExist(err) { return nil, fmt.Errorf("file not found on disk: %s", path) } return nil, fmt.Errorf("open file: %w", err) } return file, nil } // Delete removes the file at path under the storage root. func (s *LocalStorage) Delete(_ context.Context, path string) error { fullPath := filepath.Join(s.basePath, path) if !isSubPath(s.basePath, fullPath) { return fmt.Errorf("storage: path traversal attempt: %s", path) } err := os.Remove(fullPath) if os.IsNotExist(err) { return nil // idempotent: already gone } return err } // isSubPath returns true if target is within base (no path traversal). func isSubPath(base, target string) bool { rel, err := filepath.Rel(base, target) if err != nil { return false } return rel != ".." && !filepath.IsAbs(rel) && rel[:2] != ".." }