package media import ( "context" "errors" "io" "os" "path/filepath" ) // Local stores objects as files under a base directory. type Local struct { dir string } // NewLocal creates a Local store rooted at dir, creating it if needed. func NewLocal(dir string) (*Local, error) { if err := os.MkdirAll(dir, 0o750); err != nil { return nil, err } return &Local{dir: dir}, nil } func (l *Local) resolve(key string) (string, error) { clean := filepath.Clean(filepath.Join(l.dir, key)) if !hasPrefix(clean, l.dir) { return "", errors.New("media: invalid storage key") } return clean, nil } // Save writes the stream to disk atomically under the base directory. func (l *Local) Save(_ context.Context, key string, r io.Reader) error { path, err := l.resolve(key) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { return err } tmp, err := os.CreateTemp(filepath.Dir(path), ".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(), path) } // Open returns a reader for the object or ErrNotFound. func (l *Local) Open(_ context.Context, key string) (io.ReadCloser, error) { path, err := l.resolve(key) if err != nil { return nil, err } f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { return nil, ErrNotFound } return nil, err } return f, nil } // Delete removes the object; missing objects are not an error. func (l *Local) Delete(_ context.Context, key string) error { path, err := l.resolve(key) if err != nil { return err } if err := os.Remove(path); err != nil && !os.IsNotExist(err) { return err } return nil } func hasPrefix(p, dir string) bool { rel, err := filepath.Rel(dir, p) if err != nil { return false } return rel != ".." && !startsWithDotDot(rel) } func startsWithDotDot(rel string) bool { return len(rel) >= 3 && rel[0] == '.' && rel[1] == '.' && (rel[2] == '/' || rel[2] == '\\') }