package storage import ( "crypto/sha256" "encoding/hex" "fmt" "hash" "io" "os" "path/filepath" ) // BlobWriter stages blob data in a temporary file while computing its // SHA-256 digest on the fly. type BlobWriter struct { file *os.File hash hash.Hash mw io.Writer path string written int64 closed bool store *Store } // StartUpload begins a new blob upload, creating a temp file at // /. func (s *Store) StartUpload(uuid string) (*BlobWriter, error) { if err := os.MkdirAll(s.uploadsPath, 0700); err != nil { return nil, fmt.Errorf("storage: create uploads dir: %w", err) } path := filepath.Join(s.uploadsPath, uuid) f, err := os.Create(path) //nolint:gosec // upload UUID is server-generated, not user input if err != nil { return nil, fmt.Errorf("storage: create upload file: %w", err) } h := sha256.New() return &BlobWriter{ file: f, hash: h, mw: io.MultiWriter(f, h), path: path, store: s, }, nil } // Write writes p to both the staging file and the running hash. func (bw *BlobWriter) Write(p []byte) (int, error) { n, err := bw.mw.Write(p) bw.written += int64(n) if err != nil { return n, fmt.Errorf("storage: write: %w", err) } return n, nil } // Commit finalises the upload. It closes the staging file, verifies // the computed digest matches expectedDigest, and atomically moves // the file to its content-addressed location. func (bw *BlobWriter) Commit(expectedDigest string) (string, error) { if !bw.closed { bw.closed = true if err := bw.file.Close(); err != nil { return "", fmt.Errorf("storage: close upload file: %w", err) } } if err := validateDigest(expectedDigest); err != nil { _ = os.Remove(bw.path) return "", err } computed := "sha256:" + hex.EncodeToString(bw.hash.Sum(nil)) if computed != expectedDigest { _ = os.Remove(bw.path) return "", ErrDigestMismatch } dst := bw.store.blobPath(computed) if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil { return "", fmt.Errorf("storage: create blob dir: %w", err) } if err := os.Rename(bw.path, dst); err != nil { return "", fmt.Errorf("storage: rename blob: %w", err) } return computed, nil } // Cancel aborts the upload, closing and removing the temp file. func (bw *BlobWriter) Cancel() error { if !bw.closed { bw.closed = true _ = bw.file.Close() } if err := os.Remove(bw.path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("storage: remove upload file: %w", err) } return nil } // BytesWritten returns the number of bytes written so far. func (bw *BlobWriter) BytesWritten() int64 { return bw.written }