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>
This commit is contained in:
99
internal/store/blob_store.go
Normal file
99
internal/store/blob_store.go
Normal file
@@ -0,0 +1,99 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user