internal/mediastore/store_sql.go
Rama main · réplica de lectura del repositorio original.
package mediastore
import (
"context"
"errors"
"fmt"
"time"
"github.com/enterpilot/gomodel/internal/storage"
"github.com/enterpilot/gomodel/internal/storage/sqlx"
)
// SQLStore keeps media records in SQLite or PostgreSQL.
type SQLStore struct {
db sqlx.DB
}
var sqlSchema = []string{
`CREATE TABLE IF NOT EXISTS media_objects (
id TEXT PRIMARY KEY,
kind TEXT NOT NULL,
source TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT '',
bytes ` + sqlx.TypeInt64 + ` NOT NULL DEFAULT 0,
storage_key TEXT NOT NULL,
request_id TEXT NOT NULL DEFAULT '',
user_path TEXT NOT NULL DEFAULT '',
created_at ` + sqlx.TypeInt64 + ` NOT NULL,
expires_at ` + sqlx.TypeInt64 + ` NOT NULL DEFAULT 0
)`,
`CREATE INDEX IF NOT EXISTS idx_media_objects_expires_at ON media_objects(expires_at)`,
`CREATE INDEX IF NOT EXISTS idx_media_objects_request_id ON media_objects(request_id)`,
}
const sqlObjectColumns = `id, kind, source, content_type, bytes, storage_key, request_id, user_path, created_at, expires_at`
// NewSQLStore creates the media_objects table and indexes if needed.
func NewSQLStore(ctx context.Context, db sqlx.DB) (*SQLStore, error) {
if db == nil {
return nil, errors.New("database connection is required")
}
if err := db.Schema(ctx, sqlSchema...); err != nil {
return nil, fmt.Errorf("failed to create media_objects table: %w", err)
}
return &SQLStore{db: db}, nil
}
// Insert stores a record; an existing id is replaced.
func (s *SQLStore) Insert(ctx context.Context, object *Object) error {
normalized, err := normalizeObject(object)
if err != nil {
return err
}
_, err = s.db.Exec(ctx, `
INSERT INTO media_objects (`+sqlObjectColumns+`)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
kind = excluded.kind,
source = excluded.source,
content_type = excluded.content_type,
bytes = excluded.bytes,
storage_key = excluded.storage_key,
request_id = excluded.request_id,
user_path = excluded.user_path,
created_at = excluded.created_at,
expires_at = excluded.expires_at
`, normalized.ID, string(normalized.Kind), string(normalized.Source), normalized.ContentType, normalized.Bytes,
normalized.StorageKey, normalized.RequestID, normalized.UserPath,
normalized.CreatedAt.Unix(), storage.UnixOrZero(normalized.ExpiresAt))
if err != nil {
return fmt.Errorf("insert media object: %w", err)
}
return nil
}
// Get returns one record by id.
func (s *SQLStore) Get(ctx context.Context, id string) (*Object, error) {
object, err := scanObject(s.db.QueryRow(ctx, `SELECT `+sqlObjectColumns+` FROM media_objects WHERE id = ?`, id))
if err != nil {
if errors.Is(err, sqlx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("query media object: %w", err)
}
return object, nil
}
// Delete removes one record by id.
func (s *SQLStore) Delete(ctx context.Context, id string) error {
affected, err := s.db.Exec(ctx, `DELETE FROM media_objects WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete media object: %w", err)
}
if affected == 0 {
return ErrNotFound
}
return nil
}
// Expired returns expired records, oldest expiry first.
func (s *SQLStore) Expired(ctx context.Context, now time.Time, limit int) ([]*Object, error) {
if limit <= 0 {
limit = 100
}
rows, err := s.db.Query(ctx, `
SELECT `+sqlObjectColumns+`
FROM media_objects
WHERE expires_at > 0 AND expires_at <= ?
ORDER BY expires_at ASC, id ASC
LIMIT ?
`, now.Unix(), limit)
if err != nil {
return nil, fmt.Errorf("list expired media objects: %w", err)
}
defer rows.Close()
objects := make([]*Object, 0, limit)
for rows.Next() {
object, err := scanObject(rows)
if err != nil {
return nil, fmt.Errorf("scan media object: %w", err)
}
objects = append(objects, object)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate media objects: %w", err)
}
return objects, nil
}
// Close is a no-op; connection lifecycle is managed by the storage layer.
func (s *SQLStore) Close() error {
return nil
}
func scanObject(row sqlx.Row) (*Object, error) {
var (
object Object
kind string
source string
createdAt int64
expiresAt int64
)
if err := row.Scan(&object.ID, &kind, &source, &object.ContentType, &object.Bytes, &object.StorageKey,
&object.RequestID, &object.UserPath, &createdAt, &expiresAt); err != nil {
return nil, err
}
object.Kind = Kind(kind)
object.Source = Source(source)
object.CreatedAt = time.Unix(createdAt, 0).UTC()
object.ExpiresAt = storage.UnixTime(expiresAt)
return &object, nil
}