refactor(backend): zero clippy warnings, gate CI with -D warnings

Auto-fix pass (uninlined format args, needless borrows, to_vec,
size_of_val, map_err->inspect_err) plus hand fixes: fully annotated the
sqlite-vec transmute (c_char for cross-platform correctness), type alias
for the duplicate-scan tuple, slice signatures over &mut Vec/&PathBuf,
and #[allow(dead_code)] documenting the intentionally dormant caption
worker. The CI clippy step now fails on any new warning.
This commit is contained in:
2026-06-12 20:59:40 +01:00
parent 178754f6c3
commit 1640e30330
11 changed files with 207 additions and 207 deletions
+126 -127
View File
@@ -232,7 +232,7 @@ pub async fn add_folder(
.asset_protocol_scope()
.allow_directory(&folder_path, true)
{
log::error!("Failed to allow asset scope for {}: {}", path, error);
log::error!("Failed to allow asset scope for {path}: {error}");
}
let name = folder_path
@@ -403,7 +403,7 @@ pub async fn update_folder_path(
) -> Result<(), String> {
let new_path_buf = PathBuf::from(&new_path);
if !new_path_buf.is_dir() {
return Err(format!("Path is not a valid directory: {}", new_path));
return Err(format!("Path is not a valid directory: {new_path}"));
}
// Let the webview load media from the relocated folder via the asset protocol.
@@ -411,7 +411,7 @@ pub async fn update_folder_path(
.asset_protocol_scope()
.allow_directory(&new_path_buf, true)
{
log::error!("Failed to allow asset scope for {}: {}", new_path, error);
log::error!("Failed to allow asset scope for {new_path}: {error}");
}
let new_name = new_path_buf
@@ -593,7 +593,7 @@ pub async fn semantic_search_images(
let has_filters = params.folder_id.is_some()
|| params.media_kind.is_some()
|| params.favorites_only.unwrap_or(false)
|| params.rating_min.map_or(false, |r| r > 0);
|| params.rating_min.is_some_and(|r| r > 0);
if !has_filters {
// No post-query filtering — a single fetch of exactly `limit` results is optimal.
@@ -894,7 +894,7 @@ pub async fn get_tag_cloud(
hasher.digest()
};
let folder_scope = match folder_id {
Some(id) => format!("folder_{}", id),
Some(id) => format!("folder_{id}"),
None => "all".to_string(),
};
@@ -1038,132 +1038,131 @@ pub async fn find_duplicates(
// For sample-hash groups that contain files > 64 KB, compute a full-file
// hash to confirm the match before presenting them as deletable duplicates.
let app_hash = app.clone();
let (pairs, skipped_before_confirm, candidate_total): (
Vec<(u64, u64, i64, String)>,
usize,
usize,
) = tokio::task::spawn_blocking(move || {
use memmap2::Mmap;
use rayon::prelude::*;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use xxhash_rust::xxh3::{xxh3_64, Xxh3};
// (size, sample/full hash, image_id, path) for each confirmed candidate.
type HashedCandidate = (u64, u64, i64, String);
let (pairs, skipped_before_confirm, candidate_total): (Vec<HashedCandidate>, usize, usize) =
tokio::task::spawn_blocking(move || {
use memmap2::Mmap;
use rayon::prelude::*;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use xxhash_rust::xxh3::{xxh3_64, Xxh3};
const WINDOW: usize = 16 * 1024; // 16 KB per sample window
const N: usize = 4; // windows at 0%, 33%, 66%, 100%
const COVERED: usize = WINDOW * N; // 64 KB total; also full-coverage threshold
const WINDOW: usize = 16 * 1024; // 16 KB per sample window
const N: usize = 4; // windows at 0%, 33%, 66%, 100%
const COVERED: usize = WINDOW * N; // 64 KB total; also full-coverage threshold
// Hash four evenly-spaced 16 KB windows. For files ≤ 64 KB this is
// equivalent to hashing the entire file.
fn sample(mmap: &[u8]) -> u64 {
if mmap.len() <= COVERED {
return xxh3_64(mmap);
// Hash four evenly-spaced 16 KB windows. For files ≤ 64 KB this is
// equivalent to hashing the entire file.
fn sample(mmap: &[u8]) -> u64 {
if mmap.len() <= COVERED {
return xxh3_64(mmap);
}
let mut h = Xxh3::new();
for i in 0..N {
let pos = (mmap.len() - WINDOW) * i / (N - 1);
h.update(&mmap[pos..pos + WINDOW]);
}
h.digest()
}
let mut h = Xxh3::new();
for i in 0..N {
let pos = (mmap.len() - WINDOW) * i / (N - 1);
h.update(&mmap[pos..pos + WINDOW]);
}
h.digest()
}
// ── Phase 1: stat ─────────────────────────────────────────────────
let stat_counter = AtomicUsize::new(0);
let stat_skipped = AtomicUsize::new(0);
let sized: Vec<(i64, String, u64)> = records
.par_iter()
.filter_map(|r| {
let result = match std::fs::metadata(&r.path) {
Ok(metadata) => Some((r.id, r.path.clone(), metadata.len())),
Err(_) => {
stat_skipped.fetch_add(1, Ordering::Relaxed);
None
// ── Phase 1: stat ─────────────────────────────────────────────────
let stat_counter = AtomicUsize::new(0);
let stat_skipped = AtomicUsize::new(0);
let sized: Vec<(i64, String, u64)> = records
.par_iter()
.filter_map(|r| {
let result = match std::fs::metadata(&r.path) {
Ok(metadata) => Some((r.id, r.path.clone(), metadata.len())),
Err(_) => {
stat_skipped.fetch_add(1, Ordering::Relaxed);
None
}
};
let done = stat_counter.fetch_add(1, Ordering::Relaxed) + 1;
if done % 100 == 0 || done == total {
let _ = app_hash.emit(
"duplicate_scan_progress",
DuplicateScanProgress {
phase: "checking".to_string(),
processed: done,
total,
skipped: stat_skipped.load(Ordering::Relaxed),
},
);
}
};
let done = stat_counter.fetch_add(1, Ordering::Relaxed) + 1;
if done % 100 == 0 || done == total {
let _ = app_hash.emit(
"duplicate_scan_progress",
DuplicateScanProgress {
phase: "checking".to_string(),
processed: done,
total,
skipped: stat_skipped.load(Ordering::Relaxed),
},
);
}
result
})
.collect();
let stat_skipped = stat_skipped.load(Ordering::Relaxed);
result
})
.collect();
let stat_skipped = stat_skipped.load(Ordering::Relaxed);
let mut by_size: HashMap<u64, Vec<usize>> = HashMap::new();
for (i, (_, _, size)) in sized.iter().enumerate() {
by_size.entry(*size).or_default().push(i);
}
let candidates: Vec<usize> = by_size
.into_values()
.filter(|g| g.len() > 1)
.flatten()
.collect();
let mut by_size: HashMap<u64, Vec<usize>> = HashMap::new();
for (i, (_, _, size)) in sized.iter().enumerate() {
by_size.entry(*size).or_default().push(i);
}
let candidates: Vec<usize> = by_size
.into_values()
.filter(|g| g.len() > 1)
.flatten()
.collect();
if candidates.is_empty() {
return (vec![], stat_skipped, 0);
}
if candidates.is_empty() {
return (vec![], stat_skipped, 0);
}
// ── Phase 2: sample hash ──────────────────────────────────────────
let c_total = candidates.len();
let _ = app_hash.emit(
"duplicate_scan_progress",
DuplicateScanProgress {
phase: "hashing".to_string(),
processed: 0,
total: c_total,
skipped: stat_skipped,
},
);
let counter = AtomicUsize::new(0);
let hash_skipped = AtomicUsize::new(0);
// ── Phase 2: sample hash ──────────────────────────────────────────
let c_total = candidates.len();
let _ = app_hash.emit(
"duplicate_scan_progress",
DuplicateScanProgress {
phase: "hashing".to_string(),
processed: 0,
total: c_total,
skipped: stat_skipped,
},
);
let counter = AtomicUsize::new(0);
let hash_skipped = AtomicUsize::new(0);
let pairs = candidates
.par_iter()
.filter_map(|&idx| {
let (id, path, size) = &sized[idx];
let result = if *size == 0 {
Some((xxh3_64(&[]), *size, *id, path.clone()))
} else {
std::fs::File::open(path)
.ok()
// SAFETY: read-only; no external truncation expected during scan.
.and_then(|file| unsafe { Mmap::map(&file).ok() })
.map(|mmap| (sample(&mmap), *size, *id, path.clone()))
};
if result.is_none() {
hash_skipped.fetch_add(1, Ordering::Relaxed);
}
let done = counter.fetch_add(1, Ordering::Relaxed) + 1;
if done % 100 == 0 || done == c_total {
let _ = app_hash.emit(
"duplicate_scan_progress",
DuplicateScanProgress {
phase: "hashing".to_string(),
processed: done,
total: c_total,
skipped: stat_skipped + hash_skipped.load(Ordering::Relaxed),
},
);
}
result
})
.collect();
(
pairs,
stat_skipped + hash_skipped.load(Ordering::Relaxed),
c_total,
)
})
.await
.map_err(|e| e.to_string())?;
let pairs = candidates
.par_iter()
.filter_map(|&idx| {
let (id, path, size) = &sized[idx];
let result = if *size == 0 {
Some((xxh3_64(&[]), *size, *id, path.clone()))
} else {
std::fs::File::open(path)
.ok()
// SAFETY: read-only; no external truncation expected during scan.
.and_then(|file| unsafe { Mmap::map(&file).ok() })
.map(|mmap| (sample(&mmap), *size, *id, path.clone()))
};
if result.is_none() {
hash_skipped.fetch_add(1, Ordering::Relaxed);
}
let done = counter.fetch_add(1, Ordering::Relaxed) + 1;
if done % 100 == 0 || done == c_total {
let _ = app_hash.emit(
"duplicate_scan_progress",
DuplicateScanProgress {
phase: "hashing".to_string(),
processed: done,
total: c_total,
skipped: stat_skipped + hash_skipped.load(Ordering::Relaxed),
},
);
}
result
})
.collect();
(
pairs,
stat_skipped + hash_skipped.load(Ordering::Relaxed),
c_total,
)
})
.await
.map_err(|e| e.to_string())?;
// Group by (sample_hash, file_size).
let mut size_hash_map: std::collections::HashMap<(u64, u64), Vec<(i64, String)>> =
@@ -1276,7 +1275,7 @@ pub async fn find_duplicates(
.filter_map(|(hash, file_size, ids)| {
let images = db::get_images_by_ids(&conn, &ids).ok()?;
Some(DuplicateGroup {
file_hash: format!("{:016x}", hash),
file_hash: format!("{hash:016x}"),
file_size,
images,
})
@@ -1288,7 +1287,7 @@ pub async fn find_duplicates(
// Persist results so they survive restart — best-effort, ignore errors.
let folder_scope = match folder_id {
Some(id) => format!("folder:{}", id),
Some(id) => format!("folder:{id}"),
None => "all".to_string(),
};
if let Ok(json) = serde_json::to_string(&groups) {
@@ -1309,7 +1308,7 @@ pub async fn load_duplicate_scan_cache(
folder_id: Option<i64>,
) -> Result<Option<DuplicateScanCache>, String> {
let folder_scope = match folder_id {
Some(id) => format!("folder:{}", id),
Some(id) => format!("folder:{id}"),
None => "all".to_string(),
};
let conn = db.get().map_err(|e| e.to_string())?;
@@ -1329,7 +1328,7 @@ pub async fn invalidate_duplicate_scan_cache(
folder_id: Option<i64>,
) -> Result<(), String> {
let folder_scope = match folder_id {
Some(id) => format!("folder:{}", id),
Some(id) => format!("folder:{id}"),
None => "all".to_string(),
};
let conn = db.get().map_err(|e| e.to_string())?;
@@ -1382,7 +1381,7 @@ fn dot(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn normalize(v: &mut Vec<f32>) {
fn normalize(v: &mut [f32]) {
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 1e-10 {
v.iter_mut().for_each(|x| *x /= norm);