diff --git a/bruno/libraries/scan_library.yml b/bruno/libraries/scan_library.yml index c61a6e74..3d145d8f 100644 --- a/bruno/libraries/scan_library.yml +++ b/bruno/libraries/scan_library.yml @@ -1,7 +1,7 @@ info: name: Scan Library type: http - seq: 18 + seq: 19 http: method: POST diff --git a/docs/developer/api/libraries/scan_library.md b/docs/developer/api/libraries/scan_library.md index 3cb74597..ac45693b 100644 --- a/docs/developer/api/libraries/scan_library.md +++ b/docs/developer/api/libraries/scan_library.md @@ -8,10 +8,17 @@ area: libraries # Scan Library -Enqueues a full scan of one library's configured folders. The library id comes +Enqueues a scan of one library's configured folders. The library id comes from the path — no request body is needed (the generic `POST /api/scanner/scan` variant is the one that takes `folder_paths`). +**Per-folder scope**: an optional body of `{"folder_path": "…"}` restricts the +scan to ONE of the library's registered folders (the per-folder Scan buttons +in the library panel). The path must be a registered folder — anything else is +a **400**. A scoped scan never runs archive reconciliation for the library's +other folders, so items outside the scanned folder cannot be marked missing +by it. + Scans are **coalesced**: if a scan for this library is already pending or running you get **409** with the active job id instead of a second scan. @@ -29,7 +36,7 @@ running you get **409** with the active job id instead of a second scan. | Status | Body | When | | ------ | ---- | ---- | -| 400 | `{"error": "invalid library id"}` / `{"error": "library has no folders configured"}` | malformed id / empty library | +| 400 | `{"error": "invalid library id"}` / `{"error": "library has no folders configured"}` / `{"error": "folder_path is not one of this library's registered folders"}` | malformed id / empty library / unregistered folder scope | | 404 | `{"error": "library not found or has no folders"}` | unknown library | | 409 | `{"error": "a scan for this library is already pending or running", "active_job_id": "..."}` | scan coalescing | | 500 | `{"error": "failed to enqueue scan job: ..."}` | queue failure | diff --git a/internal/handlers/scanner.go b/internal/handlers/scanner.go index 6ca4ac15..126c21c9 100644 --- a/internal/handlers/scanner.go +++ b/internal/handlers/scanner.go @@ -27,7 +27,12 @@ func (h *Handler) StartBackgroundTasks() { type ScanLibraryRequest struct { FolderPaths []string `json:"folder_paths,omitempty"` LibraryID string `json:"library_id,omitempty"` - Force bool `json:"force,omitempty"` + // FolderPath (singular) scopes a library scan to ONE of the library's + // registered folders (the LibraryPanel per-folder Scan buttons). It + // must be a registered folder — validated below, never trusted. The + // raw /api/scanner/scan route ignores it (no library attribution). + FolderPath string `json:"folder_path,omitempty"` + Force bool `json:"force,omitempty"` } // ScanLibrary handles POST /api/scanner/scan (now runs in background) @@ -68,9 +73,28 @@ func (h *Handler) ScanLibrary(c *echo.Context) error { return c.JSON(http.StatusNotFound, map[string]string{"error": "library not found or has no folders"}) } - // Extract folder paths - for _, folder := range libraryFolders { - folderPaths = append(folderPaths, folder.FolderPath) + if req.FolderPath != "" { + // Per-folder scope (LibraryPanel per-folder Scan buttons): + // scan exactly one registered folder's subtree. The archive + // reconciliation pass skips partially-covered libraries + // (processScanJob → ScanFolders), so the other folders' + // items cannot be marked missing by this run. + registered := false + for _, folder := range libraryFolders { + if folder.FolderPath == req.FolderPath { + registered = true + break + } + } + if !registered { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "folder_path is not one of this library's registered folders"}) + } + folderPaths = []string{req.FolderPath} + } else { + // Extract folder paths + for _, folder := range libraryFolders { + folderPaths = append(folderPaths, folder.FolderPath) + } } if len(folderPaths) == 0 { diff --git a/templates/admin_library.templ b/templates/admin_library.templ index b708d33f..d8b7adab 100644 --- a/templates/admin_library.templ +++ b/templates/admin_library.templ @@ -267,6 +267,18 @@ templ LibraryPanel(user User, libraryID string, library LibraryData, folders []F for _, folder := range folders {
{ folder.FolderPath }
+
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var39 string
- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(currentPath)
+ var templ_7745c5c3_Var41 string
+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(currentPath)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 381, Col: 89}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 393, Col: 89}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "