feat(db): add storage backend metadata columns

Add storage_backend columns to track which backend (minio or filesystem)
holds each file, enabling dual-running during migration.

Migration 017_file_storage_metadata.sql:
- item_files.storage_backend TEXT NOT NULL DEFAULT 'minio'
- revisions.file_storage_backend TEXT NOT NULL DEFAULT 'minio'

DB repository changes:
- Revision struct: add FileStorageBackend field
- ItemFile struct: add StorageBackend field
- All INSERT queries include the new columns
- All SELECT queries read them (COALESCE for pre-migration compat)
- CreateRevisionFromExisting copies the backend from source revision
- Default to 'minio' when field is empty (backward compat)

Existing rows default to 'minio'. New uploads will write 'filesystem'
when the filesystem backend is active.

Closes #128
This commit is contained in:
2026-02-17 12:30:20 -06:00
parent 906277149e
commit 8cef4fa55f
3 changed files with 63 additions and 38 deletions

View File

@@ -35,11 +35,12 @@ type Revision struct {
ItemID string
RevisionNumber int
Properties map[string]any
FileKey *string
FileVersion *string
FileChecksum *string
FileSize *int64
ThumbnailKey *string
FileKey *string
FileVersion *string
FileChecksum *string
FileSize *int64
FileStorageBackend string // "minio" or "filesystem"
ThumbnailKey *string
CreatedAt time.Time
CreatedBy *string
Comment *string
@@ -306,16 +307,20 @@ func (r *ItemRepository) CreateRevision(ctx context.Context, rev *Revision) erro
return fmt.Errorf("marshaling properties: %w", err)
}
if rev.FileStorageBackend == "" {
rev.FileStorageBackend = "minio"
}
err = r.db.pool.QueryRow(ctx, `
INSERT INTO revisions (
item_id, revision_number, properties, file_key, file_version,
file_checksum, file_size, thumbnail_key, created_by, comment
file_checksum, file_size, file_storage_backend, thumbnail_key, created_by, comment
)
SELECT $1, current_revision + 1, $2, $3, $4, $5, $6, $7, $8, $9
SELECT $1, current_revision + 1, $2, $3, $4, $5, $6, $7, $8, $9, $10
FROM items WHERE id = $1
RETURNING id, revision_number, created_at
`, rev.ItemID, propsJSON, rev.FileKey, rev.FileVersion,
rev.FileChecksum, rev.FileSize, rev.ThumbnailKey, rev.CreatedBy, rev.Comment,
rev.FileChecksum, rev.FileSize, rev.FileStorageBackend, rev.ThumbnailKey, rev.CreatedBy, rev.Comment,
).Scan(&rev.ID, &rev.RevisionNumber, &rev.CreatedAt)
if err != nil {
return fmt.Errorf("inserting revision: %w", err)
@@ -342,7 +347,8 @@ func (r *ItemRepository) GetRevisions(ctx context.Context, itemID string) ([]*Re
if hasStatusColumn {
rows, err = r.db.pool.Query(ctx, `
SELECT id, item_id, revision_number, properties, file_key, file_version,
file_checksum, file_size, thumbnail_key, created_at, created_by, comment,
file_checksum, file_size, COALESCE(file_storage_backend, 'minio'),
thumbnail_key, created_at, created_by, comment,
COALESCE(status, 'draft') as status, COALESCE(labels, ARRAY[]::TEXT[]) as labels
FROM revisions
WHERE item_id = $1
@@ -369,7 +375,8 @@ func (r *ItemRepository) GetRevisions(ctx context.Context, itemID string) ([]*Re
if hasStatusColumn {
err = rows.Scan(
&rev.ID, &rev.ItemID, &rev.RevisionNumber, &propsJSON, &rev.FileKey, &rev.FileVersion,
&rev.FileChecksum, &rev.FileSize, &rev.ThumbnailKey, &rev.CreatedAt, &rev.CreatedBy, &rev.Comment,
&rev.FileChecksum, &rev.FileSize, &rev.FileStorageBackend,
&rev.ThumbnailKey, &rev.CreatedAt, &rev.CreatedBy, &rev.Comment,
&rev.Status, &rev.Labels,
)
} else {
@@ -379,6 +386,7 @@ func (r *ItemRepository) GetRevisions(ctx context.Context, itemID string) ([]*Re
)
rev.Status = "draft"
rev.Labels = []string{}
rev.FileStorageBackend = "minio"
}
if err != nil {
return nil, fmt.Errorf("scanning revision: %w", err)
@@ -412,13 +420,15 @@ func (r *ItemRepository) GetRevision(ctx context.Context, itemID string, revisio
if hasStatusColumn {
err = r.db.pool.QueryRow(ctx, `
SELECT id, item_id, revision_number, properties, file_key, file_version,
file_checksum, file_size, thumbnail_key, created_at, created_by, comment,
file_checksum, file_size, COALESCE(file_storage_backend, 'minio'),
thumbnail_key, created_at, created_by, comment,
COALESCE(status, 'draft') as status, COALESCE(labels, ARRAY[]::TEXT[]) as labels
FROM revisions
WHERE item_id = $1 AND revision_number = $2
`, itemID, revisionNumber).Scan(
&rev.ID, &rev.ItemID, &rev.RevisionNumber, &propsJSON, &rev.FileKey, &rev.FileVersion,
&rev.FileChecksum, &rev.FileSize, &rev.ThumbnailKey, &rev.CreatedAt, &rev.CreatedBy, &rev.Comment,
&rev.FileChecksum, &rev.FileSize, &rev.FileStorageBackend,
&rev.ThumbnailKey, &rev.CreatedAt, &rev.CreatedBy, &rev.Comment,
&rev.Status, &rev.Labels,
)
} else {
@@ -433,6 +443,7 @@ func (r *ItemRepository) GetRevision(ctx context.Context, itemID string, revisio
)
rev.Status = "draft"
rev.Labels = []string{}
rev.FileStorageBackend = "minio"
}
if err == pgx.ErrNoRows {
@@ -606,15 +617,16 @@ func (r *ItemRepository) CreateRevisionFromExisting(ctx context.Context, itemID
// Create new revision with copied properties (and optionally file reference)
newRev := &Revision{
ItemID: itemID,
Properties: source.Properties,
FileKey: source.FileKey,
FileVersion: source.FileVersion,
FileChecksum: source.FileChecksum,
FileSize: source.FileSize,
ThumbnailKey: source.ThumbnailKey,
CreatedBy: createdBy,
Comment: &comment,
ItemID: itemID,
Properties: source.Properties,
FileKey: source.FileKey,
FileVersion: source.FileVersion,
FileChecksum: source.FileChecksum,
FileSize: source.FileSize,
FileStorageBackend: source.FileStorageBackend,
ThumbnailKey: source.ThumbnailKey,
CreatedBy: createdBy,
Comment: &comment,
}
// Insert the new revision
@@ -626,13 +638,13 @@ func (r *ItemRepository) CreateRevisionFromExisting(ctx context.Context, itemID
err = r.db.pool.QueryRow(ctx, `
INSERT INTO revisions (
item_id, revision_number, properties, file_key, file_version,
file_checksum, file_size, thumbnail_key, created_by, comment, status
file_checksum, file_size, file_storage_backend, thumbnail_key, created_by, comment, status
)
SELECT $1, current_revision + 1, $2, $3, $4, $5, $6, $7, $8, $9, 'draft'
SELECT $1, current_revision + 1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'draft'
FROM items WHERE id = $1
RETURNING id, revision_number, created_at
`, newRev.ItemID, propsJSON, newRev.FileKey, newRev.FileVersion,
newRev.FileChecksum, newRev.FileSize, newRev.ThumbnailKey, newRev.CreatedBy, newRev.Comment,
newRev.FileChecksum, newRev.FileSize, newRev.FileStorageBackend, newRev.ThumbnailKey, newRev.CreatedBy, newRev.Comment,
).Scan(&newRev.ID, &newRev.RevisionNumber, &newRev.CreatedAt)
if err != nil {
return nil, fmt.Errorf("inserting revision: %w", err)