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}