repos / pico

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

pico / pkg / shared / storage
Eric Bower  ·  2025-06-23

fs_test.go

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