From 75f7de2e43af4bf4e507ed0c067a817cf780c271 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Fri, 6 Mar 2026 11:49:53 +0100 Subject: [PATCH] =?UTF-8?q?feat(watcher):=20Ajout=20watcher=20de=20fichier?= =?UTF-8?q?s=20temps=20r=C3=A9el?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migration 0006: colonne watcher_enabled - Crate notify pour surveillance FS temps réel (FSEvents/inotify) - Watcher redémarré toutes les 30s si config change - Détection instantanée création/modification/suppression - Création job immédiate quand fichier détecté - API: support watcher_enabled dans UpdateMonitoringRequest - Backoffice: toggle Watcher avec indicateur ⚡ - Fonctionne en parallèle du scheduler auto-scan Usage: Activer Watcher + Auto-scan pour réactivité max --- Cargo.lock | 127 +++++++++++++-- apps/api/src/libraries.rs | 12 +- .../app/components/MonitoringForm.tsx | 37 +++-- apps/backoffice/app/globals.css | 24 +++ apps/backoffice/lib/api.ts | 12 +- apps/indexer/Cargo.toml | 1 + apps/indexer/src/main.rs | 146 ++++++++++++++++++ infra/migrations/0006_add_watcher.sql | 6 + 8 files changed, 340 insertions(+), 25 deletions(-) create mode 100644 infra/migrations/0006_add_watcher.sql diff --git a/Cargo.lock b/Cargo.lock index f6eb33f..b36c9df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,12 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -420,6 +426,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -589,6 +604,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -631,6 +657,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.32" @@ -1117,6 +1152,7 @@ dependencies = [ "anyhow", "axum", "chrono", + "notify", "parsers", "reqwest", "serde", @@ -1143,6 +1179,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.4" @@ -1185,6 +1241,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1218,7 +1294,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags", + "bitflags 2.11.0", "libc", "plain", "redox_syscall 0.7.3", @@ -1357,6 +1433,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.1.1" @@ -1399,6 +1487,25 @@ dependencies = [ "nom", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.11.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1582,7 +1689,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags", + "bitflags 2.11.0", "crc32fast", "fdeflate", "flate2", @@ -1835,7 +1942,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -1844,7 +1951,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -2326,7 +2433,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.11.0", "byteorder", "bytes", "chrono", @@ -2370,7 +2477,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.11.0", "byteorder", "chrono", "crc", @@ -2592,7 +2699,7 @@ checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", - "mio", + "mio 1.1.1", "pin-project-lite", "signal-hook-registry", "socket2", @@ -2668,7 +2775,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -3035,7 +3142,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -3443,7 +3550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde", diff --git a/apps/api/src/libraries.rs b/apps/api/src/libraries.rs index 36dae6c..f38f790 100644 --- a/apps/api/src/libraries.rs +++ b/apps/api/src/libraries.rs @@ -19,6 +19,7 @@ pub struct LibraryResponse { pub monitor_enabled: bool, pub scan_mode: String, pub next_scan_at: Option>, + pub watcher_enabled: bool, } #[derive(Deserialize, ToSchema)] @@ -43,7 +44,7 @@ pub struct CreateLibraryRequest { )] pub async fn list_libraries(State(state): State) -> Result>, ApiError> { let rows = sqlx::query( - "SELECT l.id, l.name, l.root_path, l.enabled, l.monitor_enabled, l.scan_mode, l.next_scan_at, + "SELECT l.id, l.name, l.root_path, l.enabled, l.monitor_enabled, l.scan_mode, l.next_scan_at, l.watcher_enabled, (SELECT COUNT(*) FROM books b WHERE b.library_id = l.id) as book_count FROM libraries l ORDER BY l.created_at DESC" ) @@ -61,6 +62,7 @@ pub async fn list_libraries(State(state): State) -> Result, } /// Update monitoring settings for a library @@ -268,13 +272,16 @@ pub async fn update_monitoring( None }; + let watcher_enabled = input.watcher_enabled.unwrap_or(false); + let result = sqlx::query( - "UPDATE libraries SET monitor_enabled = $2, scan_mode = $3, next_scan_at = $4 WHERE id = $1 RETURNING id, name, root_path, enabled, monitor_enabled, scan_mode, next_scan_at" + "UPDATE libraries SET monitor_enabled = $2, scan_mode = $3, next_scan_at = $4, watcher_enabled = $5 WHERE id = $1 RETURNING id, name, root_path, enabled, monitor_enabled, scan_mode, next_scan_at, watcher_enabled" ) .bind(library_id) .bind(input.monitor_enabled) .bind(input.scan_mode) .bind(next_scan_at) + .bind(watcher_enabled) .fetch_optional(&state.pool) .await?; @@ -296,5 +303,6 @@ pub async fn update_monitoring( monitor_enabled: row.get("monitor_enabled"), scan_mode: row.get("scan_mode"), next_scan_at: row.get("next_scan_at"), + watcher_enabled: row.get("watcher_enabled"), })) } diff --git a/apps/backoffice/app/components/MonitoringForm.tsx b/apps/backoffice/app/components/MonitoringForm.tsx index 655f6de..908311d 100644 --- a/apps/backoffice/app/components/MonitoringForm.tsx +++ b/apps/backoffice/app/components/MonitoringForm.tsx @@ -6,9 +6,10 @@ interface MonitoringFormProps { libraryId: string; monitorEnabled: boolean; scanMode: string; + watcherEnabled: boolean; } -export function MonitoringForm({ libraryId, monitorEnabled, scanMode }: MonitoringFormProps) { +export function MonitoringForm({ libraryId, monitorEnabled, scanMode, watcherEnabled }: MonitoringFormProps) { const [isPending, startTransition] = useTransition(); const handleSubmit = (formData: FormData) => { @@ -20,6 +21,7 @@ export function MonitoringForm({ libraryId, monitorEnabled, scanMode }: Monitori body: JSON.stringify({ monitor_enabled: formData.get("monitor_enabled") === "true", scan_mode: formData.get("scan_mode"), + watcher_enabled: formData.get("watcher_enabled") === "true", }), }); if (response.ok) { @@ -34,16 +36,29 @@ export function MonitoringForm({ libraryId, monitorEnabled, scanMode }: Monitori return (
- +
+ + +