perf(api,indexer): optimiser pages, thumbnails, watcher et robustesse fd

- Pages: mode Original (zero-transcoding), ETag/304, cache index CBZ,
  préfetch next 2 pages, filtre Triangle par défaut
- Thumbnails: DCT scaling JPEG via jpeg-decoder (decode 7x plus rapide),
  img.thumbnail() pour resize, support format Original, fix JPEG RGBA8
- API fallback thumbnail: OutputFormat::Original + DCT scaling au lieu
  de WebP full-decode, retour (bytes, content_type) dynamique
- Watcher: remplacement notify par poll léger sans inotify/fd,
  skip poll quand job actif, snapshots en mémoire
- Jobs: mutex exclusif corrigé (tous statuts actifs, tous types exclusifs)
- Robustesse: suppression fs::canonicalize (problèmes fd Docker),
  list_folders avec erreurs explicites, has_children default true
- Backoffice: FormRow items-start pour alignement inputs avec helper text,
  labels settings clarifiés

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 23:07:42 +01:00
parent fe54f55f47
commit 6947af10fe
15 changed files with 711 additions and 395 deletions

View File

@@ -246,9 +246,9 @@ pub async fn list_folders(
base_path.to_path_buf()
};
// Ensure the path is within the libraries root
let canonical_target = target_path.canonicalize().unwrap_or(target_path.clone());
let canonical_base = base_path.canonicalize().unwrap_or(base_path.to_path_buf());
// Ensure the path is within the libraries root (avoid canonicalize — burns fd on Docker mounts)
let canonical_target = target_path.clone();
let canonical_base = base_path.to_path_buf();
if !canonical_target.starts_with(&canonical_base) {
return Err(ApiError::bad_request("Path is outside libraries root"));
@@ -263,19 +263,31 @@ pub async fn list_folders(
0
};
if let Ok(entries) = std::fs::read_dir(&canonical_target) {
for entry in entries.flatten() {
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
let entries = std::fs::read_dir(&canonical_target)
.map_err(|e| ApiError::internal(format!("cannot read directory {}: {}", canonical_target.display(), e)))?;
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => {
tracing::warn!("[FOLDERS] entry error in {}: {}", canonical_target.display(), e);
continue;
}
};
let is_dir = match entry.file_type() {
Ok(ft) => ft.is_dir(),
Err(e) => {
tracing::warn!("[FOLDERS] cannot stat {}: {}", entry.path().display(), e);
continue;
}
};
if is_dir {
let name = entry.file_name().to_string_lossy().to_string();
// Check if this folder has children
let has_children = if let Ok(sub_entries) = std::fs::read_dir(entry.path()) {
sub_entries.flatten().any(|e| {
e.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
})
} else {
false
};
// Check if this folder has children (best-effort, default to true on error)
let has_children = std::fs::read_dir(entry.path())
.map(|sub| sub.flatten().any(|e| e.file_type().map(|ft| ft.is_dir()).unwrap_or(false)))
.unwrap_or(true);
// Calculate the full path relative to libraries root
let full_path = if let Ok(relative) = entry.path().strip_prefix(&canonical_base) {
@@ -290,7 +302,6 @@ pub async fn list_folders(
depth,
has_children,
});
}
}
}