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}