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>
100 lines
2.8 KiB
Go
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
|
|
}
|