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}