refactor(scantrigger): drop redundant Target.LibraryID field
- Read library ID from Target.Folder.ID everywhere - Guard scan queue enqueue against nil Folder - Simplify admin API key auth error plumbing
This commit is contained in:
@@ -572,10 +572,9 @@ func (h *LibraryHandler) HandleCreateLibrary(w http.ResponseWriter, r *http.Requ
|
||||
} else {
|
||||
initialScanID := ulid.Make().String()
|
||||
h.recordAcceptedScan(initialScanID, &scantrigger.Target{
|
||||
Folder: folder,
|
||||
LibraryID: folder.ID,
|
||||
Mode: scantrigger.ModeLibrary,
|
||||
Trigger: "library_created",
|
||||
Folder: folder,
|
||||
Mode: scantrigger.ModeLibrary,
|
||||
Trigger: "library_created",
|
||||
})
|
||||
h.runFolderScanAsync(initialScanID, folder, "library_created")
|
||||
}
|
||||
@@ -663,10 +662,9 @@ func (h *LibraryHandler) HandleUpdateLibrary(w http.ResponseWriter, r *http.Requ
|
||||
} else {
|
||||
updateScanID := ulid.Make().String()
|
||||
h.recordAcceptedScan(updateScanID, &scantrigger.Target{
|
||||
Folder: folder,
|
||||
LibraryID: folder.ID,
|
||||
Mode: scantrigger.ModeLibrary,
|
||||
Trigger: "library_paths_changed",
|
||||
Folder: folder,
|
||||
Mode: scantrigger.ModeLibrary,
|
||||
Trigger: "library_paths_changed",
|
||||
})
|
||||
h.runFolderScanAsync(updateScanID, folder, "library_paths_changed")
|
||||
}
|
||||
@@ -819,8 +817,8 @@ func (h *LibraryHandler) HandleScan(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if h.ScanQueue != nil {
|
||||
if _, err := h.ScanQueue.EnqueueScan(r.Context(), target.LibraryID, target.Mode, target.Path, target.Trigger); err != nil {
|
||||
slog.Error("queueing library scan", "library_id", target.LibraryID, "error", err)
|
||||
if _, err := h.ScanQueue.EnqueueScan(r.Context(), target.Folder.ID, target.Mode, target.Path, target.Trigger); err != nil {
|
||||
slog.Error("queueing library scan", "library_id", target.Folder.ID, "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to queue scan")
|
||||
return
|
||||
}
|
||||
@@ -843,7 +841,7 @@ func (h *LibraryHandler) HandleScan(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusAccepted, scanResponse{
|
||||
Status: "accepted",
|
||||
Mode: target.Mode,
|
||||
LibraryID: target.LibraryID,
|
||||
LibraryID: target.Folder.ID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1057,7 +1055,7 @@ func (h *LibraryHandler) recordAcceptedScan(scanID string, target *scantrigger.T
|
||||
}
|
||||
h.ScanRegistry.Upsert(evt.ScanRun{
|
||||
ID: scanID,
|
||||
LibraryID: target.LibraryID,
|
||||
LibraryID: target.Folder.ID,
|
||||
Mode: target.Mode,
|
||||
Path: target.Path,
|
||||
Trigger: target.Trigger,
|
||||
|
||||
@@ -29,9 +29,11 @@ type AdminAPIKeyAuthenticator struct {
|
||||
}
|
||||
|
||||
type adminAPIKeyAuthResult struct {
|
||||
ctx context.Context
|
||||
status int
|
||||
ok bool
|
||||
ctx context.Context
|
||||
status int
|
||||
code string
|
||||
message string
|
||||
ok bool
|
||||
}
|
||||
|
||||
func NewAdminAPIKeyAuthenticator(keys apiKeyValidator, users apiKeyUserLoader) *AdminAPIKeyAuthenticator {
|
||||
@@ -50,7 +52,7 @@ func (a *AdminAPIKeyAuthenticator) RequireAdminAPIKey(next http.Handler) http.Ha
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
result := a.authenticate(r)
|
||||
if !result.ok {
|
||||
writeError(w, result.status, authErrorCode(result.status), authErrorMessage(result.status))
|
||||
writeError(w, result.status, result.code, result.message)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(result.ctx))
|
||||
@@ -64,7 +66,7 @@ func RequireSessionOrAdminAPIKey(sessionAuth *Authenticator, keyAuth *AdminAPIKe
|
||||
if ok && strings.HasPrefix(token, "sa_") {
|
||||
result := keyAuth.authenticate(r)
|
||||
if !result.ok {
|
||||
writeError(w, result.status, authErrorCode(result.status), authErrorMessage(result.status))
|
||||
writeError(w, result.status, result.code, result.message)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(result.ctx))
|
||||
@@ -76,23 +78,34 @@ func RequireSessionOrAdminAPIKey(sessionAuth *Authenticator, keyAuth *AdminAPIKe
|
||||
}
|
||||
|
||||
func (a *AdminAPIKeyAuthenticator) authenticate(r *http.Request) adminAPIKeyAuthResult {
|
||||
unauthorized := adminAPIKeyAuthResult{
|
||||
ctx: r.Context(),
|
||||
status: http.StatusUnauthorized,
|
||||
code: "Unauthorized",
|
||||
message: "Invalid API key",
|
||||
}
|
||||
if a == nil || a.keys == nil || a.users == nil {
|
||||
return adminAPIKeyAuthResult{ctx: r.Context(), status: http.StatusUnauthorized}
|
||||
return unauthorized
|
||||
}
|
||||
token, ok := ExtractToken(r)
|
||||
if !ok || !strings.HasPrefix(token, "sa_") {
|
||||
return adminAPIKeyAuthResult{ctx: r.Context(), status: http.StatusUnauthorized}
|
||||
return unauthorized
|
||||
}
|
||||
apiKey, err := a.keys.GetByKey(r.Context(), token)
|
||||
if err != nil || apiKey == nil {
|
||||
return adminAPIKeyAuthResult{ctx: r.Context(), status: http.StatusUnauthorized}
|
||||
return unauthorized
|
||||
}
|
||||
user, err := a.users.GetByID(r.Context(), apiKey.UserID)
|
||||
if err != nil || user == nil || !user.Enabled {
|
||||
return adminAPIKeyAuthResult{ctx: r.Context(), status: http.StatusUnauthorized}
|
||||
return unauthorized
|
||||
}
|
||||
if user.Role != "admin" {
|
||||
return adminAPIKeyAuthResult{ctx: context.WithValue(r.Context(), adminAPIKeyKey, false), status: http.StatusForbidden}
|
||||
return adminAPIKeyAuthResult{
|
||||
ctx: r.Context(),
|
||||
status: http.StatusForbidden,
|
||||
code: "Forbidden",
|
||||
message: "Admin access required",
|
||||
}
|
||||
}
|
||||
go func(id int64) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
@@ -107,17 +120,3 @@ func (a *AdminAPIKeyAuthenticator) authenticate(r *http.Request) adminAPIKeyAuth
|
||||
ok: true,
|
||||
}
|
||||
}
|
||||
|
||||
func authErrorCode(status int) string {
|
||||
if status == http.StatusForbidden {
|
||||
return "Forbidden"
|
||||
}
|
||||
return "Unauthorized"
|
||||
}
|
||||
|
||||
func authErrorMessage(status int) string {
|
||||
if status == http.StatusForbidden {
|
||||
return "Admin access required"
|
||||
}
|
||||
return "Invalid API key"
|
||||
}
|
||||
|
||||
@@ -59,8 +59,12 @@ func (q *fakeAutoscanQueue) EnqueueScans(_ context.Context, targets []scantrigge
|
||||
return q.batchErr
|
||||
}
|
||||
for _, target := range targets {
|
||||
folderID := 0
|
||||
if target.Folder != nil {
|
||||
folderID = target.Folder.ID
|
||||
}
|
||||
q.calls = append(q.calls, queuedScan{
|
||||
libraryID: target.LibraryID,
|
||||
libraryID: folderID,
|
||||
mode: target.Mode,
|
||||
path: target.Path,
|
||||
trigger: target.Trigger,
|
||||
|
||||
@@ -138,8 +138,11 @@ func (s *Service) EnqueueScans(ctx context.Context, targets []scantrigger.Target
|
||||
}
|
||||
inputs := make([]CreateInput, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
if target.Folder == nil {
|
||||
return fmt.Errorf("scan queue: target is missing folder")
|
||||
}
|
||||
inputs = append(inputs, CreateInput{
|
||||
LibraryID: target.LibraryID,
|
||||
LibraryID: target.Folder.ID,
|
||||
Mode: target.Mode,
|
||||
Path: target.Path,
|
||||
Trigger: target.Trigger,
|
||||
|
||||
@@ -36,12 +36,14 @@ type Request struct {
|
||||
Trigger string
|
||||
}
|
||||
|
||||
// Target is a fully-resolved scan request. Folder is always non-nil for
|
||||
// targets returned by Resolver; callers should read the library ID via
|
||||
// target.Folder.ID rather than tracking it separately.
|
||||
type Target struct {
|
||||
Folder *models.MediaFolder
|
||||
LibraryID int
|
||||
Mode string
|
||||
Path string
|
||||
Trigger string
|
||||
Folder *models.MediaFolder
|
||||
Mode string
|
||||
Path string
|
||||
Trigger string
|
||||
}
|
||||
|
||||
type RequestError struct {
|
||||
@@ -121,7 +123,7 @@ func (r *Resolver) resolve(ctx context.Context, req Request, pathFolders []*mode
|
||||
if folder != nil && !folder.Enabled {
|
||||
return nil, &RequestError{Status: http.StatusConflict, Code: "conflict", Message: "Library is disabled"}
|
||||
}
|
||||
return &Target{Folder: folder, LibraryID: folder.ID, Mode: ModeLibrary, Trigger: trigger}, nil
|
||||
return &Target{Folder: folder, Mode: ModeLibrary, Trigger: trigger}, nil
|
||||
}
|
||||
|
||||
cleanPath := filepath.Clean(req.Path)
|
||||
@@ -167,7 +169,7 @@ func (r *Resolver) resolve(ctx context.Context, req Request, pathFolders []*mode
|
||||
if mode == ModeLibrary {
|
||||
targetPath = ""
|
||||
}
|
||||
return &Target{Folder: folder, LibraryID: folder.ID, Mode: mode, Path: targetPath, Trigger: trigger}, nil
|
||||
return &Target{Folder: folder, Mode: mode, Path: targetPath, Trigger: trigger}, nil
|
||||
}
|
||||
|
||||
func EnqueueAll(ctx context.Context, queue Queuer, targets []Target) error {
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestResolverClassifiesLibraryRoot(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve returned error: %v", err)
|
||||
}
|
||||
if target.LibraryID != 7 || target.Mode != ModeLibrary || target.Path != "" {
|
||||
if target.Folder == nil || target.Folder.ID != 7 || target.Mode != ModeLibrary || target.Path != "" {
|
||||
t.Fatalf("unexpected target: %#v", target)
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func TestResolverClassifiesSubtree(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve returned error: %v", err)
|
||||
}
|
||||
if target.LibraryID != 8 || target.Mode != ModeSubtree || target.Path != filepath.Clean(subtree) {
|
||||
if target.Folder == nil || target.Folder.ID != 8 || target.Mode != ModeSubtree || target.Path != filepath.Clean(subtree) {
|
||||
t.Fatalf("unexpected target: %#v", target)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ func TestResolverClassifiesVideoFile(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve returned error: %v", err)
|
||||
}
|
||||
if target.LibraryID != 9 || target.Mode != ModeFile || target.Path != filepath.Clean(filePath) {
|
||||
if target.Folder == nil || target.Folder.ID != 9 || target.Mode != ModeFile || target.Path != filepath.Clean(filePath) {
|
||||
t.Fatalf("unexpected target: %#v", target)
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ type fakeQueue struct {
|
||||
}
|
||||
|
||||
func (q *fakeQueue) EnqueueScan(_ context.Context, folderID int, mode, path, trigger string) (bool, error) {
|
||||
q.calls = append(q.calls, Target{LibraryID: folderID, Mode: mode, Path: path, Trigger: trigger})
|
||||
q.calls = append(q.calls, Target{Folder: &models.MediaFolder{ID: folderID}, Mode: mode, Path: path, Trigger: trigger})
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -192,9 +192,10 @@ func (q *fakeQueue) EnqueueScans(_ context.Context, targets []Target) error {
|
||||
|
||||
func TestEnqueueAllUsesBatchQueue(t *testing.T) {
|
||||
queue := &fakeQueue{}
|
||||
folder := &models.MediaFolder{ID: 1}
|
||||
targets := []Target{
|
||||
{LibraryID: 1, Mode: ModeFile, Path: "/media/one.mkv", Trigger: "autoscan"},
|
||||
{LibraryID: 1, Mode: ModeFile, Path: "/media/two.mkv", Trigger: "autoscan"},
|
||||
{Folder: folder, Mode: ModeFile, Path: "/media/one.mkv", Trigger: "autoscan"},
|
||||
{Folder: folder, Mode: ModeFile, Path: "/media/two.mkv", Trigger: "autoscan"},
|
||||
}
|
||||
|
||||
if err := EnqueueAll(context.Background(), queue, targets); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user