ライブラリ・再生・削除を追加(Range 配信、パスの安全確認、共有ファイル名規則)
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func completeTestJob(t *testing.T, s *Store, id string) Job {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
job, _, err := s.Enqueue(ctx, sampleEpisode(id), "manual")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
claimed, err := s.ClaimNext(ctx)
|
||||
if err != nil || claimed.ID != job.ID {
|
||||
t.Fatalf("claim: %+v %v", claimed, err)
|
||||
}
|
||||
if err := s.Complete(ctx, job.ID, "番組/video.mp4", 1234); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := s.GetSucceededJob(ctx, job.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestLibraryStoreOperations(t *testing.T) {
|
||||
s := New(testPool(t, true))
|
||||
ctx := context.Background()
|
||||
first := completeTestJob(t, s, "library-1")
|
||||
second := completeTestJob(t, s, "library-2")
|
||||
jobs, err := s.ListSucceededJobs(ctx, 1)
|
||||
if err != nil || len(jobs) != 1 || jobs[0].ID != second.ID {
|
||||
t.Fatalf("完了一覧: %+v %v", jobs, err)
|
||||
}
|
||||
if first.FilePath == nil || *first.FilePath != "番組/video.mp4" || first.FileSizeBytes == nil || *first.FileSizeBytes != 1234 || first.FinishedAt == nil {
|
||||
t.Fatalf("完了情報: %+v", first)
|
||||
}
|
||||
queued, _, err := s.Enqueue(ctx, sampleEpisode("not-finished"), "manual")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.GetSucceededJob(ctx, queued.ID); !errors.Is(err, pgx.ErrNoRows) {
|
||||
t.Fatalf("未完了の取得: %v", err)
|
||||
}
|
||||
if err := s.DeleteSucceededJob(ctx, queued.ID); !errors.Is(err, ErrInvalidTransition) {
|
||||
t.Fatalf("未完了の削除: %v", err)
|
||||
}
|
||||
if err := s.DeleteSucceededJob(ctx, first.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.GetJob(ctx, first.ID); !errors.Is(err, pgx.ErrNoRows) {
|
||||
t.Fatalf("削除後の取得: %v", err)
|
||||
}
|
||||
if err := s.DeleteSucceededJob(ctx, first.ID); !errors.Is(err, ErrInvalidTransition) {
|
||||
t.Fatalf("再削除: %v", err)
|
||||
}
|
||||
if _, err := s.ListSucceededJobs(ctx, 0); err == nil {
|
||||
t.Fatal("0件の上限を許した")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSucceededJobKeepsKnownEpisodeAndAllowsManualRequeue(t *testing.T) {
|
||||
s := New(testPool(t, true))
|
||||
ctx := context.Background()
|
||||
ep := seriesEpisode("known-library", "known-series")
|
||||
created, err := s.RegisterSubscription(ctx, "known-series", "番組", []Episode{ep}, false)
|
||||
if err != nil || !created {
|
||||
t.Fatalf("番組登録: %t %v", created, err)
|
||||
}
|
||||
job, created, err := s.Enqueue(ctx, ep, "manual")
|
||||
if err != nil || !created {
|
||||
t.Fatalf("手動投入: %+v %t %v", job, created, err)
|
||||
}
|
||||
claimed, err := s.ClaimNext(ctx)
|
||||
if err != nil || claimed.ID != job.ID {
|
||||
t.Fatalf("取得: %+v %v", claimed, err)
|
||||
}
|
||||
if err := s.Complete(ctx, job.ID, "番組/video.mp4", 10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.DeleteSucceededJob(ctx, job.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unknown, err := s.UnknownEpisodeIDs(ctx, "known-series", []string{ep.ID})
|
||||
if err != nil || len(unknown) != 0 {
|
||||
t.Fatalf("削除後の既知記録: %v %v", unknown, err)
|
||||
}
|
||||
requeued, created, err := s.Enqueue(ctx, ep, "manual")
|
||||
if err != nil || !created || requeued.Status != "queued" || requeued.Source != "manual" {
|
||||
t.Fatalf("削除後の手動再投入: %+v %t %v", requeued, created, err)
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,36 @@ func (s *Store) GetJob(ctx context.Context, id int64) (Job, error) {
|
||||
return scanJob(s.pool.QueryRow(ctx, `SELECT `+jobColumns+` FROM download_jobs WHERE id=$1`, id))
|
||||
}
|
||||
|
||||
func (s *Store) GetSucceededJob(ctx context.Context, id int64) (Job, error) {
|
||||
return scanJob(s.pool.QueryRow(ctx, `SELECT `+jobColumns+` FROM download_jobs WHERE id=$1 AND status='succeeded'`, id))
|
||||
}
|
||||
|
||||
func (s *Store) ListSucceededJobs(ctx context.Context, limit int) ([]Job, error) {
|
||||
if limit <= 0 {
|
||||
return nil, errors.New("limit must be positive")
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `SELECT `+jobColumns+` FROM download_jobs
|
||||
WHERE status='succeeded' ORDER BY finished_at DESC NULLS LAST, id DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var jobs []Job
|
||||
for rows.Next() {
|
||||
job, err := scanJob(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
return jobs, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSucceededJob(ctx context.Context, id int64) error {
|
||||
tag, err := s.pool.Exec(ctx, `DELETE FROM download_jobs WHERE id=$1 AND status='succeeded'`, id)
|
||||
return transitionResult(tag.RowsAffected(), err)
|
||||
}
|
||||
|
||||
// Retry は失敗・中断されたジョブの実行時情報を消して再投入する。
|
||||
func (s *Store) Retry(ctx context.Context, id int64) error {
|
||||
tag, err := s.pool.Exec(ctx, `UPDATE download_jobs SET status='queued', phase=NULL,
|
||||
|
||||
Reference in New Issue
Block a user