feat(uploads): preview storage paths, detect templates, attach covers and withdraw batches
ci / module boundaries / boundaries (push) Successful in 7s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 55s
ci / ts / ts (packages + web) (push) Failing after 4m4s
ci / api / api (push) Successful in 55s
ci / db / db (push) Failing after 1m14s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m11s
ci / module boundaries / boundaries (push) Successful in 7s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 55s
ci / ts / ts (packages + web) (push) Failing after 4m4s
ci / api / api (push) Successful in 55s
ci / db / db (push) Failing after 1m14s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m11s
This commit is contained in:
1 parent
5e2d1e902b
commit
64dd8ed05e
29 files changed
+1219
-471
No files matched your search
@@ -98,7 +98,7 @@ type Reserved struct {
|
||||
|
||||
// CreateBatch создаёт пачку и резервирует место под файлы: проверка «занято + запрошено ≤ лимит»
|
||||
// под блокировкой строки счётчика (docs/uploads.md#3). Дубли по SHA-256 отклоняются, не занимая места.
|
||||
func (st *Store) CreateBatch(ctx context.Context, author uuid.UUID, comment *string, files []NewFile, s Settings) (uuid.UUID, []Reserved, error) {
|
||||
func (st *Store) CreateBatch(ctx context.Context, author uuid.UUID, comment *string, files []NewFile, s Settings, coverPath *string) (uuid.UUID, []Reserved, error) {
|
||||
tx, err := st.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return uuid.Nil, nil, fmt.Errorf("uploads: create batch: %w", err)
|
||||
@@ -116,8 +116,8 @@ func (st *Store) CreateBatch(ctx context.Context, author uuid.UUID, comment *str
|
||||
}
|
||||
|
||||
batchID := newID()
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO upload_batches (id, author_id, comment) VALUES ($1, $2, $3)`,
|
||||
batchID, author, comment); err != nil {
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO upload_batches (id, author_id, comment, cover_path) VALUES ($1, $2, $3, $4)`,
|
||||
batchID, author, comment, coverPath); err != nil {
|
||||
return uuid.Nil, nil, fmt.Errorf("uploads: insert batch: %w", err)
|
||||
}
|
||||
out := make([]Reserved, 0, len(files))
|
||||
@@ -231,9 +231,10 @@ const fileColumns = `f.id, f.batch_id, f.author_id, u.username, b.comment, f.sta
|
||||
f.size_bytes, f.received_bytes, f.sha256, coalesce(f.staging_path, ''), f.format, f.codec, f.bitrate, f.sample_rate,
|
||||
f.bit_depth, f.duration_ms, f.tags, f.metadata, f.metadata_edited, f.matched_artist_id, f.matched_album_id,
|
||||
f.duplicate_of_track_id, f.duplicate_level, f.possible_duplicate, f.quality_upgrade, f.target_source_id, f.target_rel_path,
|
||||
f.status_reason, f.reviewed_by, f.reviewed_at, f.imported_file_id, b.cover_path, f.created_at, f.updated_at`
|
||||
f.status_reason, f.reviewed_by, f.reviewed_at, f.imported_file_id, b.cover_path, f.created_at, f.updated_at, src.name`
|
||||
|
||||
const fileFrom = ` FROM upload_files f JOIN upload_batches b ON b.id = f.batch_id JOIN users u ON u.id = f.author_id`
|
||||
const fileFrom = ` FROM upload_files f JOIN upload_batches b ON b.id = f.batch_id JOIN users u ON u.id = f.author_id
|
||||
LEFT JOIN library_sources src ON src.id = f.target_source_id`
|
||||
|
||||
func scanFile(row pgx.Row) (File, error) {
|
||||
var (
|
||||
@@ -244,7 +245,7 @@ func scanFile(row pgx.Row) (File, error) {
|
||||
&f.ClientRelPath, &f.Size, &f.Received, &f.SHA256, &f.StagingPath, &f.Format, &f.Codec, &f.Bitrate, &f.SampleRate,
|
||||
&f.BitDepth, &f.DurationMs, &tags, &meta, &f.MetadataEdited, &f.MatchedArtistID, &f.MatchedAlbumID,
|
||||
&f.DuplicateOfTrackID, &f.DuplicateLevel, &f.PossibleDuplicate, &f.QualityUpgrade, &f.TargetSourceID, &f.TargetRelPath,
|
||||
&f.StatusReason, &f.ReviewedBy, &f.ReviewedAt, &f.ImportedFileID, &f.CoverPath, &f.CreatedAt, &f.UpdatedAt)
|
||||
&f.StatusReason, &f.ReviewedBy, &f.ReviewedAt, &f.ImportedFileID, &f.CoverPath, &f.CreatedAt, &f.UpdatedAt, &f.TargetSourceName)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return File{}, ErrNotFound
|
||||
}
|
||||
@@ -348,7 +349,9 @@ func (st *Store) NextWithStatus(ctx context.Context, s Status) (File, error) {
|
||||
|
||||
// StagingInUse — пути staging файлов, которым он ещё нужен (сверка при старте).
|
||||
func (st *Store) StagingInUse(ctx context.Context) (map[string]bool, error) {
|
||||
rows, err := st.pool.Query(ctx, `SELECT staging_path FROM upload_files WHERE status::text = ANY($1) AND staging_path IS NOT NULL`,
|
||||
rows, err := st.pool.Query(ctx, `SELECT staging_path FROM upload_files WHERE status::text = ANY($1) AND staging_path IS NOT NULL
|
||||
UNION SELECT b.cover_path FROM upload_batches b JOIN upload_files f ON f.batch_id = b.id
|
||||
WHERE f.status::text = ANY($1) AND b.cover_path IS NOT NULL`,
|
||||
occupyingStatuses)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("uploads: staging in use: %w", err)
|
||||
@@ -364,6 +367,17 @@ func (st *Store) StagingInUse(ctx context.Context) (map[string]bool, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UnusedBatchCover returns a cover only after every file in its batch has released staging.
|
||||
func (st *Store) UnusedBatchCover(ctx context.Context, batch uuid.UUID) (*string, error) {
|
||||
var cover *string
|
||||
err := st.pool.QueryRow(ctx, `SELECT b.cover_path FROM upload_batches b WHERE b.id = $1
|
||||
AND NOT EXISTS (SELECT 1 FROM upload_files f WHERE f.batch_id = b.id AND f.status::text = ANY($2))`, batch, occupyingStatuses).Scan(&cover)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return cover, err
|
||||
}
|
||||
|
||||
// Stale — недокачанные файлы старше before.
|
||||
func (st *Store) Stale(ctx context.Context, before time.Time) ([]uuid.UUID, error) {
|
||||
rows, err := st.pool.Query(ctx, `SELECT id FROM upload_files WHERE status = 'uploading' AND updated_at < $1`, before)
|
||||
|
||||
Reference in new issue
Block a user