repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

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
M pkg/pobj/storage/fs.go
+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) {
A pkg/shared/storage/fs_test.go
+217, -0
  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+}