Files
hpc/internal/store/blob_store.go
dailz bf89de12f0 feat(store): add blob, file, folder, and upload stores
Add BlobStore (ref counting), FileStore (soft delete + pagination), FolderStore (materialized path), UploadStore (idempotent upsert), and update AutoMigrate.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 09:22:44 +08:00

100 lines
2.8 KiB
Go

package store
import (
"context"
"errors"
"gcy_hpc_server/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// BlobStore manages physical file blobs with reference counting.
type BlobStore struct {
db *gorm.DB
}
// NewBlobStore creates a new BlobStore.
func NewBlobStore(db *gorm.DB) *BlobStore {
return &BlobStore{db: db}
}
// Create inserts a new FileBlob record.
func (s *BlobStore) Create(ctx context.Context, blob *model.FileBlob) error {
return s.db.WithContext(ctx).Create(blob).Error
}
// GetBySHA256 returns the FileBlob with the given SHA256 hash.
// Returns (nil, nil) if not found.
func (s *BlobStore) GetBySHA256(ctx context.Context, sha256 string) (*model.FileBlob, error) {
var blob model.FileBlob
err := s.db.WithContext(ctx).Where("sha256 = ?", sha256).First(&blob).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &blob, nil
}
// IncrementRef atomically increments the ref_count for the blob with the given SHA256.
func (s *BlobStore) IncrementRef(ctx context.Context, sha256 string) error {
result := s.db.WithContext(ctx).Model(&model.FileBlob{}).
Where("sha256 = ?", sha256).
UpdateColumn("ref_count", gorm.Expr("ref_count + 1"))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// DecrementRef atomically decrements the ref_count for the blob with the given SHA256.
// Returns the new ref_count after decrementing.
func (s *BlobStore) DecrementRef(ctx context.Context, sha256 string) (int64, error) {
result := s.db.WithContext(ctx).Model(&model.FileBlob{}).
Where("sha256 = ? AND ref_count > 0", sha256).
UpdateColumn("ref_count", gorm.Expr("ref_count - 1"))
if result.Error != nil {
return 0, result.Error
}
if result.RowsAffected == 0 {
return 0, gorm.ErrRecordNotFound
}
var blob model.FileBlob
if err := s.db.WithContext(ctx).Where("sha256 = ?", sha256).First(&blob).Error; err != nil {
return 0, err
}
return int64(blob.RefCount), nil
}
// Delete removes a FileBlob record by SHA256 (hard delete).
func (s *BlobStore) Delete(ctx context.Context, sha256 string) error {
result := s.db.WithContext(ctx).Where("sha256 = ?", sha256).Delete(&model.FileBlob{})
if result.Error != nil {
return result.Error
}
return nil
}
// GetBySHA256ForUpdate returns the FileBlob with a SELECT ... FOR UPDATE lock.
// Returns (nil, nil) if not found.
func (s *BlobStore) GetBySHA256ForUpdate(ctx context.Context, tx *gorm.DB, sha256 string) (*model.FileBlob, error) {
var blob model.FileBlob
err := tx.WithContext(ctx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("sha256 = ?", sha256).First(&blob).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &blob, nil
}