Files
Messenger/server/internal/media/local.go
T

96 lines
2.0 KiB
Go
Raw Normal View History

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] == '\\')
}