Files
silo/internal/db/item_files.go
Forbes 88d1ab1f97 refactor(storage): remove MinIO backend, filesystem-only storage
Remove the MinIO/S3 storage backend entirely. The filesystem backend is
fully implemented, already used in production, and a migrate-storage tool
exists for any remaining MinIO deployments to migrate beforehand.

Changes:
- Delete MinIO client implementation (internal/storage/storage.go)
- Delete migrate-storage tool (cmd/migrate-storage, scripts/migrate-storage.sh)
- Remove MinIO service, volumes, and env vars from all Docker Compose files
- Simplify StorageConfig: remove Endpoint, AccessKey, SecretKey, Bucket,
  UseSSL, Region fields; add SILO_STORAGE_ROOT_DIR env override
- Change all SQL COALESCE defaults from 'minio' to 'filesystem'
- Add migration 020 to update column defaults to 'filesystem'
- Remove minio-go/v7 dependency (go mod tidy)
- Update all config examples, setup scripts, docs, and tests
2026-02-19 14:36:22 -06:00

98 lines
2.8 KiB
Go

package db
import (
"context"
"fmt"
"time"
)
// ItemFile represents a file attachment on an item.
type ItemFile struct {
ID string
ItemID string
Filename string
ContentType string
Size int64
ObjectKey string
StorageBackend string
CreatedAt time.Time
}
// ItemFileRepository provides item_files database operations.
type ItemFileRepository struct {
db *DB
}
// NewItemFileRepository creates a new item file repository.
func NewItemFileRepository(db *DB) *ItemFileRepository {
return &ItemFileRepository{db: db}
}
// Create inserts a new item file record.
func (r *ItemFileRepository) Create(ctx context.Context, f *ItemFile) error {
if f.StorageBackend == "" {
f.StorageBackend = "filesystem"
}
err := r.db.pool.QueryRow(ctx,
`INSERT INTO item_files (item_id, filename, content_type, size, object_key, storage_backend)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, created_at`,
f.ItemID, f.Filename, f.ContentType, f.Size, f.ObjectKey, f.StorageBackend,
).Scan(&f.ID, &f.CreatedAt)
if err != nil {
return fmt.Errorf("creating item file: %w", err)
}
return nil
}
// ListByItem returns all file attachments for an item.
func (r *ItemFileRepository) ListByItem(ctx context.Context, itemID string) ([]*ItemFile, error) {
rows, err := r.db.pool.Query(ctx,
`SELECT id, item_id, filename, content_type, size, object_key,
COALESCE(storage_backend, 'filesystem'), created_at
FROM item_files WHERE item_id = $1 ORDER BY created_at`,
itemID,
)
if err != nil {
return nil, fmt.Errorf("listing item files: %w", err)
}
defer rows.Close()
var files []*ItemFile
for rows.Next() {
f := &ItemFile{}
if err := rows.Scan(&f.ID, &f.ItemID, &f.Filename, &f.ContentType, &f.Size, &f.ObjectKey, &f.StorageBackend, &f.CreatedAt); err != nil {
return nil, fmt.Errorf("scanning item file: %w", err)
}
files = append(files, f)
}
return files, nil
}
// Get returns a single item file by ID.
func (r *ItemFileRepository) Get(ctx context.Context, id string) (*ItemFile, error) {
f := &ItemFile{}
err := r.db.pool.QueryRow(ctx,
`SELECT id, item_id, filename, content_type, size, object_key,
COALESCE(storage_backend, 'filesystem'), created_at
FROM item_files WHERE id = $1`,
id,
).Scan(&f.ID, &f.ItemID, &f.Filename, &f.ContentType, &f.Size, &f.ObjectKey, &f.StorageBackend, &f.CreatedAt)
if err != nil {
return nil, fmt.Errorf("getting item file: %w", err)
}
return f, nil
}
// Delete removes an item file record.
func (r *ItemFileRepository) Delete(ctx context.Context, id string) error {
tag, err := r.db.pool.Exec(ctx, `DELETE FROM item_files WHERE id = $1`, id)
if err != nil {
return fmt.Errorf("deleting item file: %w", err)
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("item file not found")
}
return nil
}