- commit
- ca646f9
- parent
- 5fd38b2
- author
- Eric Bower
- date
- 2025-04-21 14:43:56 -0400 EDT
chore(storage.fs): add tests
2 files changed,
+242,
-4
+25,
-4
1@@ -76,7 +76,7 @@ func (s *StorageFS) UpsertBucket(name string) (Bucket, error) {
2 return bucket, nil
3 }
4
5- dir := filepath.Join(s.Dir, bucket.Path)
6+ dir := filepath.Join(s.Dir, name)
7 s.Logger.Info("bucket not found, creating", "dir", dir, "err", err)
8 err = os.MkdirAll(dir, os.ModePerm)
9 if err != nil {
10@@ -91,6 +91,8 @@ func (s *StorageFS) GetBucketQuota(bucket Bucket) (uint64, error) {
11 return uint64(dsize), err
12 }
13
14+// DeleteBucket will delete all contents regardless if files exist inside of it.
15+// This is different from minio impl which requires all files be deleted first.
16 func (s *StorageFS) DeleteBucket(bucket Bucket) error {
17 return os.RemoveAll(bucket.Path)
18 }
19@@ -150,15 +152,34 @@ func (s *StorageFS) DeleteObject(bucket Bucket, fpath string) error {
20 return err
21 }
22
23- // try to remove dir if it is empty
24+ // traverse up the folder tree and remove all empty folders
25 dir := filepath.Dir(loc)
26- _ = os.Remove(dir)
27+ for dir != "" {
28+ err = os.Remove(dir)
29+ if err != nil {
30+ break
31+ }
32+ fp := strings.Split(dir, "/")
33+ dir = "/" + filepath.Join(fp[:len(fp)-1]...)
34+ }
35
36 return nil
37 }
38
39 func (s *StorageFS) ListBuckets() ([]string, error) {
40- return []string{}, fmt.Errorf("not implemented")
41+ entries, err := os.ReadDir(s.Dir)
42+ if err != nil {
43+ return []string{}, err
44+ }
45+
46+ buckets := []string{}
47+ for _, e := range entries {
48+ if !e.IsDir() {
49+ continue
50+ }
51+ buckets = append(buckets, e.Name())
52+ }
53+ return buckets, nil
54 }
55
56 func (s *StorageFS) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
1@@ -0,0 +1,217 @@
2+package storage
3+
4+import (
5+ "io"
6+ "io/fs"
7+ "log/slog"
8+ "os"
9+ "path/filepath"
10+ "strings"
11+ "testing"
12+ "time"
13+
14+ "github.com/google/go-cmp/cmp"
15+ "github.com/google/go-cmp/cmp/cmpopts"
16+ "github.com/picosh/pico/pkg/send/utils"
17+)
18+
19+func TestFsAdapter(t *testing.T) {
20+ logger := slog.Default()
21+ f, err := os.MkdirTemp("", "fs-tests-")
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ defer os.RemoveAll(f)
26+
27+ st, err := NewStorageFS(logger, f)
28+ if err != nil {
29+ t.Fatal(err)
30+ }
31+
32+ bucketName := "main"
33+ // create bucket
34+ bucket, err := st.UpsertBucket(bucketName)
35+ if err != nil {
36+ t.Fatal(err)
37+ }
38+
39+ // ensure bucket exists
40+ file, err := os.Stat(bucket.Path)
41+ if err != nil {
42+ t.Fatal(err)
43+ }
44+ if !file.IsDir() {
45+ t.Fatal("bucket must be directory")
46+ }
47+
48+ bucketCheck, err := st.GetBucket(bucketName)
49+ if err != nil {
50+ t.Fatal(err)
51+ }
52+ if bucketCheck.Path != bucket.Path || bucketCheck.Name != bucket.Name {
53+ t.Fatal("upsert and get bucket incongruent")
54+ }
55+
56+ modTime := time.Now()
57+
58+ str := "here is a test file"
59+ reader := strings.NewReader(str)
60+ actualPath, size, err := st.PutObject(bucket, "./nice/test.txt", reader, &utils.FileEntry{
61+ Mtime: modTime.Unix(),
62+ })
63+ if err != nil {
64+ t.Fatal(err)
65+ }
66+ if size != int64(len(str)) {
67+ t.Fatalf("size, actual: %d, expected: %d", size, int64(len(str)))
68+ }
69+ expectedPath := filepath.Join(bucket.Path, "nice", "test.txt")
70+ if actualPath != expectedPath {
71+ t.Fatalf("path, actual: %s, expected: %s", actualPath, expectedPath)
72+ }
73+
74+ // ensure file exists
75+ _, err = os.Stat(expectedPath)
76+ if err != nil {
77+ t.Fatal(err)
78+ }
79+
80+ // get file
81+ r, info, err := st.GetObject(bucket, "nice/test.txt")
82+ if err != nil {
83+ t.Fatal(err)
84+ }
85+ buf := new(strings.Builder)
86+ _, err = io.Copy(buf, r)
87+ if err != nil {
88+ t.Fatal(err)
89+ }
90+ actualStr := buf.String()
91+ if actualStr != str {
92+ t.Fatalf("contents, actual: %s, expected: %s", actualStr, str)
93+ }
94+ if info.Size != size {
95+ t.Fatalf("size, actual: %d, expected: %d", size, info.Size)
96+ }
97+
98+ str = "a deeply nested test file"
99+ reader = strings.NewReader(str)
100+ _, _, err = st.PutObject(bucket, "./here/we/go/again.txt", reader, &utils.FileEntry{
101+ Mtime: modTime.Unix(),
102+ })
103+ if err != nil {
104+ t.Fatal(err)
105+ }
106+
107+ // list objects
108+ objs, err := st.ListObjects(bucket, "/", true)
109+ if err != nil {
110+ t.Fatal(err)
111+ }
112+
113+ expectedObjs := []fs.FileInfo{
114+ &utils.VirtualFile{
115+ FName: "main",
116+ FIsDir: true,
117+ FSize: 80,
118+ },
119+ &utils.VirtualFile{
120+ FName: "here",
121+ FIsDir: true,
122+ FSize: 60,
123+ },
124+ &utils.VirtualFile{
125+ FName: "we",
126+ FIsDir: true,
127+ FSize: 60,
128+ },
129+ &utils.VirtualFile{
130+ FName: "go",
131+ FIsDir: true,
132+ FSize: 60,
133+ },
134+ &utils.VirtualFile{FName: "again.txt", FSize: 25},
135+ &utils.VirtualFile{
136+ FName: "nice",
137+ FIsDir: true,
138+ FSize: 60,
139+ },
140+ &utils.VirtualFile{FName: "test.txt", FSize: 19},
141+ }
142+ ignore := cmpopts.IgnoreFields(utils.VirtualFile{}, "FModTime")
143+ if cmp.Equal(objs, expectedObjs, ignore) == false {
144+ //nolint
145+ t.Fatal(cmp.Diff(objs, expectedObjs, ignore))
146+ }
147+
148+ // list buckets
149+ aBucket, _ := st.UpsertBucket("another")
150+ _, _ = st.UpsertBucket("and-another")
151+ buckets, err := st.ListBuckets()
152+ if err != nil {
153+ t.Fatal(err)
154+ }
155+ expectedBuckets := []string{"and-another", "another", "main"}
156+ if cmp.Equal(buckets, expectedBuckets) == false {
157+ //nolint
158+ t.Fatal(cmp.Diff(buckets, expectedBuckets))
159+ }
160+
161+ // delete bucket
162+ err = st.DeleteBucket(aBucket)
163+ if err != nil {
164+ t.Fatal(err)
165+ }
166+
167+ // ensure bucket was actually deleted
168+ _, err = os.Stat(aBucket.Path)
169+ if !os.IsNotExist(err) {
170+ t.Fatal("directory should have been deleted")
171+ }
172+
173+ err = st.DeleteObject(bucket, "nice/test.txt")
174+ if err != nil {
175+ t.Fatal(err)
176+ }
177+
178+ // ensure file was actually deleted
179+ _, err = os.Stat(filepath.Join(bucket.Path, "nice/test.txt"))
180+ if !os.IsNotExist(err) {
181+ t.Fatal("file should have been deleted")
182+ }
183+
184+ // ensure containing folder was also deleted
185+ _, err = os.Stat(filepath.Join(bucket.Path, "nice"))
186+ if !os.IsNotExist(err) {
187+ t.Fatal("containing folder should have been deleted")
188+ }
189+
190+ str = "a deeply nested test file"
191+ reader = strings.NewReader(str)
192+ _, _, err = st.PutObject(bucket, "./here/yes/we/can.txt", reader, &utils.FileEntry{
193+ Mtime: modTime.Unix(),
194+ })
195+ if err != nil {
196+ t.Fatal(err)
197+ }
198+
199+ // delete deeply nested file and all parent folders that are now empty
200+ err = st.DeleteObject(bucket, "here/yes/we/can.txt")
201+ if err != nil {
202+ t.Fatal(err)
203+ }
204+ _, err = os.Stat(filepath.Join(bucket.Path, "here"))
205+ if os.IsNotExist(err) {
206+ t.Fatal("this folder had multiple files and should not have been deleted")
207+ }
208+ _, err = os.Stat(filepath.Join(bucket.Path, "here/yes"))
209+ if !os.IsNotExist(err) {
210+ t.Fatal("containing folder should have been deleted")
211+ }
212+
213+ // delete bucket even with file contents
214+ err = st.DeleteBucket(bucket)
215+ if err != nil {
216+ t.Fatal(err)
217+ }
218+}