package api import ( "crypto/sha256" "encoding/hex" "io" "os" "path/filepath" "strings" "time" ) // SimpleFSStore is a minimal FS-backed implementation of BlobStore. // Layout under root: // // / - content // /.priv - presence means "private" type SimpleFSStore struct { root string } func NewSimpleFSStore(root string) *SimpleFSStore { return &SimpleFSStore{root: root} } func (fs *SimpleFSStore) ensureRoot() error { return os.MkdirAll(fs.root, 0o755) } func (fs *SimpleFSStore) pathFor(hash string) string { return filepath.Join(fs.root, hash) } func (fs *SimpleFSStore) privPathFor(hash string) string { return filepath.Join(fs.root, hash+".priv") } // Get implements BlobStore.Get func (fs *SimpleFSStore) Get(hash string) (io.ReadCloser, int64, error) { if err := fs.ensureRoot(); err != nil { return nil, 0, err } f, err := os.Open(fs.pathFor(hash)) if err != nil { return nil, 0, err } st, err := f.Stat() if err != nil { _ = f.Close() return nil, 0, err } return f, st.Size(), nil } // Put implements BlobStore.Put func (fs *SimpleFSStore) Put(r io.Reader, private bool) (string, int64, time.Time, error) { if err := fs.ensureRoot(); err != nil { return "", 0, time.Time{}, err } tmp, err := os.CreateTemp(fs.root, "put-*") if err != nil { return "", 0, time.Time{}, err } defer func() { _ = tmp.Close() _ = os.Remove(tmp.Name()) }() h := sha256.New() w := io.MultiWriter(tmp, h) n, err := io.Copy(w, r) if err != nil { return "", 0, time.Time{}, err } hash := hex.EncodeToString(h.Sum(nil)) final := fs.pathFor(hash) if err := os.Rename(tmp.Name(), final); err != nil { return "", 0, time.Time{}, err } if private { if err := os.WriteFile(fs.privPathFor(hash), nil, 0o600); err != nil { _ = os.Remove(final) return "", 0, time.Time{}, err } } st, err := os.Stat(final) if err != nil { return "", 0, time.Time{}, err } return hash, n, st.ModTime().UTC(), nil } // Delete implements BlobStore.Delete func (fs *SimpleFSStore) Delete(hash string) error { if err := fs.ensureRoot(); err != nil { return err } _ = os.Remove(fs.privPathFor(hash)) return os.Remove(fs.pathFor(hash)) } // Walk implements BlobStore.Walk func (fs *SimpleFSStore) Walk(fn func(hash string, bytes int64, private bool, storedAt time.Time) error) (int, error) { if err := fs.ensureRoot(); err != nil { return 0, err } ents, err := os.ReadDir(fs.root) if err != nil { return 0, err } count := 0 for _, e := range ents { if e.IsDir() { continue } name := e.Name() // skip sidecars and non-64-hex filenames if strings.HasSuffix(name, ".priv") || len(name) != 64 || !isHex(name) { continue } full := fs.pathFor(name) st, err := os.Stat(full) if err != nil { continue } private := false if _, err := os.Stat(fs.privPathFor(name)); err == nil { private = true } if err := fn(name, st.Size(), private, st.ModTime().UTC()); err != nil { return count, err } count++ } return count, nil } func isHex(s string) bool { for i := 0; i < len(s); i++ { c := s[i] if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { return false } } return true }