repos / pico

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

pico / pkg / shared / storage
Eric Bower  ·  2025-04-22

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