Skip to content

Commit 92188e1

Browse files
committed
Add generic filestore
Signed-off-by: apostasie <[email protected]>
1 parent f2bfc60 commit 92188e1

File tree

5 files changed

+790
-0
lines changed

5 files changed

+790
-0
lines changed

pkg/store/filestore.go

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package store
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"os"
23+
"path/filepath"
24+
"strings"
25+
"sync"
26+
27+
"github.com/containerd/nerdctl/v2/pkg/lockutil"
28+
)
29+
30+
// TODO: implement a read-lock in lockutil, in addition to the current exclusive write-lock
31+
// This might improve performance in case of (mostly read) massively parallel concurrent scenarios
32+
33+
const (
34+
// Default filesystem permissions to use when creating dir or files
35+
defaultFilePerm = 0o600
36+
defaultDirPerm = 0o700
37+
)
38+
39+
// New returns a filesystem based Store implementation that satisfies both Manager and Locker
40+
// Note that atomicity is "guaranteed" by `os.Rename`, which arguably is not *always* atomic.
41+
// In particular, operating-system crashes may break that promise, and windows behavior is probably questionable.
42+
// That being said, this is still a much better solution than writing directly to the destination file.
43+
func New(rootPath string, dirPerm os.FileMode, filePerm os.FileMode) (Store, error) {
44+
if rootPath == "" {
45+
return nil, errors.Join(ErrInvalidArgument, fmt.Errorf("FileStore rootPath cannot be empty"))
46+
}
47+
48+
if dirPerm == 0 {
49+
dirPerm = defaultDirPerm
50+
}
51+
52+
if filePerm == 0 {
53+
filePerm = defaultFilePerm
54+
}
55+
56+
if err := os.MkdirAll(rootPath, dirPerm); err != nil {
57+
return nil, errors.Join(ErrSystemFailure, err)
58+
}
59+
60+
return &fileStore{
61+
dir: rootPath,
62+
dirPerm: dirPerm,
63+
filePerm: filePerm,
64+
}, nil
65+
}
66+
67+
type fileStore struct {
68+
mutex sync.RWMutex
69+
dir string
70+
locked *os.File
71+
dirPerm os.FileMode
72+
filePerm os.FileMode
73+
}
74+
75+
func (vs *fileStore) Lock() error {
76+
vs.mutex.Lock()
77+
78+
dirFile, err := lockutil.Lock(vs.dir)
79+
if err != nil {
80+
return errors.Join(ErrLockFailure, err)
81+
}
82+
83+
vs.locked = dirFile
84+
85+
return nil
86+
}
87+
88+
func (vs *fileStore) Release() error {
89+
if vs.locked == nil {
90+
return errors.Join(ErrFaultyImplementation, fmt.Errorf("cannot unlock already unlocked volume store %q", vs.dir))
91+
}
92+
93+
defer vs.mutex.Unlock()
94+
95+
defer func() {
96+
vs.locked = nil
97+
}()
98+
99+
if err := lockutil.Unlock(vs.locked); err != nil {
100+
return errors.Join(ErrLockFailure, err)
101+
}
102+
103+
return nil
104+
}
105+
106+
func (vs *fileStore) WithLock(fun func() error) (err error) {
107+
if err = vs.Lock(); err != nil {
108+
return err
109+
}
110+
111+
defer func() {
112+
err = errors.Join(vs.Release(), err)
113+
}()
114+
115+
return fun()
116+
}
117+
118+
func (vs *fileStore) Get(key ...string) ([]byte, error) {
119+
if vs.locked == nil {
120+
return nil, errors.Join(ErrFaultyImplementation, fmt.Errorf("operations on the store must use locking"))
121+
}
122+
123+
if err := validateAllPathComponents(key...); err != nil {
124+
return nil, err
125+
}
126+
127+
path := filepath.Join(append([]string{vs.dir}, key...)...)
128+
129+
st, err := os.Stat(path)
130+
if err != nil {
131+
if errors.Is(err, os.ErrNotExist) {
132+
return nil, errors.Join(ErrNotFound, fmt.Errorf("%q does not exist", filepath.Join(key...)))
133+
}
134+
135+
return nil, errors.Join(ErrSystemFailure, err)
136+
}
137+
138+
if st.IsDir() {
139+
return nil, errors.Join(ErrFaultyImplementation, fmt.Errorf("%q is a directory and cannot be read as a file", path))
140+
}
141+
142+
content, err := os.ReadFile(filepath.Join(append([]string{vs.dir}, key...)...))
143+
if err != nil {
144+
return nil, errors.Join(ErrSystemFailure, err)
145+
}
146+
147+
return content, nil
148+
}
149+
150+
func (vs *fileStore) Exists(key ...string) (bool, error) {
151+
if err := validateAllPathComponents(key...); err != nil {
152+
return false, err
153+
}
154+
155+
path := filepath.Join(append([]string{vs.dir}, key...)...)
156+
157+
_, err := os.Stat(filepath.Join(path))
158+
if err != nil {
159+
if errors.Is(err, os.ErrNotExist) {
160+
return false, nil
161+
}
162+
163+
return false, errors.Join(ErrSystemFailure, err)
164+
}
165+
166+
return true, nil
167+
}
168+
169+
func (vs *fileStore) Set(data []byte, key ...string) error {
170+
if vs.locked == nil {
171+
return errors.Join(ErrFaultyImplementation, fmt.Errorf("operations on the store must use locking"))
172+
}
173+
174+
if err := validateAllPathComponents(key...); err != nil {
175+
return err
176+
}
177+
178+
fileName := key[len(key)-1]
179+
parent := vs.dir
180+
181+
if len(key) > 1 {
182+
parent = filepath.Join(append([]string{parent}, key[0:len(key)-1]...)...)
183+
err := os.MkdirAll(parent, vs.dirPerm)
184+
if err != nil {
185+
return errors.Join(ErrSystemFailure, err)
186+
}
187+
}
188+
189+
dest := filepath.Join(parent, fileName)
190+
st, err := os.Stat(dest)
191+
if err == nil {
192+
if st.IsDir() {
193+
return errors.Join(ErrFaultyImplementation, fmt.Errorf("%q is a directory and cannot be written to", dest))
194+
}
195+
}
196+
197+
return atomicWrite(parent, fileName, vs.filePerm, data)
198+
}
199+
200+
func (vs *fileStore) List(key ...string) ([]string, error) {
201+
if vs.locked == nil {
202+
return nil, errors.Join(ErrFaultyImplementation, fmt.Errorf("operations on the store must use locking"))
203+
}
204+
205+
// Unlike Get, Set and Delete, List can have zero length key
206+
for _, k := range key {
207+
if err := validatePathComponent(k); err != nil {
208+
return nil, err
209+
}
210+
}
211+
212+
path := filepath.Join(append([]string{vs.dir}, key...)...)
213+
214+
st, err := os.Stat(path)
215+
if err != nil {
216+
if errors.Is(err, os.ErrNotExist) {
217+
return nil, errors.Join(ErrNotFound, err)
218+
}
219+
220+
return nil, errors.Join(ErrSystemFailure, err)
221+
}
222+
223+
if !st.IsDir() {
224+
return nil, errors.Join(ErrFaultyImplementation, fmt.Errorf("%q is not a directory and cannot be enumerated", path))
225+
}
226+
227+
dirEntries, err := os.ReadDir(path)
228+
if err != nil {
229+
return nil, errors.Join(ErrSystemFailure, err)
230+
}
231+
232+
entries := []string{}
233+
for _, dirEntry := range dirEntries {
234+
entries = append(entries, dirEntry.Name())
235+
}
236+
237+
return entries, nil
238+
}
239+
240+
func (vs *fileStore) Delete(key ...string) error {
241+
if vs.locked == nil {
242+
return errors.Join(ErrFaultyImplementation, fmt.Errorf("operations on the store must use locking"))
243+
}
244+
245+
if err := validateAllPathComponents(key...); err != nil {
246+
return err
247+
}
248+
249+
path := filepath.Join(append([]string{vs.dir}, key...)...)
250+
251+
_, err := os.Stat(path)
252+
if err != nil {
253+
if errors.Is(err, os.ErrNotExist) {
254+
return errors.Join(ErrNotFound, err)
255+
}
256+
257+
return errors.Join(ErrSystemFailure, err)
258+
}
259+
260+
if err = os.RemoveAll(path); err != nil {
261+
return errors.Join(ErrSystemFailure, err)
262+
}
263+
264+
return nil
265+
}
266+
267+
func (vs *fileStore) Location(key ...string) (string, error) {
268+
if err := validateAllPathComponents(key...); err != nil {
269+
return "", err
270+
}
271+
272+
return filepath.Join(append([]string{vs.dir}, key...)...), nil
273+
}
274+
275+
func (vs *fileStore) GroupEnsure(key ...string) error {
276+
if vs.locked == nil {
277+
return errors.Join(ErrFaultyImplementation, fmt.Errorf("operations on the store must use locking"))
278+
}
279+
280+
if err := validateAllPathComponents(key...); err != nil {
281+
return err
282+
}
283+
284+
path := filepath.Join(append([]string{vs.dir}, key...)...)
285+
286+
if err := os.MkdirAll(path, vs.dirPerm); err != nil {
287+
return errors.Join(ErrSystemFailure, err)
288+
}
289+
290+
return nil
291+
}
292+
293+
func (vs *fileStore) GroupSize(key ...string) (int64, error) {
294+
if vs.locked == nil {
295+
return 0, errors.Join(ErrFaultyImplementation, fmt.Errorf("operations on the store must use locking"))
296+
}
297+
298+
if err := validateAllPathComponents(key...); err != nil {
299+
return 0, err
300+
}
301+
302+
path := filepath.Join(append([]string{vs.dir}, key...)...)
303+
304+
st, err := os.Stat(path)
305+
if err != nil {
306+
if errors.Is(err, os.ErrNotExist) {
307+
return 0, errors.Join(ErrNotFound, err)
308+
}
309+
310+
return 0, errors.Join(ErrSystemFailure, err)
311+
}
312+
313+
if !st.IsDir() {
314+
return 0, errors.Join(ErrFaultyImplementation, fmt.Errorf("%q is not a directory", path))
315+
}
316+
317+
var size int64
318+
var walkFn = func(_ string, info os.FileInfo, err error) error {
319+
if err != nil {
320+
return err
321+
}
322+
if !info.IsDir() {
323+
size += info.Size()
324+
}
325+
return err
326+
}
327+
328+
err = filepath.Walk(path, walkFn)
329+
if err != nil {
330+
return 0, err
331+
}
332+
333+
return size, nil
334+
}
335+
336+
// validatePathComponent will enforce os specific filename restrictions on a single path component
337+
func validatePathComponent(pathComponent string) error {
338+
// https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits
339+
if len(pathComponent) > 255 {
340+
return errors.Join(ErrInvalidArgument, errors.New("identifiers must be stricly shorter than 256 characters"))
341+
}
342+
343+
if strings.TrimSpace(pathComponent) == "" {
344+
return errors.Join(ErrInvalidArgument, errors.New("identifier cannot be empty"))
345+
}
346+
347+
if err := validatePlatformSpecific(pathComponent); err != nil {
348+
return errors.Join(ErrInvalidArgument, err)
349+
}
350+
351+
return nil
352+
}
353+
354+
// validateAllPathComponents will enforce validation for a slice of components
355+
func validateAllPathComponents(pathComponent ...string) error {
356+
if len(pathComponent) == 0 {
357+
return errors.Join(ErrInvalidArgument, errors.New("you must specify an identifier"))
358+
}
359+
360+
for _, key := range pathComponent {
361+
if err := validatePathComponent(key); err != nil {
362+
return err
363+
}
364+
}
365+
366+
return nil
367+
}
368+
369+
func atomicWrite(parent string, fileName string, perm os.FileMode, data []byte) error {
370+
dest := filepath.Join(parent, fileName)
371+
temp := filepath.Join(parent, ".temp."+fileName)
372+
373+
err := os.WriteFile(temp, data, perm)
374+
if err != nil {
375+
return errors.Join(ErrSystemFailure, err)
376+
}
377+
378+
err = os.Rename(temp, dest)
379+
if err != nil {
380+
return errors.Join(ErrSystemFailure, err)
381+
}
382+
383+
return nil
384+
}

0 commit comments

Comments
 (0)