Parent directory

local.go

2782 bytes
  1package storage
  2
  3import (
  4	"errors"
  5	"io"
  6	"io/fs"
  7	"os"
  8	"path/filepath"
  9	"strings"
 10)
 11
 12// ErrPathTraversal is returned when a path attempts to escape the storage root.
 13var ErrPathTraversal = errors.New("path traversal detected")
 14
 15// LocalStorage is a storage implementation that stores objects on the local
 16// filesystem.
 17type LocalStorage struct {
 18	root string
 19}
 20
 21var _ Storage = (*LocalStorage)(nil)
 22
 23// NewLocalStorage creates a new LocalStorage.
 24func NewLocalStorage(root string) *LocalStorage {
 25	return &LocalStorage{root: root}
 26}
 27
 28// Delete implements Storage.
 29func (l *LocalStorage) Delete(name string) error {
 30	name, err := l.fixPath(name)
 31	if err != nil {
 32		return err
 33	}
 34	return os.Remove(name)
 35}
 36
 37// Open implements Storage.
 38func (l *LocalStorage) Open(name string) (Object, error) {
 39	name, err := l.fixPath(name)
 40	if err != nil {
 41		return nil, err
 42	}
 43	return os.Open(name)
 44}
 45
 46// Stat implements Storage.
 47func (l *LocalStorage) Stat(name string) (fs.FileInfo, error) {
 48	name, err := l.fixPath(name)
 49	if err != nil {
 50		return nil, err
 51	}
 52	return os.Stat(name)
 53}
 54
 55// Put implements Storage.
 56func (l *LocalStorage) Put(name string, r io.Reader) (int64, error) {
 57	name, err := l.fixPath(name)
 58	if err != nil {
 59		return 0, err
 60	}
 61	if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
 62		return 0, err
 63	}
 64
 65	f, err := os.Create(name)
 66	if err != nil {
 67		return 0, err
 68	}
 69	defer f.Close() //nolint: errcheck
 70	return io.Copy(f, r)
 71}
 72
 73// Exists implements Storage.
 74func (l *LocalStorage) Exists(name string) (bool, error) {
 75	name, err := l.fixPath(name)
 76	if err != nil {
 77		return false, err
 78	}
 79	_, err = os.Stat(name)
 80	if err == nil {
 81		return true, nil
 82	}
 83	if errors.Is(err, fs.ErrNotExist) {
 84		return false, nil
 85	}
 86	return false, err
 87}
 88
 89// Rename implements Storage.
 90func (l *LocalStorage) Rename(oldName, newName string) error {
 91	oldName, err := l.fixPath(oldName)
 92	if err != nil {
 93		return err
 94	}
 95	newName, err = l.fixPath(newName)
 96	if err != nil {
 97		return err
 98	}
 99	if err := os.MkdirAll(filepath.Dir(newName), os.ModePerm); err != nil {
100		return err
101	}
102
103	return os.Rename(oldName, newName)
104}
105
106// fixPath resolves the given path relative to the storage root and ensures
107// it does not escape outside the root directory.
108func (l LocalStorage) fixPath(name string) (string, error) {
109	name = strings.ReplaceAll(name, "/", string(os.PathSeparator))
110	if filepath.IsAbs(name) {
111		return "", ErrPathTraversal
112	}
113
114	resolved := filepath.Join(l.root, name)
115	// Clean the path to resolve any ".." components.
116	resolved = filepath.Clean(resolved)
117
118	// Ensure the resolved path is still under root.
119	root := filepath.Clean(l.root)
120	if !strings.HasPrefix(resolved, root+string(os.PathSeparator)) && resolved != root {
121		return "", ErrPathTraversal
122	}
123
124	return resolved, nil
125}