Files
mygo/internal/storage/local.go
T
ld b988b4b15e fix(file)!: stream uploads through staged storage
- fix: replace multipart form parsing with streaming multipart reads and
  apply request body limits when max_upload_size is configured.
- refactor: route uploads through staging paths before promotion to
  long-term data paths, keeping incomplete uploads out of durable
  storage records.
- test: cover oversized uploads, unlimited uploads, staged cleanup, and
  local storage promotion boundaries.
- docs: document the staged upload model and multipart parent_id query
  parameter.
2026-07-05 17:18:19 +08:00

163 lines
4.8 KiB
Go

package storage
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
const (
dataPathPrefix = "data"
stagingPathPrefix = "staging"
)
// 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
}
// SaveStaged writes the contents of reader to a staging path under the storage root.
func (s *LocalStorage) SaveStaged(_ 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 !hasPathPrefix(path, stagingPathPrefix) {
return 0, fmt.Errorf("storage: staged path must be under %s/: %s", stagingPathPrefix, 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
}
// PromoteStaged moves a staged file to its final path under the storage root.
func (s *LocalStorage) PromoteStaged(_ context.Context, stagedPath, finalPath string) error {
fullStagedPath := filepath.Join(s.basePath, stagedPath)
if !isSubPath(s.basePath, fullStagedPath) {
return fmt.Errorf("storage: path traversal attempt: %s", stagedPath)
}
if !hasPathPrefix(stagedPath, stagingPathPrefix) {
return fmt.Errorf("storage: staged path must be under %s/: %s", stagingPathPrefix, stagedPath)
}
fullFinalPath := filepath.Join(s.basePath, finalPath)
if !isSubPath(s.basePath, fullFinalPath) {
return fmt.Errorf("storage: path traversal attempt: %s", finalPath)
}
if !hasPathPrefix(finalPath, dataPathPrefix) {
return fmt.Errorf("storage: final path must be under %s/: %s", dataPathPrefix, finalPath)
}
if _, err := os.Stat(fullFinalPath); err == nil {
return fmt.Errorf("final file already exists: %s", finalPath)
} else if !os.IsNotExist(err) {
return fmt.Errorf("check final file: %w", err)
}
if err := os.MkdirAll(filepath.Dir(fullFinalPath), 0750); err != nil {
return fmt.Errorf("create parent directory: %w", err)
}
if err := os.Rename(fullStagedPath, fullFinalPath); err != nil {
return fmt.Errorf("promote staged file: %w", err)
}
return 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
}
// DeleteStaged removes a staged file under the storage root.
func (s *LocalStorage) DeleteStaged(_ 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)
}
if !hasPathPrefix(path, stagingPathPrefix) {
return fmt.Errorf("storage: staged path must be under %s/: %s", stagingPathPrefix, path)
}
err := os.Remove(fullPath)
if os.IsNotExist(err) {
return nil
}
return err
}
// 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
}
if filepath.IsAbs(rel) {
return false
}
// Reject paths that escape the base directory.
// filepath.Rel returns ".." or starts with "../" for paths outside base.
return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
func hasPathPrefix(path, prefix string) bool {
clean := filepath.Clean(path)
return clean == prefix || strings.HasPrefix(clean, prefix+string(filepath.Separator))
}