package storage import ( "context" "fmt" "io" "os" "path/filepath" "strings" "time" ) // local stores objects under root (STORAGE_PATH, default ./data/photos). // Deliberately disjoint from backend/uploads; nothing here is ever served // statically — all reads go through the access-checked files handler. type local struct { root string } func newLocal(root string) (*local, error) { if err := os.MkdirAll(root, 0o755); err != nil { return nil, fmt.Errorf("create storage dir %s: %w", root, err) } return &local{root: root}, nil } // path validates the key stays inside root (keys are generated internally, // but defense-in-depth costs one Clean call). func (l *local) path(key string) (string, error) { clean := filepath.Clean("/" + key) if strings.Contains(clean, "..") { return "", fmt.Errorf("invalid key %q", key) } return filepath.Join(l.root, clean), nil } func (l *local) Put(_ context.Context, key string, r io.Reader, _ int64, _ string) error { dst, err := l.path(key) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { return err } tmp, err := os.CreateTemp(filepath.Dir(dst), ".upload-*") if err != nil { return err } defer os.Remove(tmp.Name()) if _, err := io.Copy(tmp, r); err != nil { tmp.Close() return err } if err := tmp.Close(); err != nil { return err } return os.Rename(tmp.Name(), dst) } func (l *local) Open(_ context.Context, key string) (io.ReadCloser, int64, error) { p, err := l.path(key) if err != nil { return nil, 0, err } f, err := os.Open(p) if err != nil { return nil, 0, err } info, err := f.Stat() if err != nil { f.Close() return nil, 0, err } return f, info.Size(), nil } func (l *local) Delete(_ context.Context, key string) error { p, err := l.path(key) if err != nil { return err } err = os.Remove(p) if os.IsNotExist(err) { return nil } return err } func (l *local) PresignGet(context.Context, string, string, string, time.Duration) (string, error) { return "", ErrNoPresign }