// Package blob implements a content-addressable store for artifact content. // Files are addressed by their SHA256 hash and stored in a hierarchical // directory layout for filesystem friendliness. package blob import ( "crypto/sha256" "errors" "fmt" "os" "path/filepath" ) // Store manages a content-addressable blob store on the local filesystem. type Store struct { basePath string } // NewStore creates a Store rooted at the given base path. func NewStore(basePath string) *Store { return &Store{basePath: basePath} } // Write computes the SHA256 hash of data, writes it to the store, and returns // the hex-encoded hash (which is the blob ID). func (s *Store) Write(data []byte) (string, error) { hash := sha256.Sum256(data) id := fmt.Sprintf("%x", hash[:]) p := s.path(id) dir := filepath.Dir(p) if err := os.MkdirAll(dir, 0o750); err != nil { return "", fmt.Errorf("blob: failed to create directory %q: %w", dir, err) } if err := os.WriteFile(p, data, 0o600); err != nil { return "", fmt.Errorf("blob: failed to write blob %q: %w", id, err) } return id, nil } // Read returns the content of the blob with the given ID. func (s *Store) Read(id string) ([]byte, error) { data, err := os.ReadFile(s.path(id)) if err != nil { return nil, fmt.Errorf("blob: failed to read blob %q: %w", id, err) } return data, nil } // Exists returns true if a blob with the given ID exists in the store. func (s *Store) Exists(id string) bool { _, err := os.Stat(s.path(id)) return !errors.Is(err, os.ErrNotExist) } // Path returns the full filesystem path for a blob ID. func (s *Store) Path(id string) string { return s.path(id) } // HashData returns the SHA256 hex digest of data without writing it. func HashData(data []byte) string { hash := sha256.Sum256(data) return fmt.Sprintf("%x", hash[:]) } // path computes the filesystem path for a blob ID. The hex hash is split // into 4-character segments as nested directories. // Example: "a1b2c3d4..." -> basePath/a1b2/c3d4/.../a1b2c3d4... func (s *Store) path(id string) string { parts := []string{s.basePath} for i := 0; i+4 <= len(id); i += 4 { parts = append(parts, id[i:i+4]) } parts = append(parts, id) return filepath.Join(parts...) }