Commit b229935

Eric Bower  ·  2026-03-28 20:53:04 -0400 EDT
parent abd75f3
refactor: merge pboj and shared/storage
19 files changed,  +557, -651
+2, -2
......@@ -13,8 +13,8 @@ import (
1313
1414 pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
1515 "github.com/picosh/pico/pkg/db"
16- sst "github.com/picosh/pico/pkg/pobj/storage"
1716 "github.com/picosh/pico/pkg/shared"
17+ "github.com/picosh/pico/pkg/shared/storage"
1818 )
1919
2020 func NewTabWriter(out io.Writer) *tabwriter.Writer {
......@@ -48,7 +48,7 @@ type Cmd struct {
4848 User *db.User
4949 Session shared.CmdSession
5050 Log *slog.Logger
51- Store sst.ObjectStorage
51+ Store storage.ObjectStorage
5252 Dbpool pgsdb.PgsDB
5353 Write bool
5454 Width int
+2, -2
......@@ -9,7 +9,7 @@ import (
99 "sort"
1010 "strings"
1111
12- sst "github.com/picosh/pico/pkg/pobj/storage"
12+ "github.com/picosh/pico/pkg/shared/storage"
1313 )
1414
1515 //go:embed html/*
......@@ -92,7 +92,7 @@ func toDisplayEntries(entries []os.FileInfo) []dirEntryDisplay {
9292 return displayEntries
9393 }
9494
95-func shouldGenerateListing(st sst.ObjectStorage, bucket sst.Bucket, projectDir string, path string) bool {
95+func shouldGenerateListing(st storage.ObjectStorage, bucket storage.Bucket, projectDir string, path string) bool {
9696 dirPath := projectDir + path
9797 if path == "/" {
9898 dirPath = projectDir + "/"
+3, -3
......@@ -6,8 +6,8 @@ import (
66 "testing"
77 "time"
88
9- sst "github.com/picosh/pico/pkg/pobj/storage"
109 "github.com/picosh/pico/pkg/send/utils"
10+ "github.com/picosh/pico/pkg/shared/storage"
1111 )
1212
1313 func TestGenerateDirectoryHTML(t *testing.T) {
......@@ -208,8 +208,8 @@ func TestShouldGenerateListing(t *testing.T) {
208208
209209 for _, fixture := range fixtures {
210210 t.Run(fixture.Name, func(t *testing.T) {
211- st, _ := sst.NewStorageMemory(fixture.Storage)
212- bucket := sst.Bucket{Name: "testbucket", Path: "testbucket"}
211+ st, _ := storage.NewStorageMemory(fixture.Storage)
212+ bucket := storage.Bucket{Name: "testbucket", Path: "testbucket"}
213213
214214 result := shouldGenerateListing(st, bucket, "project", fixture.Path)
215215
+6, -7
......@@ -17,11 +17,10 @@ import (
1717
1818 pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
1919 "github.com/picosh/pico/pkg/db"
20- "github.com/picosh/pico/pkg/pobj"
21- sst "github.com/picosh/pico/pkg/pobj/storage"
2220 "github.com/picosh/pico/pkg/pssh"
2321 sendutils "github.com/picosh/pico/pkg/send/utils"
2422 "github.com/picosh/pico/pkg/shared"
23+ "github.com/picosh/pico/pkg/shared/storage"
2524 ignore "github.com/sabhiram/go-gitignore"
2625 )
2726
......@@ -60,8 +59,8 @@ func setProject(s *pssh.SSHServerConnSession, project *db.Project) {
6059 s.SetValue(ctxProjectKey{}, project)
6160 }
6261
63-func getBucket(s *pssh.SSHServerConnSession) (sst.Bucket, error) {
64- bucket := s.Context().Value(ctxBucketKey{}).(sst.Bucket)
62+func getBucket(s *pssh.SSHServerConnSession) (storage.Bucket, error) {
63+ bucket := s.Context().Value(ctxBucketKey{}).(storage.Bucket)
6564 if bucket.Name == "" {
6665 return bucket, fmt.Errorf("bucket not set on `ssh.Context()` for connection")
6766 }
......@@ -92,7 +91,7 @@ func shouldIgnoreFile(fp, ignoreStr string) bool {
9291 type FileData struct {
9392 *sendutils.FileEntry
9493 User *db.User
95- Bucket sst.Bucket
94+ Bucket storage.Bucket
9695 Project *db.Project
9796 DenyList string
9897 }
......@@ -145,7 +144,7 @@ func (h *UploadAssetHandler) Read(s *pssh.SSHServerConnSession, entry *sendutils
145144 fileInfo.FSize = info.Size
146145 fileInfo.FModTime = info.LastModified
147146
148- reader := pobj.NewAllReaderAt(contents)
147+ reader := storage.NewAllReaderAt(contents)
149148
150149 return fileInfo, reader, nil
151150 }
......@@ -238,7 +237,7 @@ func (h *UploadAssetHandler) Validate(s *pssh.SSHServerConnSession) error {
238237 return nil
239238 }
240239
241-func (h *UploadAssetHandler) findDenylist(bucket sst.Bucket, project *db.Project, logger *slog.Logger) (string, error) {
240+func (h *UploadAssetHandler) findDenylist(bucket storage.Bucket, project *db.Project, logger *slog.Logger) (string, error) {
242241 fp, _, err := h.Cfg.Storage.GetObject(bucket, filepath.Join(project.ProjectDir, "_pgs_ignore"))
243242 if err != nil {
244243 return "", fmt.Errorf("_pgs_ignore not found")
+1, -2
......@@ -24,7 +24,6 @@ import (
2424 "github.com/gorilla/feeds"
2525 "github.com/hashicorp/golang-lru/v2/expirable"
2626 "github.com/picosh/pico/pkg/db"
27- sst "github.com/picosh/pico/pkg/pobj/storage"
2827 "github.com/picosh/pico/pkg/shared"
2928 "github.com/picosh/pico/pkg/shared/router"
3029 "github.com/picosh/pico/pkg/shared/storage"
......@@ -486,7 +485,7 @@ func (web *WebRouter) ServeAsset(fname string, opts *storage.ImgProcessOpts, has
486485 "userId", user.ID,
487486 )
488487
489- var bucket sst.Bucket
488+ var bucket storage.Bucket
490489 bucket, err = web.Cfg.Storage.GetBucket(shared.GetAssetBucketName(user.ID))
491490 project, perr := web.Cfg.DB.FindProjectByName(user.ID, props.ProjectName)
492491 if perr != nil {
+2, -3
......@@ -14,7 +14,6 @@ import (
1414 "net/http/httputil"
1515 _ "net/http/pprof"
1616
17- sst "github.com/picosh/pico/pkg/pobj/storage"
1817 "github.com/picosh/pico/pkg/shared/storage"
1918 )
2019
......@@ -27,7 +26,7 @@ type ApiAssetHandler struct {
2726 Subdomain string
2827 ProjectDir string
2928 Filepath string
30- Bucket sst.Bucket
29+ Bucket storage.Bucket
3130 ImgProcessOpts *storage.ImgProcessOpts
3231 ProjectID string
3332 HasPicoPlus bool
......@@ -87,7 +86,7 @@ func (h *ApiAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
8786
8887 var contents io.ReadCloser
8988 assetFilepath := ""
90- var info *sst.ObjectInfo
89+ var info *storage.ObjectInfo
9190 status := http.StatusOK
9291 attempts := []string{}
9392 for _, fp := range routes {
+2, -3
......@@ -11,7 +11,6 @@ import (
1111 "time"
1212
1313 pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
14- sst "github.com/picosh/pico/pkg/pobj/storage"
1514 "github.com/picosh/pico/pkg/shared"
1615 "github.com/picosh/pico/pkg/shared/storage"
1716 )
......@@ -481,10 +480,10 @@ type ImageStorageMemory struct {
481480 Fpath string
482481 }
483482
484-func (s *ImageStorageMemory) ServeObject(r *http.Request, bucket sst.Bucket, fpath string, opts *storage.ImgProcessOpts) (io.ReadCloser, *sst.ObjectInfo, error) {
483+func (s *ImageStorageMemory) ServeObject(r *http.Request, bucket storage.Bucket, fpath string, opts *storage.ImgProcessOpts) (io.ReadCloser, *storage.ObjectInfo, error) {
485484 s.Opts = opts
486485 s.Fpath = fpath
487- info := sst.ObjectInfo{
486+ info := storage.ObjectInfo{
488487 Metadata: make(http.Header),
489488 }
490489 info.Metadata.Set("content-type", "image/jpeg")
+2, -4
......@@ -13,8 +13,6 @@ import (
1313
1414 exifremove "github.com/neurosnap/go-exif-remove"
1515 "github.com/picosh/pico/pkg/db"
16- "github.com/picosh/pico/pkg/pobj"
17- sst "github.com/picosh/pico/pkg/pobj/storage"
1816 "github.com/picosh/pico/pkg/pssh"
1917 sendutils "github.com/picosh/pico/pkg/send/utils"
2018 "github.com/picosh/pico/pkg/shared"
......@@ -30,7 +28,7 @@ type PostMetaData struct {
3028 Filename string
3129 User *db.User
3230 FeatureFlag *db.FeatureFlag
33- Bucket sst.Bucket
31+ Bucket storage.Bucket
3432 }
3533
3634 type UploadImgHandler struct {
......@@ -125,7 +123,7 @@ func (h *UploadImgHandler) Read(s *pssh.SSHServerConnSession, entry *sendutils.F
125123 if err != nil {
126124 return nil, nil, err
127125 }
128- reader := pobj.NewAllReaderAt(contents)
126+ reader := storage.NewAllReaderAt(contents)
129127
130128 fileInfo := &sendutils.VirtualFile{
131129 FName: cleanFilename,
+0, -322
......@@ -1,322 +0,0 @@
1-package storage
2-
3-import (
4- "crypto/md5"
5- "encoding/hex"
6- "fmt"
7- "io"
8- "io/fs"
9- "log/slog"
10- "net/http"
11- "os"
12- "path"
13- "path/filepath"
14- "strings"
15- "time"
16-
17- "github.com/google/renameio/v2"
18- "github.com/picosh/pico/pkg/send/utils"
19- "github.com/picosh/pico/pkg/shared/mime"
20-)
21-
22-var KB = 1000
23-var MB = KB * 1000
24-
25-// https://stackoverflow.com/a/32482941
26-func dirSize(path string) (int64, error) {
27- var size int64
28- err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
29- if err != nil {
30- return err
31- }
32- if !info.IsDir() {
33- size += info.Size()
34- }
35- return err
36- })
37-
38- return size, err
39-}
40-
41-type StorageFS struct {
42- Dir string
43- Logger *slog.Logger
44-}
45-
46-var _ ObjectStorage = &StorageFS{}
47-var _ ObjectStorage = (*StorageFS)(nil)
48-
49-func NewStorageFS(logger *slog.Logger, dir string) (*StorageFS, error) {
50- return &StorageFS{Logger: logger, Dir: dir}, nil
51-}
52-
53-func (s *StorageFS) GetBucket(name string) (Bucket, error) {
54- dirPath := filepath.Join(s.Dir, name)
55- bucket := Bucket{
56- Name: name,
57- Path: dirPath,
58- }
59- // s.Logger.Info("get bucket", "dir", dirPath)
60-
61- info, err := os.Stat(dirPath)
62- if os.IsNotExist(err) {
63- return bucket, fmt.Errorf("directory does not exist: %v %w", dirPath, err)
64- }
65-
66- if err != nil {
67- return bucket, fmt.Errorf("directory error: %v %w", dirPath, err)
68-
69- }
70-
71- if !info.IsDir() {
72- return bucket, fmt.Errorf("directory is a file, not a directory: %#v", dirPath)
73- }
74-
75- return bucket, nil
76-}
77-
78-func (s *StorageFS) UpsertBucket(name string) (Bucket, error) {
79- s.Logger.Info("upsert bucket", "name", name)
80- bucket, err := s.GetBucket(name)
81- if err == nil {
82- return bucket, nil
83- }
84-
85- dir := filepath.Join(s.Dir, name)
86- s.Logger.Info("bucket not found, creating", "dir", dir, "err", err)
87- err = os.MkdirAll(dir, os.ModePerm)
88- if err != nil {
89- return bucket, err
90- }
91-
92- return bucket, nil
93-}
94-
95-func (s *StorageFS) GetBucketQuota(bucket Bucket) (uint64, error) {
96- dsize, err := dirSize(bucket.Path)
97- return uint64(dsize), err
98-}
99-
100-// DeleteBucket will delete all contents regardless if files exist inside of it.
101-func (s *StorageFS) DeleteBucket(bucket Bucket) error {
102- return os.RemoveAll(bucket.Path)
103-}
104-
105-func (s *StorageFS) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
106- objInfo := &ObjectInfo{
107- Size: 0,
108- LastModified: time.Time{},
109- Metadata: make(http.Header),
110- ETag: "",
111- }
112-
113- dat, err := os.Open(filepath.Join(bucket.Path, fpath))
114- if err != nil {
115- return nil, objInfo, err
116- }
117-
118- info, err := dat.Stat()
119- if err != nil {
120- _ = dat.Close()
121- return nil, objInfo, err
122- }
123-
124- etag := ""
125- // only generate etag if file is less than 10MB
126- if info.Size() <= int64(10*MB) {
127- // calculate etag
128- h := md5.New()
129- if _, err := io.Copy(h, dat); err != nil {
130- _ = dat.Close()
131- return nil, objInfo, err
132- }
133- md5Sum := h.Sum(nil)
134- etag = hex.EncodeToString(md5Sum)
135-
136- // reset os.File reader
137- _, err = dat.Seek(0, io.SeekStart)
138- if err != nil {
139- _ = dat.Close()
140- return nil, objInfo, err
141- }
142- }
143-
144- objInfo.ETag = etag
145- objInfo.Size = info.Size()
146- objInfo.LastModified = info.ModTime()
147- objInfo.Metadata.Set("content-type", mime.GetMimeType(fpath))
148- return dat, objInfo, nil
149-}
150-
151-func (s *StorageFS) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
152- loc := filepath.Join(bucket.Path, fpath)
153- err := os.MkdirAll(filepath.Dir(loc), os.ModePerm)
154- if err != nil {
155- return "", 0, err
156- }
157- out, err := renameio.NewPendingFile(loc, renameio.WithPermissions(os.ModePerm))
158- if err != nil {
159- return "", 0, err
160- }
161-
162- size, err := io.Copy(out, contents)
163- if err != nil {
164- return "", 0, err
165- }
166-
167- if err := out.CloseAtomicallyReplace(); err != nil {
168- return "", 0, err
169- }
170-
171- if entry.Mtime > 0 {
172- uTime := time.Unix(entry.Mtime, 0)
173- _ = os.Chtimes(loc, uTime, uTime)
174- }
175-
176- return loc, size, nil
177-}
178-
179-func (s *StorageFS) DeleteObject(bucket Bucket, fpath string) error {
180- loc := filepath.Join(bucket.Path, fpath)
181- err := os.Remove(loc)
182- if err != nil {
183- if os.IsNotExist(err) {
184- return nil
185- }
186- return err
187- }
188-
189- // traverse up the folder tree and remove all empty folders
190- dir := filepath.Dir(loc)
191- for dir != "" {
192- f, err := os.Open(dir)
193- if err != nil {
194- s.Logger.Info("open dir", "dir", dir, "err", err)
195- break
196- }
197- defer func() {
198- _ = f.Close()
199- }()
200-
201- // https://stackoverflow.com/a/30708914
202- contents, err := f.Readdirnames(-1)
203- if err != nil {
204- s.Logger.Info("read dir", "dir", dir, "err", err)
205- break
206- }
207- if len(contents) > 0 {
208- break
209- }
210-
211- err = os.Remove(dir)
212- if err != nil {
213- s.Logger.Info("remove dir", "dir", dir, "err", err)
214- break
215- }
216- fp := strings.Split(dir, "/")
217- prefix := ""
218- if strings.HasPrefix(loc, "/") {
219- prefix = "/"
220- }
221- dir = prefix + filepath.Join(fp[:len(fp)-1]...)
222- }
223-
224- return nil
225-}
226-
227-func (s *StorageFS) ListBuckets() ([]string, error) {
228- entries, err := os.ReadDir(s.Dir)
229- if err != nil {
230- return []string{}, err
231- }
232-
233- buckets := []string{}
234- for _, e := range entries {
235- if !e.IsDir() {
236- continue
237- }
238- buckets = append(buckets, e.Name())
239- }
240- return buckets, nil
241-}
242-
243-func (s *StorageFS) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
244- fileList := []os.FileInfo{}
245-
246- fpath := path.Join(bucket.Path, dir)
247-
248- info, err := os.Stat(fpath)
249- if err != nil {
250- if os.IsNotExist(err) {
251- return fileList, nil
252- }
253- return fileList, err
254- }
255-
256- if info.IsDir() && !strings.HasSuffix(dir, "/") {
257- fileList = append(fileList, &utils.VirtualFile{
258- FName: "",
259- FIsDir: info.IsDir(),
260- FSize: info.Size(),
261- FModTime: info.ModTime(),
262- })
263-
264- return fileList, err
265- }
266-
267- var files []utils.VirtualFile
268-
269- if recursive {
270- err = filepath.WalkDir(fpath, func(s string, d fs.DirEntry, err error) error {
271- if err != nil {
272- return err
273- }
274- info, err := d.Info()
275- if err != nil {
276- return nil
277- }
278- fname := strings.TrimPrefix(s, fpath)
279- if fname == "" {
280- return nil
281- }
282- // rsync does not expect prefixed `/` so without this `rsync --delete` is borked
283- fname = strings.TrimPrefix(fname, "/")
284- files = append(files, utils.VirtualFile{
285- FName: fname,
286- FIsDir: info.IsDir(),
287- FSize: info.Size(),
288- FModTime: info.ModTime(),
289- })
290- return nil
291- })
292- if err != nil {
293- fileList = append(fileList, info)
294- return fileList, nil
295- }
296- } else {
297- fls, err := os.ReadDir(fpath)
298- if err != nil {
299- fileList = append(fileList, info)
300- return fileList, nil
301- }
302- for _, d := range fls {
303- info, err := d.Info()
304- if err != nil {
305- continue
306- }
307- fp := info.Name()
308- files = append(files, utils.VirtualFile{
309- FName: fp,
310- FIsDir: info.IsDir(),
311- FSize: info.Size(),
312- FModTime: info.ModTime(),
313- })
314- }
315- }
316-
317- for _, f := range files {
318- fileList = append(fileList, &f)
319- }
320-
321- return fileList, err
322-}
+0, -207
......@@ -1,207 +0,0 @@
1-package storage
2-
3-import (
4- "fmt"
5- "io"
6- "os"
7- "path/filepath"
8- "strings"
9- "sync"
10- "time"
11-
12- "github.com/picosh/pico/pkg/send/utils"
13-)
14-
15-type StorageMemory struct {
16- storage map[string]map[string]string
17- mu sync.RWMutex
18-}
19-
20-var _ ObjectStorage = &StorageMemory{}
21-var _ ObjectStorage = (*StorageMemory)(nil)
22-
23-func NewStorageMemory(st map[string]map[string]string) (*StorageMemory, error) {
24- return &StorageMemory{
25- storage: st,
26- }, nil
27-}
28-
29-func (s *StorageMemory) GetBucket(name string) (Bucket, error) {
30- s.mu.RLock()
31- defer s.mu.RUnlock()
32-
33- bucket := Bucket{
34- Name: name,
35- Path: name,
36- }
37-
38- _, ok := s.storage[name]
39- if !ok {
40- return bucket, fmt.Errorf("bucket does not exist")
41- }
42-
43- return bucket, nil
44-}
45-
46-func (s *StorageMemory) UpsertBucket(name string) (Bucket, error) {
47- bucket, err := s.GetBucket(name)
48- if err == nil {
49- return bucket, nil
50- }
51-
52- s.mu.Lock()
53- defer s.mu.Unlock()
54-
55- s.storage[name] = map[string]string{}
56- return bucket, nil
57-}
58-
59-func (s *StorageMemory) GetBucketQuota(bucket Bucket) (uint64, error) {
60- s.mu.RLock()
61- defer s.mu.RUnlock()
62-
63- objects := s.storage[bucket.Path]
64- size := 0
65- for _, val := range objects {
66- size += len([]byte(val))
67- }
68- return uint64(size), nil
69-}
70-
71-func (s *StorageMemory) DeleteBucket(bucket Bucket) error {
72- s.mu.Lock()
73- defer s.mu.Unlock()
74-
75- delete(s.storage, bucket.Path)
76- return nil
77-}
78-
79-func (s *StorageMemory) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
80- s.mu.RLock()
81- defer s.mu.RUnlock()
82-
83- if !strings.HasPrefix(fpath, "/") {
84- fpath = "/" + fpath
85- }
86-
87- objInfo := &ObjectInfo{
88- LastModified: time.Time{},
89- Metadata: nil,
90- }
91-
92- dat, ok := s.storage[bucket.Path][fpath]
93- if !ok {
94- return nil, objInfo, fmt.Errorf("object does not exist: %s", fpath)
95- }
96-
97- objInfo.Size = int64(len([]byte(dat)))
98- reader := utils.NopReadAndReaderAtCloser(strings.NewReader(dat))
99- return reader, objInfo, nil
100-}
101-
102-func (s *StorageMemory) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
103- s.mu.Lock()
104- defer s.mu.Unlock()
105-
106- d, err := io.ReadAll(contents)
107- if err != nil {
108- return "", 0, err
109- }
110-
111- s.storage[bucket.Path][fpath] = string(d)
112- return fmt.Sprintf("%s%s", bucket.Path, fpath), int64(len(d)), nil
113-}
114-
115-func (s *StorageMemory) DeleteObject(bucket Bucket, fpath string) error {
116- s.mu.Lock()
117- defer s.mu.Unlock()
118-
119- delete(s.storage[bucket.Path], fpath)
120- return nil
121-}
122-
123-func (s *StorageMemory) ListBuckets() ([]string, error) {
124- s.mu.RLock()
125- defer s.mu.RUnlock()
126-
127- buckets := []string{}
128- for key := range s.storage {
129- buckets = append(buckets, key)
130- }
131- return buckets, nil
132-}
133-
134-func (s *StorageMemory) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
135- s.mu.RLock()
136- defer s.mu.RUnlock()
137-
138- var fileList []os.FileInfo
139-
140- resolved := dir
141-
142- if !strings.HasPrefix(resolved, "/") {
143- resolved = "/" + resolved
144- }
145-
146- objects := s.storage[bucket.Path]
147- // dir is actually an object
148- oval, ok := objects[resolved]
149- if ok {
150- fileList = append(fileList, &utils.VirtualFile{
151- FName: filepath.Base(resolved),
152- FIsDir: false,
153- FSize: int64(len([]byte(oval))),
154- FModTime: time.Time{},
155- })
156- return fileList, nil
157- }
158-
159- for key, val := range objects {
160- if !strings.HasPrefix(key, resolved) {
161- continue
162- }
163-
164- rep := strings.Replace(key, resolved, "", 1)
165- fdir := filepath.Dir(rep)
166- fname := filepath.Base(rep)
167- paths := strings.Split(fdir, "/")
168-
169- if fdir == "/" {
170- ffname := filepath.Base(resolved)
171- fileList = append(fileList, &utils.VirtualFile{
172- FName: ffname,
173- FIsDir: true,
174- })
175- }
176-
177- for _, p := range paths {
178- if p == "" || p == "/" || p == "." {
179- continue
180- }
181- fileList = append(fileList, &utils.VirtualFile{
182- FName: p,
183- FIsDir: true,
184- })
185- }
186-
187- trimRes := strings.TrimSuffix(resolved, "/")
188- dirKey := filepath.Dir(key)
189- if recursive {
190- fileList = append(fileList, &utils.VirtualFile{
191- FName: fname,
192- FIsDir: false,
193- FSize: int64(len([]byte(val))),
194- FModTime: time.Time{},
195- })
196- } else if resolved == dirKey || trimRes == dirKey {
197- fileList = append(fileList, &utils.VirtualFile{
198- FName: fname,
199- FIsDir: false,
200- FSize: int64(len([]byte(val))),
201- FModTime: time.Time{},
202- })
203- }
204- }
205-
206- return fileList, nil
207-}
+0, -36
......@@ -1,36 +0,0 @@
1-package storage
2-
3-import (
4- "io"
5- "net/http"
6- "os"
7- "time"
8-
9- "github.com/picosh/pico/pkg/send/utils"
10-)
11-
12-type Bucket struct {
13- Name string
14- Path string
15- Root string
16-}
17-
18-type ObjectStorage interface {
19- GetBucket(name string) (Bucket, error)
20- GetBucketQuota(bucket Bucket) (uint64, error)
21- UpsertBucket(name string) (Bucket, error)
22- ListBuckets() ([]string, error)
23- DeleteBucket(bucket Bucket) error
24-
25- GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error)
26- PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error)
27- DeleteObject(bucket Bucket, fpath string) error
28- ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error)
29-}
30-
31-type ObjectInfo struct {
32- Size int64
33- LastModified time.Time
34- ETag string
35- Metadata http.Header
36-}
+0, -30
......@@ -1,30 +0,0 @@
1-package pobj
2-
3-import (
4- "log/slog"
5- "os"
6-
7- "github.com/picosh/pico/pkg/pobj/storage"
8-)
9-
10-func GetEnv(key string, defaultVal string) string {
11- if value, exists := os.LookupEnv(key); exists {
12- return value
13- }
14- return defaultVal
15-}
16-
17-func EnvDriverDetector(logger *slog.Logger) (storage.ObjectStorage, error) {
18- driver := GetEnv("OBJECT_DRIVER", "fs")
19- logger.Info("driver detected", "driver", driver)
20-
21- switch driver {
22- case "memory":
23- return storage.NewStorageMemory(map[string]map[string]string{})
24- }
25-
26- // implied driver == "fs"
27- storageDir := GetEnv("OBJECT_URL", "./.storage")
28- logger.Info("object config detected", "dir", storageDir)
29- return storage.NewStorageFS(logger, storageDir)
30-}
R pkg/pobj/asset.go => pkg/shared/storage/asset.go
+1, -1
......@@ -1,4 +1,4 @@
1-package pobj
1+package storage
22
33 import (
44 "fmt"
+302, -7
......@@ -1,34 +1,329 @@
11 package storage
22
33 import (
4+ "crypto/md5"
5+ "encoding/hex"
46 "fmt"
57 "io"
8+ "io/fs"
69 "log/slog"
710 "net/http"
811 "os"
12+ "path"
913 "path/filepath"
1014 "strings"
15+ "time"
1116
12- sst "github.com/picosh/pico/pkg/pobj/storage"
17+ "github.com/google/renameio/v2"
18+ "github.com/picosh/pico/pkg/send/utils"
1319 "github.com/picosh/pico/pkg/shared/mime"
1420 )
1521
22+var KB = 1000
23+var MB = KB * 1000
24+
25+// https://stackoverflow.com/a/32482941
26+func dirSize(path string) (int64, error) {
27+ var size int64
28+ err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
29+ if err != nil {
30+ return err
31+ }
32+ if !info.IsDir() {
33+ size += info.Size()
34+ }
35+ return err
36+ })
37+
38+ return size, err
39+}
40+
1641 type StorageFS struct {
17- *sst.StorageFS
42+ Dir string
1843 Logger *slog.Logger
1944 }
2045
46+var _ ObjectStorage = &StorageFS{}
47+var _ ObjectStorage = (*StorageFS)(nil)
48+
2149 func NewStorageFS(logger *slog.Logger, dir string) (*StorageFS, error) {
22- st, err := sst.NewStorageFS(logger, dir)
50+ return &StorageFS{Logger: logger, Dir: dir}, nil
51+}
52+
53+func (s *StorageFS) GetBucket(name string) (Bucket, error) {
54+ dirPath := filepath.Join(s.Dir, name)
55+ bucket := Bucket{
56+ Name: name,
57+ Path: dirPath,
58+ }
59+ // s.Logger.Info("get bucket", "dir", dirPath)
60+
61+ info, err := os.Stat(dirPath)
62+ if os.IsNotExist(err) {
63+ return bucket, fmt.Errorf("directory does not exist: %v %w", dirPath, err)
64+ }
65+
66+ if err != nil {
67+ return bucket, fmt.Errorf("directory error: %v %w", dirPath, err)
68+
69+ }
70+
71+ if !info.IsDir() {
72+ return bucket, fmt.Errorf("directory is a file, not a directory: %#v", dirPath)
73+ }
74+
75+ return bucket, nil
76+}
77+
78+func (s *StorageFS) UpsertBucket(name string) (Bucket, error) {
79+ s.Logger.Info("upsert bucket", "name", name)
80+ bucket, err := s.GetBucket(name)
81+ if err == nil {
82+ return bucket, nil
83+ }
84+
85+ dir := filepath.Join(s.Dir, name)
86+ s.Logger.Info("bucket not found, creating", "dir", dir, "err", err)
87+ err = os.MkdirAll(dir, os.ModePerm)
88+ if err != nil {
89+ return bucket, err
90+ }
91+
92+ return bucket, nil
93+}
94+
95+func (s *StorageFS) GetBucketQuota(bucket Bucket) (uint64, error) {
96+ dsize, err := dirSize(bucket.Path)
97+ return uint64(dsize), err
98+}
99+
100+// DeleteBucket will delete all contents regardless if files exist inside of it.
101+func (s *StorageFS) DeleteBucket(bucket Bucket) error {
102+ return os.RemoveAll(bucket.Path)
103+}
104+
105+func (s *StorageFS) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
106+ objInfo := &ObjectInfo{
107+ Size: 0,
108+ LastModified: time.Time{},
109+ Metadata: make(http.Header),
110+ ETag: "",
111+ }
112+
113+ dat, err := os.Open(filepath.Join(bucket.Path, fpath))
114+ if err != nil {
115+ return nil, objInfo, err
116+ }
117+
118+ info, err := dat.Stat()
23119 if err != nil {
24- return nil, err
120+ _ = dat.Close()
121+ return nil, objInfo, err
25122 }
26- return &StorageFS{st, logger}, nil
123+
124+ etag := ""
125+ // only generate etag if file is less than 10MB
126+ if info.Size() <= int64(10*MB) {
127+ // calculate etag
128+ h := md5.New()
129+ if _, err := io.Copy(h, dat); err != nil {
130+ _ = dat.Close()
131+ return nil, objInfo, err
132+ }
133+ md5Sum := h.Sum(nil)
134+ etag = hex.EncodeToString(md5Sum)
135+
136+ // reset os.File reader
137+ _, err = dat.Seek(0, io.SeekStart)
138+ if err != nil {
139+ _ = dat.Close()
140+ return nil, objInfo, err
141+ }
142+ }
143+
144+ objInfo.ETag = etag
145+ objInfo.Size = info.Size()
146+ objInfo.LastModified = info.ModTime()
147+ objInfo.Metadata.Set("content-type", mime.GetMimeType(fpath))
148+ return dat, objInfo, nil
149+}
150+
151+func (s *StorageFS) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
152+ loc := filepath.Join(bucket.Path, fpath)
153+ err := os.MkdirAll(filepath.Dir(loc), os.ModePerm)
154+ if err != nil {
155+ return "", 0, err
156+ }
157+ out, err := renameio.NewPendingFile(loc, renameio.WithPermissions(os.ModePerm))
158+ if err != nil {
159+ return "", 0, err
160+ }
161+
162+ size, err := io.Copy(out, contents)
163+ if err != nil {
164+ return "", 0, err
165+ }
166+
167+ if err := out.CloseAtomicallyReplace(); err != nil {
168+ return "", 0, err
169+ }
170+
171+ if entry.Mtime > 0 {
172+ uTime := time.Unix(entry.Mtime, 0)
173+ _ = os.Chtimes(loc, uTime, uTime)
174+ }
175+
176+ return loc, size, nil
177+}
178+
179+func (s *StorageFS) DeleteObject(bucket Bucket, fpath string) error {
180+ loc := filepath.Join(bucket.Path, fpath)
181+ err := os.Remove(loc)
182+ if err != nil {
183+ if os.IsNotExist(err) {
184+ return nil
185+ }
186+ return err
187+ }
188+
189+ // traverse up the folder tree and remove all empty folders
190+ dir := filepath.Dir(loc)
191+ for dir != "" {
192+ f, err := os.Open(dir)
193+ if err != nil {
194+ s.Logger.Info("open dir", "dir", dir, "err", err)
195+ break
196+ }
197+ defer func() {
198+ _ = f.Close()
199+ }()
200+
201+ // https://stackoverflow.com/a/30708914
202+ contents, err := f.Readdirnames(-1)
203+ if err != nil {
204+ s.Logger.Info("read dir", "dir", dir, "err", err)
205+ break
206+ }
207+ if len(contents) > 0 {
208+ break
209+ }
210+
211+ err = os.Remove(dir)
212+ if err != nil {
213+ s.Logger.Info("remove dir", "dir", dir, "err", err)
214+ break
215+ }
216+ fp := strings.Split(dir, "/")
217+ prefix := ""
218+ if strings.HasPrefix(loc, "/") {
219+ prefix = "/"
220+ }
221+ dir = prefix + filepath.Join(fp[:len(fp)-1]...)
222+ }
223+
224+ return nil
225+}
226+
227+func (s *StorageFS) ListBuckets() ([]string, error) {
228+ entries, err := os.ReadDir(s.Dir)
229+ if err != nil {
230+ return []string{}, err
231+ }
232+
233+ buckets := []string{}
234+ for _, e := range entries {
235+ if !e.IsDir() {
236+ continue
237+ }
238+ buckets = append(buckets, e.Name())
239+ }
240+ return buckets, nil
241+}
242+
243+func (s *StorageFS) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
244+ fileList := []os.FileInfo{}
245+
246+ fpath := path.Join(bucket.Path, dir)
247+
248+ info, err := os.Stat(fpath)
249+ if err != nil {
250+ if os.IsNotExist(err) {
251+ return fileList, nil
252+ }
253+ return fileList, err
254+ }
255+
256+ if info.IsDir() && !strings.HasSuffix(dir, "/") {
257+ fileList = append(fileList, &utils.VirtualFile{
258+ FName: "",
259+ FIsDir: info.IsDir(),
260+ FSize: info.Size(),
261+ FModTime: info.ModTime(),
262+ })
263+
264+ return fileList, err
265+ }
266+
267+ var files []utils.VirtualFile
268+
269+ if recursive {
270+ err = filepath.WalkDir(fpath, func(s string, d fs.DirEntry, err error) error {
271+ if err != nil {
272+ return err
273+ }
274+ info, err := d.Info()
275+ if err != nil {
276+ return nil
277+ }
278+ fname := strings.TrimPrefix(s, fpath)
279+ if fname == "" {
280+ return nil
281+ }
282+ // rsync does not expect prefixed `/` so without this `rsync --delete` is borked
283+ fname = strings.TrimPrefix(fname, "/")
284+ files = append(files, utils.VirtualFile{
285+ FName: fname,
286+ FIsDir: info.IsDir(),
287+ FSize: info.Size(),
288+ FModTime: info.ModTime(),
289+ })
290+ return nil
291+ })
292+ if err != nil {
293+ fileList = append(fileList, info)
294+ return fileList, nil
295+ }
296+ } else {
297+ fls, err := os.ReadDir(fpath)
298+ if err != nil {
299+ fileList = append(fileList, info)
300+ return fileList, nil
301+ }
302+ for _, d := range fls {
303+ info, err := d.Info()
304+ if err != nil {
305+ continue
306+ }
307+ fp := info.Name()
308+ files = append(files, utils.VirtualFile{
309+ FName: fp,
310+ FIsDir: info.IsDir(),
311+ FSize: info.Size(),
312+ FModTime: info.ModTime(),
313+ })
314+ }
315+ }
316+
317+ for _, f := range files {
318+ fileList = append(fileList, &f)
319+ }
320+
321+ return fileList, err
27322 }
28323
29-func (s *StorageFS) ServeObject(r *http.Request, bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, *sst.ObjectInfo, error) {
324+func (s *StorageFS) ServeObject(r *http.Request, bucket Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, *ObjectInfo, error) {
30325 var rc io.ReadCloser
31- info := &sst.ObjectInfo{}
326+ info := &ObjectInfo{}
32327 var err error
33328 mimeType := mime.GetMimeType(fpath)
34329 if !strings.HasPrefix(mimeType, "image/") || opts == nil || os.Getenv("IMGPROXY_URL") == "" {
R pkg/pobj/handler.go => pkg/shared/storage/handler.go
+6, -7
......@@ -1,4 +1,4 @@
1-package pobj
1+package storage
22
33 import (
44 "bytes"
......@@ -10,15 +10,14 @@ import (
1010 "path/filepath"
1111 "time"
1212
13- "github.com/picosh/pico/pkg/pobj/storage"
1413 "github.com/picosh/pico/pkg/pssh"
1514 "github.com/picosh/pico/pkg/send/utils"
1615 )
1716
1817 type ctxBucketKey struct{}
1918
20-func getBucket(ctx *pssh.SSHServerConnSession) (storage.Bucket, error) {
21- bucket, ok := ctx.Value(ctxBucketKey{}).(storage.Bucket)
19+func getBucket(ctx *pssh.SSHServerConnSession) (Bucket, error) {
20+ bucket, ok := ctx.Value(ctxBucketKey{}).(Bucket)
2221 if !ok {
2322 return bucket, fmt.Errorf("bucket not set on `ssh.Context()` for connection")
2423 }
......@@ -27,7 +26,7 @@ func getBucket(ctx *pssh.SSHServerConnSession) (storage.Bucket, error) {
2726 }
2827 return bucket, nil
2928 }
30-func setBucket(ctx *pssh.SSHServerConnSession, bucket storage.Bucket) {
29+func setBucket(ctx *pssh.SSHServerConnSession, bucket Bucket) {
3130 ctx.SetValue(ctxBucketKey{}, bucket)
3231 }
3332
......@@ -35,12 +34,12 @@ type FileData struct {
3534 *utils.FileEntry
3635 Text []byte
3736 User string
38- Bucket storage.Bucket
37+ Bucket Bucket
3938 }
4039
4140 type Config struct {
4241 Logger *slog.Logger
43- Storage storage.ObjectStorage
42+ Storage ObjectStorage
4443 AssetNames AssetNames
4544 }
4645
+194, -7
......@@ -1,27 +1,214 @@
11 package storage
22
33 import (
4+ "fmt"
45 "io"
56 "net/http"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "sync"
611 "time"
712
8- sst "github.com/picosh/pico/pkg/pobj/storage"
13+ "github.com/picosh/pico/pkg/send/utils"
914 "github.com/picosh/pico/pkg/shared/mime"
1015 )
1116
1217 type StorageMemory struct {
13- *sst.StorageMemory
18+ storage map[string]map[string]string
19+ mu sync.RWMutex
1420 }
1521
16-func NewStorageMemory(sto map[string]map[string]string) (*StorageMemory, error) {
17- st, err := sst.NewStorageMemory(sto)
22+var _ ObjectStorage = &StorageMemory{}
23+var _ ObjectStorage = (*StorageMemory)(nil)
24+
25+func NewStorageMemory(st map[string]map[string]string) (*StorageMemory, error) {
26+ return &StorageMemory{
27+ storage: st,
28+ }, nil
29+}
30+
31+func (s *StorageMemory) GetBucket(name string) (Bucket, error) {
32+ s.mu.RLock()
33+ defer s.mu.RUnlock()
34+
35+ bucket := Bucket{
36+ Name: name,
37+ Path: name,
38+ }
39+
40+ _, ok := s.storage[name]
41+ if !ok {
42+ return bucket, fmt.Errorf("bucket does not exist")
43+ }
44+
45+ return bucket, nil
46+}
47+
48+func (s *StorageMemory) UpsertBucket(name string) (Bucket, error) {
49+ bucket, err := s.GetBucket(name)
50+ if err == nil {
51+ return bucket, nil
52+ }
53+
54+ s.mu.Lock()
55+ defer s.mu.Unlock()
56+
57+ s.storage[name] = map[string]string{}
58+ return bucket, nil
59+}
60+
61+func (s *StorageMemory) GetBucketQuota(bucket Bucket) (uint64, error) {
62+ s.mu.RLock()
63+ defer s.mu.RUnlock()
64+
65+ objects := s.storage[bucket.Path]
66+ size := 0
67+ for _, val := range objects {
68+ size += len([]byte(val))
69+ }
70+ return uint64(size), nil
71+}
72+
73+func (s *StorageMemory) DeleteBucket(bucket Bucket) error {
74+ s.mu.Lock()
75+ defer s.mu.Unlock()
76+
77+ delete(s.storage, bucket.Path)
78+ return nil
79+}
80+
81+func (s *StorageMemory) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
82+ s.mu.RLock()
83+ defer s.mu.RUnlock()
84+
85+ if !strings.HasPrefix(fpath, "/") {
86+ fpath = "/" + fpath
87+ }
88+
89+ objInfo := &ObjectInfo{
90+ LastModified: time.Time{},
91+ Metadata: nil,
92+ }
93+
94+ dat, ok := s.storage[bucket.Path][fpath]
95+ if !ok {
96+ return nil, objInfo, fmt.Errorf("object does not exist: %s", fpath)
97+ }
98+
99+ objInfo.Size = int64(len([]byte(dat)))
100+ reader := utils.NopReadAndReaderAtCloser(strings.NewReader(dat))
101+ return reader, objInfo, nil
102+}
103+
104+func (s *StorageMemory) PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error) {
105+ s.mu.Lock()
106+ defer s.mu.Unlock()
107+
108+ d, err := io.ReadAll(contents)
18109 if err != nil {
19- return nil, err
110+ return "", 0, err
111+ }
112+
113+ s.storage[bucket.Path][fpath] = string(d)
114+ return fmt.Sprintf("%s%s", bucket.Path, fpath), int64(len(d)), nil
115+}
116+
117+func (s *StorageMemory) DeleteObject(bucket Bucket, fpath string) error {
118+ s.mu.Lock()
119+ defer s.mu.Unlock()
120+
121+ delete(s.storage[bucket.Path], fpath)
122+ return nil
123+}
124+
125+func (s *StorageMemory) ListBuckets() ([]string, error) {
126+ s.mu.RLock()
127+ defer s.mu.RUnlock()
128+
129+ buckets := []string{}
130+ for key := range s.storage {
131+ buckets = append(buckets, key)
20132 }
21- return &StorageMemory{st}, nil
133+ return buckets, nil
134+}
135+
136+func (s *StorageMemory) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
137+ s.mu.RLock()
138+ defer s.mu.RUnlock()
139+
140+ var fileList []os.FileInfo
141+
142+ resolved := dir
143+
144+ if !strings.HasPrefix(resolved, "/") {
145+ resolved = "/" + resolved
146+ }
147+
148+ objects := s.storage[bucket.Path]
149+ // dir is actually an object
150+ oval, ok := objects[resolved]
151+ if ok {
152+ fileList = append(fileList, &utils.VirtualFile{
153+ FName: filepath.Base(resolved),
154+ FIsDir: false,
155+ FSize: int64(len([]byte(oval))),
156+ FModTime: time.Time{},
157+ })
158+ return fileList, nil
159+ }
160+
161+ for key, val := range objects {
162+ if !strings.HasPrefix(key, resolved) {
163+ continue
164+ }
165+
166+ rep := strings.Replace(key, resolved, "", 1)
167+ fdir := filepath.Dir(rep)
168+ fname := filepath.Base(rep)
169+ paths := strings.Split(fdir, "/")
170+
171+ if fdir == "/" {
172+ ffname := filepath.Base(resolved)
173+ fileList = append(fileList, &utils.VirtualFile{
174+ FName: ffname,
175+ FIsDir: true,
176+ })
177+ }
178+
179+ for _, p := range paths {
180+ if p == "" || p == "/" || p == "." {
181+ continue
182+ }
183+ fileList = append(fileList, &utils.VirtualFile{
184+ FName: p,
185+ FIsDir: true,
186+ })
187+ }
188+
189+ trimRes := strings.TrimSuffix(resolved, "/")
190+ dirKey := filepath.Dir(key)
191+ if recursive {
192+ fileList = append(fileList, &utils.VirtualFile{
193+ FName: fname,
194+ FIsDir: false,
195+ FSize: int64(len([]byte(val))),
196+ FModTime: time.Time{},
197+ })
198+ } else if resolved == dirKey || trimRes == dirKey {
199+ fileList = append(fileList, &utils.VirtualFile{
200+ FName: fname,
201+ FIsDir: false,
202+ FSize: int64(len([]byte(val))),
203+ FModTime: time.Time{},
204+ })
205+ }
206+ }
207+
208+ return fileList, nil
22209 }
23210
24-func (s *StorageMemory) ServeObject(r *http.Request, bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, *sst.ObjectInfo, error) {
211+func (s *StorageMemory) ServeObject(r *http.Request, bucket Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, *ObjectInfo, error) {
25212 obj, info, err := s.GetObject(bucket, fpath)
26213 if info.Metadata == nil {
27214 info.Metadata = make(http.Header)
+2, -4
......@@ -13,8 +13,6 @@ import (
1313 "strconv"
1414 "strings"
1515 "time"
16-
17- "github.com/picosh/pico/pkg/pobj/storage"
1816 )
1917
2018 func UriToImgProcessOpts(uri string) (*ImgProcessOpts, error) {
......@@ -133,7 +131,7 @@ func (img *ImgProcessOpts) String() string {
133131 return processOpts
134132 }
135133
136-func HandleProxy(r *http.Request, logger *slog.Logger, dataURL string, opts *ImgProcessOpts) (io.ReadCloser, *storage.ObjectInfo, error) {
134+func HandleProxy(r *http.Request, logger *slog.Logger, dataURL string, opts *ImgProcessOpts) (io.ReadCloser, *ObjectInfo, error) {
137135 imgProxyURL := os.Getenv("IMGPROXY_URL")
138136 imgProxySalt := os.Getenv("IMGPROXY_SALT")
139137 imgProxyKey := os.Getenv("IMGPROXY_KEY")
......@@ -182,7 +180,7 @@ func HandleProxy(r *http.Request, logger *slog.Logger, dataURL string, opts *Img
182180 if err != nil {
183181 logger.Error("decoding last-modified", "err", err)
184182 }
185- info := &storage.ObjectInfo{
183+ info := &ObjectInfo{
186184 Size: res.ContentLength,
187185 ETag: trimEtag(res.Header.Get("etag")),
188186 Metadata: res.Header.Clone(),
R pkg/pobj/reader.go => pkg/shared/storage/reader.go
+1, -1
......@@ -1,4 +1,4 @@
1-package pobj
1+package storage
22
33 import (
44 "errors"
+31, -3
......@@ -3,11 +3,39 @@ package storage
33 import (
44 "io"
55 "net/http"
6+ "os"
7+ "time"
68
7- sst "github.com/picosh/pico/pkg/pobj/storage"
9+ "github.com/picosh/pico/pkg/send/utils"
810 )
911
12+type Bucket struct {
13+ Name string
14+ Path string
15+ Root string
16+}
17+
18+type ObjectStorage interface {
19+ GetBucket(name string) (Bucket, error)
20+ GetBucketQuota(bucket Bucket) (uint64, error)
21+ UpsertBucket(name string) (Bucket, error)
22+ ListBuckets() ([]string, error)
23+ DeleteBucket(bucket Bucket) error
24+
25+ GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error)
26+ PutObject(bucket Bucket, fpath string, contents io.Reader, entry *utils.FileEntry) (string, int64, error)
27+ DeleteObject(bucket Bucket, fpath string) error
28+ ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error)
29+}
30+
31+type ObjectInfo struct {
32+ Size int64
33+ LastModified time.Time
34+ ETag string
35+ Metadata http.Header
36+}
37+
1038 type StorageServe interface {
11- sst.ObjectStorage
12- ServeObject(r *http.Request, bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, *sst.ObjectInfo, error)
39+ ObjectStorage
40+ ServeObject(r *http.Request, bucket Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, *ObjectInfo, error)
1341 }