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:
Silo Server Migration
2026-05-25 12:11:56 -04:00
parent 6a1189f2d8
commit a05a0d26a2
6 changed files with 58 additions and 51 deletions
+10 -12
View File
@@ -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,
+23 -24
View File
@@ -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,
+4 -1
View File
@@ -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,
+9 -7
View File
@@ -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 {
+7 -6
View File
@@ -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 {