package depot import ( "context" "crypto/sha256" "fmt" "io" "os" "path/filepath" ) // LocalBackend implements Backend for local filesystem storage. type LocalBackend struct { Root string } // Name returns the backend name. func (b *LocalBackend) Name() string { return "local" } // Probe checks if a blob exists. func (b *LocalBackend) Probe(ctx context.Context, sha string) (ProbeState, bool, error) { blobPath := filepath.Join(b.Root, BlobKey(sha)) _, err := os.Stat(blobPath) if err == nil { return Present, false, nil } if os.IsNotExist(err) { return Absent, false, nil } // Real I/O error (permission, etc.) return Absent, false, err } // Put copies the file from src to the blob storage. func (b *LocalBackend) Put(ctx context.Context, sha, src string) error { blobPath := filepath.Join(b.Root, BlobKey(sha)) blobDir := filepath.Dir(blobPath) // Create parent directories if err := os.MkdirAll(blobDir, 0755); err != nil { return err } // Create temp file in the same directory for atomic rename tmpFile, err := os.CreateTemp(blobDir, ".tmp-") if err != nil { return err } defer os.Remove(tmpFile.Name()) // Copy source to temp file srcFile, err := os.Open(src) if err != nil { tmpFile.Close() return err } defer srcFile.Close() _, err = io.Copy(tmpFile, srcFile) tmpFile.Close() if err != nil { return err } // Atomic rename return os.Rename(tmpFile.Name(), blobPath) } // Get fetches the blob, re-hashes it, and deletes it if the hash doesn't match. func (b *LocalBackend) Get(ctx context.Context, sha, dest string) error { blobPath := filepath.Join(b.Root, BlobKey(sha)) // Create temp file in dest directory for atomic rename destDir := filepath.Dir(dest) if err := os.MkdirAll(destDir, 0755); err != nil { return err } tmpFile, err := os.CreateTemp(destDir, ".tmp-") if err != nil { return err } defer os.Remove(tmpFile.Name()) // Copy and hash simultaneously srcFile, err := os.Open(blobPath) if err != nil { tmpFile.Close() return err } defer srcFile.Close() hasher := sha256.New() teeReader := io.TeeReader(srcFile, hasher) _, err = io.Copy(tmpFile, teeReader) tmpFile.Close() if err != nil { return err } // Check hash gotHash := fmt.Sprintf("%x", hasher.Sum(nil)) if gotHash != sha { os.Remove(tmpFile.Name()) return fmt.Errorf("hash mismatch: expected %s, got %s", sha, gotHash) } // Atomic rename return os.Rename(tmpFile.Name(), dest) }