96 lines
2.0 KiB
Go
96 lines
2.0 KiB
Go
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] == '\\')
|
|
}
|