main pico / pkg / storage / memory.go
Eric Bower  ·  2026-10-12
  1package storage
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"io"
  7	"io/fs"
  8	"os"
  9	"path"
 10	"path/filepath"
 11	"strings"
 12	"sync"
 13	"time"
 14
 15	"github.com/picosh/pico/pkg/send/utils"
 16)
 17
 18type seekableReader struct {
 19	*bytes.Reader
 20}
 21
 22func (s *seekableReader) Close() error { return nil }
 23
 24type StorageMemory struct {
 25	storage map[string]map[string]string
 26	mtimes  map[string]map[string]time.Time
 27	mu      sync.RWMutex
 28}
 29
 30var _ StorageServe = &StorageMemory{}
 31var _ StorageServe = (*StorageMemory)(nil)
 32
 33func NewStorageMemory(st map[string]map[string]string) (*StorageMemory, error) {
 34	now := time.Now()
 35	mtimes := map[string]map[string]time.Time{}
 36	for bucket, objects := range st {
 37		mtimes[bucket] = map[string]time.Time{}
 38		for key := range objects {
 39			mtimes[bucket][key] = now
 40		}
 41	}
 42	return &StorageMemory{
 43		storage: st,
 44		mtimes:  mtimes,
 45	}, nil
 46}
 47
 48func memoryKey(fpath string) string {
 49	if !strings.HasPrefix(fpath, "/") {
 50		return "/" + fpath
 51	}
 52	return fpath
 53}
 54
 55// dirKey is the key that keeps a directory listed while it's empty. Keys are
 56// flat like S3's, so a directory is a "dir/" key with no contents.
 57func dirKey(dir string) string {
 58	return strings.TrimSuffix(memoryKey(dir), "/") + "/"
 59}
 60
 61// hasPrefix reports whether any key other than skip starts with prefix.
 62func hasPrefix(objects map[string]string, prefix, skip string) bool {
 63	for key := range objects {
 64		if key != skip && strings.HasPrefix(key, prefix) {
 65			return true
 66		}
 67	}
 68	return false
 69}
 70
 71func (s *StorageMemory) GetBucket(name string) (Bucket, error) {
 72	s.mu.RLock()
 73	defer s.mu.RUnlock()
 74
 75	bucket := Bucket{
 76		Name: name,
 77		Path: name,
 78	}
 79
 80	_, ok := s.storage[name]
 81	if !ok {
 82		return bucket, fmt.Errorf("bucket does not exist")
 83	}
 84
 85	return bucket, nil
 86}
 87
 88func (s *StorageMemory) UpsertBucket(name string) (Bucket, error) {
 89	bucket, err := s.GetBucket(name)
 90	if err == nil {
 91		return bucket, nil
 92	}
 93
 94	s.mu.Lock()
 95	defer s.mu.Unlock()
 96
 97	s.storage[name] = map[string]string{}
 98	s.mtimes[name] = map[string]time.Time{}
 99	return bucket, nil
100}
101
102func (s *StorageMemory) GetBucketQuota(bucket Bucket) (uint64, error) {
103	s.mu.RLock()
104	defer s.mu.RUnlock()
105
106	objects := s.storage[bucket.Path]
107	size := 0
108	for _, val := range objects {
109		size += len([]byte(val))
110	}
111	return uint64(size), nil
112}
113
114func (s *StorageMemory) DeleteBucket(bucket Bucket) error {
115	s.mu.Lock()
116	defer s.mu.Unlock()
117
118	delete(s.storage, bucket.Path)
119	delete(s.mtimes, bucket.Path)
120	return nil
121}
122
123func (s *StorageMemory) GetObject(bucket Bucket, fpath string) (utils.ReadAndReaderAtCloser, *ObjectInfo, error) {
124	s.mu.RLock()
125	defer s.mu.RUnlock()
126
127	fpath = memoryKey(fpath)
128	objInfo := &ObjectInfo{}
129
130	dat, ok := s.storage[bucket.Path][fpath]
131	if !ok || strings.HasSuffix(fpath, "/") {
132		return nil, objInfo, fmt.Errorf("object does not exist: %s: %w", fpath, fs.ErrNotExist)
133	}
134
135	objInfo.Size = int64(len([]byte(dat)))
136	objInfo.LastModified = s.mtimes[bucket.Path][fpath]
137	return &seekableReader{bytes.NewReader([]byte(dat))}, objInfo, nil
138}
139
140func (s *StorageMemory) StatObject(bucket Bucket, fpath string) (*ObjectInfo, error) {
141	s.mu.RLock()
142	defer s.mu.RUnlock()
143
144	fpath = memoryKey(fpath)
145	dat, ok := s.storage[bucket.Path][fpath]
146	if !ok || strings.HasSuffix(fpath, "/") {
147		return nil, fmt.Errorf("object does not exist: %s: %w", fpath, fs.ErrNotExist)
148	}
149	return &ObjectInfo{
150		Size:         int64(len(dat)),
151		LastModified: s.mtimes[bucket.Path][fpath],
152	}, nil
153}
154
155func (s *StorageMemory) PutObject(bucket Bucket, fpath string, contents io.Reader, info *ObjectInfo) (string, int64, error) {
156	s.mu.Lock()
157	defer s.mu.Unlock()
158
159	d, err := io.ReadAll(contents)
160	if err != nil {
161		return "", 0, err
162	}
163
164	fpath = memoryKey(fpath)
165	mtime := info.LastModified
166	if mtime.IsZero() {
167		mtime = time.Now()
168	}
169	s.storage[bucket.Path][fpath] = string(d)
170	if s.mtimes[bucket.Path] == nil {
171		s.mtimes[bucket.Path] = map[string]time.Time{}
172	}
173	s.mtimes[bucket.Path][fpath] = mtime
174	return fmt.Sprintf("%s%s", bucket.Path, fpath), int64(len(d)), nil
175}
176
177func (s *StorageMemory) PutDir(bucket Bucket, dir string) error {
178	s.mu.Lock()
179	defer s.mu.Unlock()
180
181	key := dirKey(dir)
182	if key == "/" {
183		return nil
184	}
185	s.storage[bucket.Path][key] = ""
186	s.mtimes[bucket.Path][key] = time.Now()
187	return nil
188}
189
190func (s *StorageMemory) DeleteObject(bucket Bucket, fpath string) error {
191	s.mu.Lock()
192	defer s.mu.Unlock()
193
194	objects := s.storage[bucket.Path]
195	mtimes := s.mtimes[bucket.Path]
196	fpath = memoryKey(fpath)
197	if _, ok := objects[fpath]; ok && !strings.HasSuffix(fpath, "/") {
198		delete(objects, fpath)
199		delete(mtimes, fpath)
200		if parent := dirKey(path.Dir(fpath)); parent != "/" && !hasPrefix(objects, parent, "") {
201			objects[parent] = ""
202			mtimes[parent] = time.Now()
203		}
204		return nil
205	}
206
207	key := dirKey(fpath)
208	if hasPrefix(objects, key, key) {
209		return fmt.Errorf("%s: directory not empty", fpath)
210	}
211	delete(objects, key)
212	delete(mtimes, key)
213	return nil
214}
215
216func (s *StorageMemory) ListBuckets() ([]string, error) {
217	s.mu.RLock()
218	defer s.mu.RUnlock()
219
220	buckets := []string{}
221	for key := range s.storage {
222		buckets = append(buckets, key)
223	}
224	return buckets, nil
225}
226
227// ListObjects follows StorageFS: a path naming an object lists that object,
228// a directory path without a trailing slash lists only the directory itself,
229// and a path ending in a slash lists its contents relative to it.
230func (s *StorageMemory) ListObjects(bucket Bucket, dir string, recursive bool) ([]os.FileInfo, error) {
231	s.mu.RLock()
232	defer s.mu.RUnlock()
233
234	fileList := []os.FileInfo{}
235	resolved := memoryKey(dir)
236	objects := s.storage[bucket.Path]
237	mtimes := s.mtimes[bucket.Path]
238
239	if val, ok := objects[resolved]; ok && !strings.HasSuffix(resolved, "/") {
240		fileList = append(fileList, &utils.VirtualFile{
241			FName:    filepath.Base(resolved),
242			FSize:    int64(len(val)),
243			FModTime: mtimes[resolved],
244		})
245		return fileList, nil
246	}
247
248	if !strings.HasSuffix(resolved, "/") {
249		for key := range objects {
250			if strings.HasPrefix(key, resolved+"/") {
251				fileList = append(fileList, &utils.VirtualFile{FName: "", FIsDir: true})
252				break
253			}
254		}
255		return fileList, nil
256	}
257
258	seen := map[string]bool{}
259	addDir := func(name string) {
260		if !seen[name] {
261			seen[name] = true
262			fileList = append(fileList, &utils.VirtualFile{FName: name, FIsDir: true})
263		}
264	}
265	for key, val := range objects {
266		rel, ok := strings.CutPrefix(key, resolved)
267		if !ok || rel == "" {
268			continue
269		}
270		if !recursive {
271			if d, _, nested := strings.Cut(rel, "/"); nested {
272				addDir(d)
273				continue
274			}
275		}
276		name, isDir := strings.CutSuffix(rel, "/")
277		start := filepath.Dir(name)
278		if isDir {
279			start = name
280		}
281		for d := start; d != "." && !seen[d]; d = filepath.Dir(d) {
282			addDir(d)
283		}
284		if isDir {
285			continue
286		}
287		fileList = append(fileList, &utils.VirtualFile{
288			FName:    rel,
289			FSize:    int64(len(val)),
290			FModTime: mtimes[key],
291		})
292	}
293
294	return fileList, nil
295}