package cas import ( "context" "encoding/hex" "errors" "fmt" "os" "path/filepath" "git.michelsen.id/phill/chron/chron-note/internal/domain" "git.michelsen.id/phill/chron/chron-note/internal/util" ) var ErrBlobNotFound = errors.New("blob not found") type FS struct { root string } func NewFS(root string) *FS { return &FS{root: root} } func (s *FS) Put(ctx context.Context, data []byte) (domain.BlobID, error) { _ = ctx sum := util.Hash256(data) var id domain.BlobID copy(id[:], sum[:]) p := s.pathFor(id) // Fast path: already exists if _, err := os.Stat(p); err == nil { return id, nil } if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { return domain.BlobID{}, err } tmp := p + ".tmp" if err := os.WriteFile(tmp, data, 0o644); err != nil { return domain.BlobID{}, err } if err := os.Rename(tmp, p); err != nil { _ = os.Remove(tmp) // If another writer won the race, accept it. if _, statErr := os.Stat(p); statErr == nil { return id, nil } return domain.BlobID{}, err } return id, nil } func (s *FS) Get(ctx context.Context, id domain.BlobID) ([]byte, error) { _ = ctx p := s.pathFor(id) b, err := os.ReadFile(p) if err != nil { if os.IsNotExist(err) { return nil, ErrBlobNotFound } return nil, err } return b, nil } func (s *FS) Has(ctx context.Context, id domain.BlobID) (bool, error) { _ = ctx p := s.pathFor(id) _, err := os.Stat(p) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return false, err } func (s *FS) pathFor(id domain.BlobID) string { hexID := hex.EncodeToString(id[:]) if len(hexID) < 4 { // should never happen return filepath.Join(s.root, fmt.Sprintf("bad-%s", hexID)) } // git-style fanout return filepath.Join(s.root, hexID[:2], hexID[2:4], hexID) }