150 lines
4.2 KiB
Go
150 lines
4.2 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// UploadMeta is the database index record for an uploaded file.
|
|
//
|
|
// The .meta.json sidecar remains the source of truth; this struct is the
|
|
// queryable mirror used by the admin UI.
|
|
type UploadMeta struct {
|
|
FileID string `json:"file_id"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
SHA256 string `json:"sha256"`
|
|
UploadedAt time.Time `json:"uploaded_at"`
|
|
}
|
|
|
|
// UploadRepo provides CRUD operations for upload file index records.
|
|
type UploadRepo struct {
|
|
db *DB
|
|
}
|
|
|
|
// Uploads returns a repository bound to this DB handle.
|
|
func (d *DB) Uploads() *UploadRepo {
|
|
return &UploadRepo{db: d}
|
|
}
|
|
|
|
// Create inserts a new upload index record. Timestamps are stored as Unix
|
|
// nanoseconds for stable ORDER BY semantics.
|
|
func (r *UploadRepo) Create(ctx context.Context, fileID, name string, size int64, sha256Hex string, uploadedAt time.Time) error {
|
|
_, err := r.db.sqlDB.ExecContext(ctx, `
|
|
INSERT INTO upload_files (file_id, name, size, sha256, uploaded_at)
|
|
VALUES (?, ?, ?, ?, ?)`,
|
|
fileID, name, size, sha256Hex, uploadedAt.UnixNano(),
|
|
)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
|
return fmt.Errorf("storage: create upload %s: primary key conflict: %w", fileID, err)
|
|
}
|
|
return fmt.Errorf("storage: create upload %s: %w", fileID, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Get retrieves an upload index record by file_id. If the record does not
|
|
// exist, it returns ErrNotFound.
|
|
func (r *UploadRepo) Get(ctx context.Context, fileID string) (UploadMeta, error) {
|
|
row := r.db.sqlDB.QueryRowContext(ctx, `
|
|
SELECT file_id, name, size, sha256, uploaded_at
|
|
FROM upload_files
|
|
WHERE file_id = ?`, fileID)
|
|
|
|
m, err := scanUpload(row.Scan)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return UploadMeta{}, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return UploadMeta{}, fmt.Errorf("storage: get upload %s: %w", fileID, err)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// List returns upload index records ordered by uploaded_at descending.
|
|
//
|
|
// If search is non-empty, file_name or any associated spark_submit app_id is
|
|
// filtered with a case-insensitive LIKE.
|
|
// A limit of zero or less falls back to 100; values above 500 are capped.
|
|
func (r *UploadRepo) List(ctx context.Context, search string, limit int) ([]UploadMeta, error) {
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
if limit > 500 {
|
|
limit = 500
|
|
}
|
|
|
|
var rows *sql.Rows
|
|
var err error
|
|
if search != "" {
|
|
rows, err = r.db.sqlDB.QueryContext(ctx, `
|
|
SELECT DISTINCT u.file_id, u.name, u.size, u.sha256, u.uploaded_at
|
|
FROM upload_files u
|
|
LEFT JOIN spark_submissions s ON u.file_id = s.file_id
|
|
WHERE u.name LIKE ? OR s.app_id LIKE ?
|
|
ORDER BY u.uploaded_at DESC
|
|
LIMIT ?`,
|
|
"%"+search+"%", "%"+search+"%", limit,
|
|
)
|
|
} else {
|
|
rows, err = r.db.sqlDB.QueryContext(ctx, `
|
|
SELECT u.file_id, u.name, u.size, u.sha256, u.uploaded_at
|
|
FROM upload_files u
|
|
ORDER BY u.uploaded_at DESC
|
|
LIMIT ?`, limit)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: list uploads: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []UploadMeta
|
|
for rows.Next() {
|
|
m, err := scanUpload(rows.Scan)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: list uploads: %w", err)
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("storage: list uploads: %w", err)
|
|
}
|
|
if out == nil {
|
|
// Force empty slice (not null) in JSON.
|
|
out = []UploadMeta{}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Delete removes an upload index record by file_id. If the record does not
|
|
// exist, it returns ErrNotFound.
|
|
func (r *UploadRepo) Delete(ctx context.Context, fileID string) error {
|
|
res, err := r.db.sqlDB.ExecContext(ctx, `DELETE FROM upload_files WHERE file_id = ?`, fileID)
|
|
if err != nil {
|
|
return fmt.Errorf("storage: delete upload %s: %w", fileID, err)
|
|
}
|
|
n, err := res.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("storage: delete upload %s: rows affected: %w", fileID, err)
|
|
}
|
|
if n == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func scanUpload(scan scanFunc) (UploadMeta, error) {
|
|
var m UploadMeta
|
|
var uploadedAt int64
|
|
if err := scan(&m.FileID, &m.Name, &m.Size, &m.SHA256, &uploadedAt); err != nil {
|
|
return UploadMeta{}, err
|
|
}
|
|
m.UploadedAt = time.Unix(0, uploadedAt)
|
|
return m, nil
|
|
}
|