style(backend): apply rustfmt across the backend

Mechanical cargo fmt pass so the CI rustfmt gate starts green; no semantic
changes (verified token-level + cargo check).
This commit is contained in:
2026-06-12 18:36:31 +01:00
parent 69e53ed62a
commit 1d782a6d57
6 changed files with 242 additions and 131 deletions
+6 -2
View File
@@ -15,13 +15,17 @@ fn main() {
if let Ok(path) = &cuda_path {
println!("cargo:warning=CUDA Toolkit found at {path} — GPU acceleration enabled.");
} else {
println!("cargo:warning=CUDA Toolkit (nvcc) found in PATH — GPU acceleration enabled.");
println!(
"cargo:warning=CUDA Toolkit (nvcc) found in PATH — GPU acceleration enabled."
);
}
} else {
println!("cargo:warning=━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("cargo:warning= candle-cuda feature is enabled but no CUDA Toolkit found.");
println!("cargo:warning= Install CUDA Toolkit from https://developer.nvidia.com/cuda-downloads");
println!("cargo:warning= Or build without GPU support: cargo build --no-default-features");
println!(
"cargo:warning= Or build without GPU support: cargo build --no-default-features"
);
println!("cargo:warning=━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
}
}
+157 -91
View File
@@ -445,7 +445,7 @@ pub async fn find_similar_images(
offset,
limit + 1,
)
.map_err(|e| e.to_string())?;
.map_err(|e| e.to_string())?;
let has_more = matches.len() > limit;
let image_ids = matches
.into_iter()
@@ -499,8 +499,9 @@ pub async fn find_similar_by_region(
None => {
// Fetch one extra candidate to compensate for the source image that
// will be removed, so has_more is accurate and results span multiple pages.
let mut ids = vector::search_image_ids_by_embedding(&conn, &embedding, offset + limit + 2)
.map_err(|e| e.to_string())?;
let mut ids =
vector::search_image_ids_by_embedding(&conn, &embedding, offset + limit + 2)
.map_err(|e| e.to_string())?;
ids.retain(|&id| id != params.image_id);
ids
}
@@ -639,7 +640,12 @@ pub async fn search_images_by_tag(
offset,
)
.map_err(|e| e.to_string())?;
Ok(TagSearchPage { images, total, offset, limit })
Ok(TagSearchPage {
images,
total,
offset,
limit,
})
}
#[tauri::command]
@@ -835,7 +841,6 @@ pub struct TagCloudEntry {
pub image_ids: Vec<i64>,
}
/// Clusters the library's image embeddings with k-means and returns one representative
/// image per cluster — the member whose embedding is closest to its cluster centroid.
/// Results are cached in SQLite keyed by a hash of the embedded image IDs, so repeated
@@ -957,7 +962,8 @@ pub async fn get_explore_tags(
params: GetExploreTagsParams,
) -> Result<Vec<ExploreTagEntry>, String> {
let conn = db.get().map_err(|e| e.to_string())?;
db::get_explore_tags(&conn, params.folder_id, params.limit.unwrap_or(48)).map_err(|e| e.to_string())
db::get_explore_tags(&conn, params.folder_id, params.limit.unwrap_or(48))
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -969,8 +975,13 @@ pub async fn search_tags_autocomplete(
return Ok(vec![]);
}
let conn = db.get().map_err(|e| e.to_string())?;
db::search_tags_autocomplete(&conn, &params.query, params.folder_id, params.limit.unwrap_or(10))
.map_err(|e| e.to_string())
db::search_tags_autocomplete(
&conn,
&params.query,
params.folder_id,
params.limit.unwrap_or(10),
)
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -985,12 +996,15 @@ pub async fn find_duplicates(
};
let total = records.len();
let _ = app.emit("duplicate_scan_progress", DuplicateScanProgress {
phase: "checking".to_string(),
processed: 0,
total,
skipped: 0,
});
let _ = app.emit(
"duplicate_scan_progress",
DuplicateScanProgress {
phase: "checking".to_string(),
processed: 0,
total,
skipped: 0,
},
);
// Three-phase detection.
//
@@ -1007,8 +1021,11 @@ 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 || {
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;
@@ -1016,7 +1033,7 @@ pub async fn find_duplicates(
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 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
@@ -1048,12 +1065,15 @@ pub async fn find_duplicates(
};
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 _ = app_hash.emit(
"duplicate_scan_progress",
DuplicateScanProgress {
phase: "checking".to_string(),
processed: done,
total,
skipped: stat_skipped.load(Ordering::Relaxed),
},
);
}
result
})
@@ -1076,12 +1096,15 @@ pub async fn find_duplicates(
// ── 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 _ = 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);
@@ -1103,12 +1126,15 @@ pub async fn find_duplicates(
}
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),
});
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
})
@@ -1126,7 +1152,10 @@ pub async fn find_duplicates(
let mut size_hash_map: std::collections::HashMap<(u64, u64), Vec<(i64, String)>> =
std::collections::HashMap::new();
for (hash, file_size, id, path) in pairs {
size_hash_map.entry((hash, file_size)).or_default().push((id, path));
size_hash_map
.entry((hash, file_size))
.or_default()
.push((id, path));
}
// Phase 3: for large-file groups (> 64 KB) the sample hash is not exact;
@@ -1140,12 +1169,15 @@ pub async fn find_duplicates(
.map(|(_, entries)| entries.len())
.sum();
if confirming_total > 0 {
let _ = app.emit("duplicate_scan_progress", DuplicateScanProgress {
phase: "confirming".to_string(),
processed: 0,
total: confirming_total,
skipped: skipped_before_confirm,
});
let _ = app.emit(
"duplicate_scan_progress",
DuplicateScanProgress {
phase: "confirming".to_string(),
processed: 0,
total: confirming_total,
skipped: skipped_before_confirm,
},
);
}
let app_confirm = app.clone();
let (confirmed, skipped_files): (Vec<(u64, u64, Vec<i64>)>, usize) =
@@ -1163,7 +1195,11 @@ pub async fn find_duplicates(
.flat_map(|((sample_hash, file_size), entries)| {
if file_size <= COVERED {
// Sample was already a full hash — one group, no re-read needed.
return vec![(sample_hash, file_size, entries.into_iter().map(|(id, _)| id).collect())];
return vec![(
sample_hash,
file_size,
entries.into_iter().map(|(id, _)| id).collect(),
)];
}
// Full-file hash pass. Group by full hash so that two sets of
// files that collide only in the sample produce separate groups.
@@ -1179,13 +1215,16 @@ pub async fn find_duplicates(
}
let done = counter.fetch_add(1, Ordering::Relaxed) + 1;
if done % 100 == 0 || done == confirming_total {
let _ = app_confirm.emit("duplicate_scan_progress", DuplicateScanProgress {
phase: "confirming".to_string(),
processed: done,
total: confirming_total,
skipped: skipped_before_confirm
+ confirm_skipped.load(Ordering::Relaxed),
});
let _ = app_confirm.emit(
"duplicate_scan_progress",
DuplicateScanProgress {
phase: "confirming".to_string(),
processed: done,
total: confirming_total,
skipped: skipped_before_confirm
+ confirm_skipped.load(Ordering::Relaxed),
},
);
}
(*id, hash)
})
@@ -1470,16 +1509,17 @@ pub async fn get_worker_states(folder_ids: Vec<i64>) -> Result<Vec<FolderWorkerS
Ok(folder_ids
.into_iter()
.map(|folder_id| {
let state = states
.get(&folder_id)
.copied()
.unwrap_or(indexer::FolderWorkerPausedState {
thumbnail: false,
metadata: false,
embedding: false,
caption: false,
tagging: false,
});
let state =
states
.get(&folder_id)
.copied()
.unwrap_or(indexer::FolderWorkerPausedState {
thumbnail: false,
metadata: false,
embedding: false,
caption: false,
tagging: false,
});
FolderWorkerStates {
folder_id,
thumbnail_paused: state.thumbnail,
@@ -1631,7 +1671,11 @@ pub async fn queue_tagging_jobs(
) -> Result<usize, String> {
let conn = db.get().map_err(|e| e.to_string())?;
let requested_folder_ids = params.folder_ids.unwrap_or_default();
let (total, folder_ids) = match (params.folder_id, params.image_id, requested_folder_ids.is_empty()) {
let (total, folder_ids) = match (
params.folder_id,
params.image_id,
requested_folder_ids.is_empty(),
) {
(_, Some(image_id), _) => {
db::enqueue_tagging_job(&conn, image_id).map_err(|e| e.to_string())?;
// Look up just this image's folder_id rather than fetching all folders
@@ -1675,27 +1719,29 @@ pub async fn clear_tagging_jobs(
) -> Result<usize, String> {
let conn = db.get().map_err(|e| e.to_string())?;
let requested_folder_ids = params.folder_ids.unwrap_or_default();
let (n, folder_ids): (usize, Vec<i64>) = match (params.folder_id, requested_folder_ids.is_empty()) {
(Some(id), _) => (
db::clear_tagging_jobs(&conn, Some(id)).map_err(|e| e.to_string())?,
vec![id],
),
(None, false) => {
let mut total = 0usize;
for &folder_id in &requested_folder_ids {
total += db::clear_tagging_jobs(&conn, Some(folder_id)).map_err(|e| e.to_string())?;
let (n, folder_ids): (usize, Vec<i64>) =
match (params.folder_id, requested_folder_ids.is_empty()) {
(Some(id), _) => (
db::clear_tagging_jobs(&conn, Some(id)).map_err(|e| e.to_string())?,
vec![id],
),
(None, false) => {
let mut total = 0usize;
for &folder_id in &requested_folder_ids {
total += db::clear_tagging_jobs(&conn, Some(folder_id))
.map_err(|e| e.to_string())?;
}
(total, requested_folder_ids)
}
(total, requested_folder_ids)
}
(None, true) => (
db::clear_tagging_jobs(&conn, None).map_err(|e| e.to_string())?,
db::get_folders(&conn)
.map_err(|e| e.to_string())?
.into_iter()
.map(|f| f.id)
.collect(),
),
};
(None, true) => (
db::clear_tagging_jobs(&conn, None).map_err(|e| e.to_string())?,
db::get_folders(&conn)
.map_err(|e| e.to_string())?
.into_iter()
.map(|f| f.id)
.collect(),
),
};
drop(conn);
indexer::emit_folder_job_progress(&app, db.inner(), &folder_ids, true);
Ok(n)
@@ -1747,7 +1793,11 @@ pub async fn get_tagging_queue_scope(app: AppHandle) -> Result<String, String> {
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
let path = app_dir.join(TAGGING_QUEUE_SCOPE_FILE);
let value = std::fs::read_to_string(path).unwrap_or_default();
Ok(if value.trim() == "selected" { "selected".to_string() } else { "all".to_string() })
Ok(if value.trim() == "selected" {
"selected".to_string()
} else {
"all".to_string()
})
}
#[tauri::command]
@@ -1760,7 +1810,11 @@ pub async fn set_tagging_queue_scope(
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let value = if params.scope == "selected" { "selected" } else { "all" };
let value = if params.scope == "selected" {
"selected"
} else {
"all"
};
std::fs::write(path, value).map_err(|e| e.to_string())?;
Ok(value.to_string())
}
@@ -1825,14 +1879,21 @@ pub struct VacuumResult {
}
#[tauri::command]
pub async fn get_database_info(app: AppHandle, db: State<'_, DbState>) -> Result<DatabaseInfo, String> {
pub async fn get_database_info(
app: AppHandle,
db: State<'_, DbState>,
) -> Result<DatabaseInfo, String> {
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
let db_path = app_dir.join("gallery.db");
let size_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
let conn = db.get().map_err(|e| e.to_string())?;
let page_size: i64 = conn.query_row("PRAGMA page_size", [], |r| r.get(0)).map_err(|e| e.to_string())?;
let freelist_count: i64 = conn.query_row("PRAGMA freelist_count", [], |r| r.get(0)).map_err(|e| e.to_string())?;
let page_size: i64 = conn
.query_row("PRAGMA page_size", [], |r| r.get(0))
.map_err(|e| e.to_string())?;
let freelist_count: i64 = conn
.query_row("PRAGMA freelist_count", [], |r| r.get(0))
.map_err(|e| e.to_string())?;
Ok(DatabaseInfo {
size_mb: size_bytes as f64 / 1_048_576.0,
@@ -1841,13 +1902,17 @@ pub async fn get_database_info(app: AppHandle, db: State<'_, DbState>) -> Result
}
#[tauri::command]
pub async fn vacuum_database(app: AppHandle, db: State<'_, DbState>) -> Result<VacuumResult, String> {
pub async fn vacuum_database(
app: AppHandle,
db: State<'_, DbState>,
) -> Result<VacuumResult, String> {
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
let db_path = app_dir.join("gallery.db");
let before_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
let conn = db.get().map_err(|e| e.to_string())?;
conn.execute_batch("PRAGMA wal_checkpoint(FULL); VACUUM;").map_err(|e| e.to_string())?;
conn.execute_batch("PRAGMA wal_checkpoint(FULL); VACUUM;")
.map_err(|e| e.to_string())?;
drop(conn);
let after_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
@@ -1870,7 +1935,9 @@ pub struct CleanupOrphanedThumbnailsResult {
pub freed_mb: f64,
}
fn collect_db_thumbnail_filenames(conn: &rusqlite::Connection) -> Result<std::collections::HashSet<String>, String> {
fn collect_db_thumbnail_filenames(
conn: &rusqlite::Connection,
) -> Result<std::collections::HashSet<String>, String> {
let mut stmt = conn
.prepare("SELECT thumbnail_path FROM images WHERE thumbnail_path IS NOT NULL")
.map_err(|e| e.to_string())?;
@@ -1993,8 +2060,7 @@ pub async fn set_muted_folder_ids(
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
std::fs::write(settings_dir.join("muted_folder_ids.txt"), content)
.map_err(|e| e.to_string())
std::fs::write(settings_dir.join("muted_folder_ids.txt"), content).map_err(|e| e.to_string())
}
#[tauri::command]
+30 -18
View File
@@ -318,9 +318,7 @@ pub fn migrate(conn: &Connection) -> Result<()> {
// Index must be created after ensure_column adds the column; it cannot live
// in the execute_batch above because that batch runs before the column exists
// on databases that predate Phase 1.
conn.execute_batch(
"CREATE INDEX IF NOT EXISTS idx_images_taken_at ON images(taken_at);",
)?;
conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_images_taken_at ON images(taken_at);")?;
ensure_column(conn, "folders", "scan_error", "TEXT")?;
vector::migrate(conn)?;
@@ -1410,7 +1408,10 @@ pub fn update_image_details(
/// Look up the lightweight indexed-media entry for a single path.
/// Used by the filesystem watcher to run change-detection before upserting.
pub fn get_indexed_entry_by_path(conn: &Connection, path: &str) -> Result<Option<IndexedMediaEntry>> {
pub fn get_indexed_entry_by_path(
conn: &Connection,
path: &str,
) -> Result<Option<IndexedMediaEntry>> {
let result = conn.query_row(
"SELECT id, path, modified_at, file_size, media_kind FROM images WHERE path = ?1",
[path],
@@ -1434,11 +1435,9 @@ pub fn get_indexed_entry_by_path(conn: &Connection, path: &str) -> Result<Option
/// Look up just the image id for a path. Used by the filesystem watcher
/// to find the DB row to delete when a file is removed from disk.
pub fn get_image_id_by_path(conn: &Connection, path: &str) -> Result<Option<i64>> {
let result = conn.query_row(
"SELECT id FROM images WHERE path = ?1",
[path],
|row| row.get(0),
);
let result = conn.query_row("SELECT id FROM images WHERE path = ?1", [path], |row| {
row.get(0)
});
match result {
Ok(id) => Ok(Some(id)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
@@ -1474,8 +1473,9 @@ pub fn get_images_by_ids(conn: &Connection, image_ids: &[i64]) -> Result<Vec<Ima
}
pub fn get_folders(conn: &Connection) -> Result<Vec<Folder>> {
let mut stmt =
conn.prepare("SELECT id, path, name, image_count, indexed_at, scan_error FROM folders ORDER BY name")?;
let mut stmt = conn.prepare(
"SELECT id, path, name, image_count, indexed_at, scan_error FROM folders ORDER BY name",
)?;
let rows = stmt.query_map([], |row| {
Ok(Folder {
id: row.get(0)?,
@@ -1497,7 +1497,13 @@ pub fn rename_folder(conn: &Connection, folder_id: i64, new_name: &str) -> Resul
Ok(())
}
pub fn update_folder_path(conn: &Connection, folder_id: i64, old_path: &str, new_path: &str, new_name: &str) -> Result<()> {
pub fn update_folder_path(
conn: &Connection,
folder_id: i64,
old_path: &str,
new_path: &str,
new_name: &str,
) -> Result<()> {
// Both updates must be atomic: if the image path rewrite fails (e.g. a
// uniqueness collision) the folder row must not remain at the new location.
let tx = conn.unchecked_transaction()?;
@@ -1659,7 +1665,13 @@ pub fn search_images_by_tag(
AND (?3 = 0 OR i.favorite = 1)
AND i.rating >= ?4
AND LOWER(TRIM(t.tag)) = ?5",
params![folder_id, media_kind, favorites_flag, rating_min, normalized_query],
params![
folder_id,
media_kind,
favorites_flag,
rating_min,
normalized_query
],
|row| row.get::<_, i64>(0),
)? as usize;
@@ -1769,10 +1781,7 @@ pub fn get_image_id_and_thumbnail_by_path(
/// Returns all non-null thumbnail_path values for images in a folder.
/// Called before folder deletion so callers can remove the files from disk.
pub fn get_thumbnail_paths_for_folder(
conn: &Connection,
folder_id: i64,
) -> Result<Vec<String>> {
pub fn get_thumbnail_paths_for_folder(conn: &Connection, folder_id: i64) -> Result<Vec<String>> {
let mut stmt = conn.prepare(
"SELECT thumbnail_path FROM images WHERE folder_id = ?1 AND thumbnail_path IS NOT NULL",
)?;
@@ -2054,7 +2063,10 @@ pub fn clear_tagging_jobs(conn: &Connection, folder_id: Option<i64>) -> Result<u
WHERE status = 'processing'",
[],
)?;
let n = conn.execute("DELETE FROM tagging_jobs WHERE status NOT IN ('processing', 'cancelled')", [])?;
let n = conn.execute(
"DELETE FROM tagging_jobs WHERE status NOT IN ('processing', 'cancelled')",
[],
)?;
conn.execute(
"UPDATE images SET ai_tagger_error = NULL WHERE ai_tagged_at IS NULL",
[],
+46 -13
View File
@@ -7,9 +7,9 @@ use crate::tagger::{self, WdTagger};
use crate::thumbnail;
use crate::vector;
use anyhow::Result;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use serde::Serialize;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
@@ -1401,7 +1401,11 @@ impl WatcherHandle {
}
}
}
self.inner.folder_map.lock().unwrap().insert(path, folder_id);
self.inner
.folder_map
.lock()
.unwrap()
.insert(path, folder_id);
}
pub fn remove_folder(&self, path: &Path) {
@@ -1446,7 +1450,9 @@ pub fn start_watcher(app: AppHandle, pool: DbPool, thumb_dir: PathBuf) -> Watche
// Seed the folder map from the DB so existing folders are watched on startup.
let folder_map: Arc<Mutex<HashMap<PathBuf, i64>>> = Arc::new(Mutex::new(HashMap::new()));
{
let conn = pool.get().expect("Watcher: failed to get DB connection for init");
let conn = pool
.get()
.expect("Watcher: failed to get DB connection for init");
let folders = db::get_folders(&conn).unwrap_or_default();
let mut map = folder_map.lock().unwrap();
for f in folders {
@@ -1505,7 +1511,10 @@ pub fn start_watcher(app: AppHandle, pool: DbPool, thumb_dir: PathBuf) -> Watche
// Absorb incoming event — coalesces rapid writes into one deadline.
if let Some(Ok(event)) = received {
use notify::{EventKind, event::{ModifyKind, RenameMode}};
use notify::{
event::{ModifyKind, RenameMode},
EventKind,
};
if !matches!(event.kind, EventKind::Access(_)) {
// Intercept atomic rename events (old + new path in one event).
// Handle these separately to preserve embeddings and thumbnails.
@@ -1536,7 +1545,14 @@ pub fn start_watcher(app: AppHandle, pool: DbPool, thumb_dir: PathBuf) -> Watche
// Process renames immediately — they are atomic filesystem operations
// and do not benefit from debouncing.
for (old_path, new_path) in pending_renames.drain(..) {
process_watcher_rename(&app, &pool, &folder_map_thread, &thumb_dir, &old_path, &new_path);
process_watcher_rename(
&app,
&pool,
&folder_map_thread,
&thumb_dir,
&old_path,
&new_path,
);
}
// Process all paths whose debounce window has expired.
@@ -1594,7 +1610,13 @@ fn process_watcher_path(
match commit_batch(pool, &[record]) {
Ok(committed) if !committed.is_empty() => {
// Always emit the images — they are committed to the DB.
emit_images(app, &IndexedImagesBatch { folder_id, images: committed });
emit_images(
app,
&IndexedImagesBatch {
folder_id,
images: committed,
},
);
emit_folder_job_progress(app, pool, &[folder_id], false);
// Update the sidebar count only if we successfully write the new
// count; skip the frontend notification on pool/DB failure to
@@ -1643,7 +1665,9 @@ fn process_watcher_rename(
let folder_id = {
let map = folder_map.lock().unwrap();
map.iter()
.find(|(fp, _)| old_path.starts_with(fp.as_path()) || new_path.starts_with(fp.as_path()))
.find(|(fp, _)| {
old_path.starts_with(fp.as_path()) || new_path.starts_with(fp.as_path())
})
.map(|(_, &id)| id)
};
let Some(folder_id) = folder_id else { return };
@@ -1684,19 +1708,28 @@ fn process_watcher_rename(
// the thumbnail worker regenerates it.
let effective_thumb: Option<&str> = if thumb_ok { Some(&new_thumb_str) } else { None };
let new_filename = new_path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("");
let new_filename = new_path.file_name().and_then(|f| f.to_str()).unwrap_or("");
if let Err(e) = db::update_image_path(&conn, image_id, &new_path_str, new_filename, effective_thumb) {
if let Err(e) = db::update_image_path(
&conn,
image_id,
&new_path_str,
new_filename,
effective_thumb,
) {
eprintln!("Watcher rename: DB update error: {}", e);
return;
}
// Emit the updated record so the frontend refreshes without a full reload.
match db::get_image_by_id(&conn, image_id) {
Ok(record) => emit_images(app, &IndexedImagesBatch { folder_id, images: vec![record] }),
Ok(record) => emit_images(
app,
&IndexedImagesBatch {
folder_id,
images: vec![record],
},
),
Err(e) => eprintln!("Watcher rename: post-update fetch error: {}", e),
}
}
+1 -5
View File
@@ -200,11 +200,7 @@ pub fn tagger_batch_size(app_data_dir: &Path) -> usize {
let Ok(value) = std::fs::read_to_string(path) else {
return 8;
};
value
.trim()
.parse::<usize>()
.unwrap_or(8)
.clamp(1, 100)
value.trim().parse::<usize>().unwrap_or(8).clamp(1, 100)
}
pub fn set_tagger_batch_size(app_data_dir: &Path, batch_size: usize) -> Result<usize> {
+2 -2
View File
@@ -289,8 +289,8 @@ mod tests {
assert_eq!((decoded.width(), decoded.height()), (400, 300));
// Cover mode: shortest side (1200) must stay >= 224 -> numerator 2
let covered = decode_jpeg_scaled(&src_path, 224, true)
.expect("fast path should handle plain JPEG");
let covered =
decode_jpeg_scaled(&src_path, 224, true).expect("fast path should handle plain JPEG");
assert_eq!((covered.width(), covered.height()), (400, 300));
let cache = dir.join("cache");