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 } func (s *BlobStore) GetBySHA256s(ctx context.Context, sha256s []string) ([]model.FileBlob, error) { var blobs []model.FileBlob if len(sha256s) == 0 { return blobs, nil } err := s.db.WithContext(ctx).Where("sha256 IN ?", sha256s).Find(&blobs).Error return blobs, err } // 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 }