feat(watcher): adaptive filesystem watchdog with zero CPU when idle
Monitors all registered folders using OS-native events (ReadDirectoryChangesW on Windows) so new, modified, and deleted files are reflected in the gallery automatically without manual reindexing. Key design points: - Adaptive blocking: recv() when no events pending (zero CPU), switches to recv_timeout(earliest_deadline) only when debounce timers are running — wakes exactly when the soonest event is ready, no busy-polling - 500 ms per-path debounce coalesces rapid OS event bursts into one action - Change detection preserved: build_record skips upsert if file_size + mtime unchanged, preventing spurious thumbnail/embedding re-queues and avoiding clobbering of existing metadata like thumbnail_path - Access events (reads) filtered out; only Create/Modify/Remove trigger work - Deletion path: emits watcher-deleted event with image IDs; frontend removes those images from state and clears selectedImage if it was deleted WatcherHandle stored in app state; add_folder / remove_folder / update_folder_path commands keep the watched path set in sync with the DB.
This commit is contained in:
@@ -5,7 +5,7 @@ use crate::captioner::{
|
||||
use crate::db::{self, DbPool, ExploreTagEntry, Folder, FolderJobProgress, ImageRecord, ImageTag};
|
||||
use crate::embedder;
|
||||
use crate::hnsw_index;
|
||||
use crate::indexer;
|
||||
use crate::indexer::{self, WatcherHandle};
|
||||
use crate::tagger::{self, TaggerAcceleration, TaggerModelStatus, TaggerRuntimeProbe};
|
||||
use crate::vector;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -202,6 +202,7 @@ pub struct GetImagesByIdsParams {
|
||||
pub async fn add_folder(
|
||||
app: AppHandle,
|
||||
db: State<'_, DbState>,
|
||||
watcher: State<'_, WatcherHandle>,
|
||||
path: String,
|
||||
) -> Result<Folder, String> {
|
||||
let folder_path = PathBuf::from(&path);
|
||||
@@ -230,6 +231,7 @@ pub async fn add_folder(
|
||||
.find(|f| f.id == folder_id)
|
||||
.ok_or("Folder not found after insert")?;
|
||||
|
||||
watcher.add_folder(folder_path.clone(), folder_id);
|
||||
indexer::index_folder(app, db.inner().clone(), folder_id, folder_path);
|
||||
|
||||
Ok(folder)
|
||||
@@ -250,9 +252,23 @@ pub async fn get_background_job_progress(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_folder(db: State<'_, DbState>, folder_id: i64) -> Result<(), String> {
|
||||
pub async fn remove_folder(
|
||||
db: State<'_, DbState>,
|
||||
watcher: State<'_, WatcherHandle>,
|
||||
folder_id: i64,
|
||||
) -> Result<(), String> {
|
||||
let conn = db.get().map_err(|e| e.to_string())?;
|
||||
db::delete_folder(&conn, folder_id).map_err(|e| e.to_string())
|
||||
// Capture the path before deletion so we can unregister the watcher.
|
||||
let folder_path = db::get_folders(&conn)
|
||||
.map_err(|e| e.to_string())?
|
||||
.into_iter()
|
||||
.find(|f| f.id == folder_id)
|
||||
.map(|f| PathBuf::from(f.path));
|
||||
db::delete_folder(&conn, folder_id).map_err(|e| e.to_string())?;
|
||||
if let Some(path) = folder_path {
|
||||
watcher.remove_folder(&path);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -352,6 +368,7 @@ pub async fn rename_folder(
|
||||
pub async fn update_folder_path(
|
||||
app: AppHandle,
|
||||
db: State<'_, DbState>,
|
||||
watcher: State<'_, WatcherHandle>,
|
||||
folder_id: i64,
|
||||
new_path: String,
|
||||
) -> Result<(), String> {
|
||||
@@ -363,7 +380,7 @@ pub async fn update_folder_path(
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| new_path.clone());
|
||||
{
|
||||
let old_path = {
|
||||
let conn = db.get().map_err(|e| e.to_string())?;
|
||||
// Fetch the old path before updating so image paths can be rewritten.
|
||||
let old_path = db::get_folders(&conn)
|
||||
@@ -374,7 +391,9 @@ pub async fn update_folder_path(
|
||||
.ok_or("Folder not found")?;
|
||||
db::update_folder_path(&conn, folder_id, &old_path, &new_path, &new_name)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
old_path
|
||||
};
|
||||
watcher.update_folder(&PathBuf::from(old_path), new_path_buf.clone(), folder_id);
|
||||
indexer::index_folder(app, db.inner().clone(), folder_id, new_path_buf);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user