mirror of
https://github.com/PR0M3TH3AN/Marlin.git
synced 2025-09-08 07:08:44 +00:00
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,7 +1,6 @@
|
|||||||
# === Rust build artifacts ===
|
# === Rust build artifacts ===
|
||||||
|
|
||||||
/target/
|
/target/
|
||||||
/Cargo.lock
|
|
||||||
|
|
||||||
# === IDE & Editor settings ===
|
# === IDE & Editor settings ===
|
||||||
|
|
||||||
|
50
Cargo.lock
generated
50
Cargo.lock
generated
@@ -79,12 +79,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-wincon"
|
name = "anstyle-wincon"
|
||||||
version = "3.0.7"
|
version = "3.0.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"once_cell",
|
"once_cell_polyfill",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -156,9 +156,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.23"
|
version = "1.2.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
|
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
@@ -470,12 +470,6 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.12.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -530,16 +524,6 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indexmap"
|
|
||||||
version = "1.9.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"hashbrown 0.12.3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -635,8 +619,8 @@ dependencies = [
|
|||||||
"glob",
|
"glob",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"notify",
|
"notify",
|
||||||
"priority-queue",
|
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
"same-file",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
@@ -820,6 +804,12 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -874,16 +864,6 @@ dependencies = [
|
|||||||
"termtree",
|
"termtree",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "priority-queue"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a0bda9164fe05bc9225752d54aae413343c36f684380005398a6a8fde95fe785"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"indexmap 1.9.3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.95"
|
version = "1.0.95"
|
||||||
@@ -1012,9 +992,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.20"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
@@ -1069,7 +1049,7 @@ version = "0.9.34+deprecated"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.9.0",
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
|
@@ -22,3 +22,6 @@
|
|||||||
| `event add` | — |
|
| `event add` | — |
|
||||||
| `event timeline` | — |
|
| `event timeline` | — |
|
||||||
| `backup run` | --dir, --prune, --verify, --file |
|
| `backup run` | --dir, --prune, --verify, --file |
|
||||||
|
| `watch start` | --debounce-ms |
|
||||||
|
| `watch status` | — |
|
||||||
|
| `watch stop` | — |
|
||||||
|
@@ -8,14 +8,14 @@
|
|||||||
|
|
||||||
We’ve landed a basic SQLite-backed `files` table and a contentless FTS5 index. Before we build out higher-level features, we need to lock down our **v1.1** metadata schema for:
|
We’ve landed a basic SQLite-backed `files` table and a contentless FTS5 index. Before we build out higher-level features, we need to lock down our **v1.1** metadata schema for:
|
||||||
|
|
||||||
- **Hierarchical tags** (`tags` + `file_tags`) – optional `canonical_id` for aliases
|
- **Hierarchical tags** (`tags` + `file_tags`) – alias resolution handled at query time
|
||||||
- **Custom attributes** (`attributes`)
|
- **Custom attributes** (`attributes`)
|
||||||
- **File-to-file relationships** (`links`)
|
- **File-to-file relationships** (`links`)
|
||||||
- **Named collections** (`collections` + `collection_files`)
|
- **Named collections** (`collections` + `collection_files`)
|
||||||
- **Views** (`views`)
|
- **Views** (`views`)
|
||||||
|
|
||||||
Locking this schema now lets downstream CLI & GUI work against a stable model and ensures our migrations stay easy to reason about.
|
Locking this schema now lets downstream CLI & GUI work against a stable model and ensures our migrations stay easy to reason about.
|
||||||
Tags optionally reference a canonical tag via the `canonical_id` column.
|
Alias relationships are resolved outside the table itself; there is no `canonical_id` column.
|
||||||
|
|
||||||
## 2. Decision
|
## 2. Decision
|
||||||
|
|
||||||
@@ -58,7 +58,6 @@ entity tags {
|
|||||||
--
|
--
|
||||||
name : TEXT
|
name : TEXT
|
||||||
parent_id : INTEGER <<FK>>
|
parent_id : INTEGER <<FK>>
|
||||||
canonical_id : INTEGER <<FK>>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entity file_tags {
|
entity file_tags {
|
||||||
@@ -151,6 +150,7 @@ Or in plain-ASCII:
|
|||||||
| **0003\_create\_links\_collections\_views.sql** | Add `links`, `collections`, `collection_files`, `views` |
|
| **0003\_create\_links\_collections\_views.sql** | Add `links`, `collections`, `collection_files`, `views` |
|
||||||
| **0004\_fix\_hierarchical\_tags\_fts.sql** | Recursive CTE for full tag-path indexing in FTS triggers |
|
| **0004\_fix\_hierarchical\_tags\_fts.sql** | Recursive CTE for full tag-path indexing in FTS triggers |
|
||||||
| **0005_add_dirty_table.sql** | Track modified files needing reindexing |
|
| **0005_add_dirty_table.sql** | Track modified files needing reindexing |
|
||||||
|
| **0006_drop_tags_canonical_id.sql** | Remove legacy `canonical_id` column from `tags` |
|
||||||
|
|
||||||
### Performance-Critical Indexes
|
### Performance-Critical Indexes
|
||||||
|
|
||||||
|
@@ -46,7 +46,7 @@
|
|||||||
| Tarpaulin coverage gate | S0 | — | – |
|
| Tarpaulin coverage gate | S0 | — | – |
|
||||||
| Watch mode (FS events) | Epic 1 | `marlin watch .` | DP‑002 |
|
| Watch mode (FS events) | Epic 1 | `marlin watch .` | DP‑002 |
|
||||||
| Backup auto‑prune | Epic 1 | `backup --prune N` | – |
|
| Backup auto‑prune | Epic 1 | `backup --prune N` | – |
|
||||||
| Rename/move tracking | Epic 2 | automatic path update | Spec‑RMH |
|
| ~~Rename/move tracking~~ | Epic 2 | automatic path update | Spec‑RMH |
|
||||||
| Dirty‑scan | Epic 2 | `scan --dirty` | DP‑002 |
|
| Dirty‑scan | Epic 2 | `scan --dirty` | DP‑002 |
|
||||||
| Grep snippets | Phase 3 | `search -C3 …` | DP‑004 |
|
| Grep snippets | Phase 3 | `search -C3 …` | DP‑004 |
|
||||||
| Hash / dedupe | Phase 4 | `scan --rehash` | DP‑005 |
|
| Hash / dedupe | Phase 4 | `scan --rehash` | DP‑005 |
|
||||||
@@ -75,7 +75,7 @@ Before a milestone is declared “shipped”:
|
|||||||
| - | ------------------------------ | ------ | ------------- |
|
| - | ------------------------------ | ------ | ------------- |
|
||||||
| ~~1~~ | ~~Crate split + CI baseline~~ | @alice | ~~26 May 25~~ |
|
| ~~1~~ | ~~Crate split + CI baseline~~ | @alice | ~~26 May 25~~ |
|
||||||
| ~~2~~ | ~~Tarpaulin + Hyperfine jobs~~ | @bob | ~~26 May 25~~ |
|
| ~~2~~ | ~~Tarpaulin + Hyperfine jobs~~ | @bob | ~~26 May 25~~ |
|
||||||
| 3 | **DP‑001 Schema v1.1** draft | @carol | **30 May 25** |
|
| ~~3~~ | ~~DP‑001 Schema v1.1 draft~~ | @carol | ~~30 May 25~~ |
|
||||||
| ~~4~~ | ~~backup prune CLI + nightly job~~ | @dave | ~~05 Jun 25~~ |
|
| ~~4~~ | ~~backup prune CLI + nightly job~~ | @dave | ~~05 Jun 25~~ |
|
||||||
|
|
||||||
> *This roadmap now contains both product-level “what” and engineering-level “how/when/prove it”. It should allow a new contributor to jump in, pick the matching DP, and know exactly the bar they must clear for their code to merge.*
|
> *This roadmap now contains both product-level “what” and engineering-level “how/when/prove it”. It should allow a new contributor to jump in, pick the matching DP, and know exactly the bar they must clear for their code to merge.*
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
| Feature Area | Capabilities |
|
| Feature Area | Capabilities |
|
||||||
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Tagging System** | • Unlimited, hierarchical or flat tags.<br>• Alias/synonym support with precedence rules (admin‑defined canonical name).<br>• **Bulk tag editing** via multi‑select context menu.<br>• Folder‑to‑Tag import with optional *watch & sync* mode so new sub‑folders inherit tags automatically. |
|
| **Tagging System** | • Unlimited, hierarchical or flat tags.<br>• Alias/synonym support via admin‑defined mappings (canonical names resolved at query time).<br>• **Bulk tag editing** via multi‑select context menu.<br>• Folder‑to‑Tag import with optional *watch & sync* mode so new sub‑folders inherit tags automatically. |
|
||||||
| **Custom Metadata Attributes** | • User‑defined fields (text, number, date, enum, boolean).<br>• Per‑template **Custom Metadata Schemas** (e.g. *Photo* → *Date, Location*). |
|
| **Custom Metadata Attributes** | • User‑defined fields (text, number, date, enum, boolean).<br>• Per‑template **Custom Metadata Schemas** (e.g. *Photo* → *Date, Location*). |
|
||||||
| **File Relationships** | • Typed, directional or bidirectional links (*related to*, *duplicate of*, *cites*…).<br>• Plugin API can register new relationship sets. |
|
| **File Relationships** | • Typed, directional or bidirectional links (*related to*, *duplicate of*, *cites*…).<br>• Plugin API can register new relationship sets. |
|
||||||
| **Version Control for Metadata** | • Every change logged; unlimited roll‑back.<br>• Side‑by‑side diff viewer and *blame* panel showing *who/when/what*.<br>• Offline edits stored locally and merged (Git‑style optimistic merge with conflict prompts). |
|
| **Version Control for Metadata** | • Every change logged; unlimited roll‑back.<br>• Side‑by‑side diff viewer and *blame* panel showing *who/when/what*.<br>• Offline edits stored locally and merged (Git‑style optimistic merge with conflict prompts). |
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
files(id PK, path, inode, size, mtime, ctime, hash)
|
files(id PK, path, inode, size, mtime, ctime, hash)
|
||||||
tags(id PK, name, parent_id, canonical_id)
|
tags(id PK, name, parent_id)
|
||||||
file_tags(file_id FK, tag_id FK)
|
file_tags(file_id FK, tag_id FK)
|
||||||
attributes(id PK, file_id FK, key, value, value_type)
|
attributes(id PK, file_id FK, key, value, value_type)
|
||||||
relationships(id PK, src_file_id FK, dst_file_id FK, rel_type, direction)
|
relationships(id PK, src_file_id FK, dst_file_id FK, rel_type, direction)
|
||||||
|
@@ -11,13 +11,13 @@ crossbeam-channel = "0.5"
|
|||||||
directories = "5"
|
directories = "5"
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
notify = "6.0"
|
notify = "6.0"
|
||||||
priority-queue = "1.3"
|
|
||||||
rusqlite = { version = "0.31", features = ["bundled", "backup"] }
|
rusqlite = { version = "0.31", features = ["bundled", "backup"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||||
walkdir = "2.5"
|
walkdir = "2.5"
|
||||||
shlex = "1.3"
|
shlex = "1.3"
|
||||||
|
same-file = "1"
|
||||||
shellexpand = "3.1"
|
shellexpand = "3.1"
|
||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
|
|
||||||
|
20
libmarlin/src/db/migrations/0007_fix_rename_trigger.sql
Normal file
20
libmarlin/src/db/migrations/0007_fix_rename_trigger.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- src/db/migrations/0007_fix_rename_trigger.sql
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
|
||||||
|
-- Recreate files_fts_au_file trigger using INSERT OR REPLACE
|
||||||
|
DROP TRIGGER IF EXISTS files_fts_au_file;
|
||||||
|
CREATE TRIGGER files_fts_au_file
|
||||||
|
AFTER UPDATE OF path ON files
|
||||||
|
BEGIN
|
||||||
|
INSERT OR REPLACE INTO files_fts(rowid, path, tags_text, attrs_text)
|
||||||
|
SELECT NEW.id,
|
||||||
|
NEW.path,
|
||||||
|
(SELECT IFNULL(GROUP_CONCAT(t.name, ' '), '')
|
||||||
|
FROM file_tags ft
|
||||||
|
JOIN tags t ON ft.tag_id = t.id
|
||||||
|
WHERE ft.file_id = NEW.id),
|
||||||
|
(SELECT IFNULL(GROUP_CONCAT(a.key || '=' || a.value, ' '), '')
|
||||||
|
FROM attributes a
|
||||||
|
WHERE a.file_id = NEW.id);
|
||||||
|
END;
|
@@ -50,6 +50,10 @@ const MIGRATIONS: &[(&str, &str)] = &[
|
|||||||
"0006_drop_tags_canonical_id.sql",
|
"0006_drop_tags_canonical_id.sql",
|
||||||
include_str!("migrations/0006_drop_tags_canonical_id.sql"),
|
include_str!("migrations/0006_drop_tags_canonical_id.sql"),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"0007_fix_rename_trigger.sql",
|
||||||
|
include_str!("migrations/0007_fix_rename_trigger.sql"),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ─── schema helpers ─────────────────────────────────────────────── */
|
/* ─── schema helpers ─────────────────────────────────────────────── */
|
||||||
@@ -387,6 +391,39 @@ pub fn take_dirty(conn: &Connection) -> Result<Vec<i64>> {
|
|||||||
Ok(ids)
|
Ok(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── rename helpers ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
pub fn update_file_path(conn: &Connection, old_path: &str, new_path: &str) -> Result<()> {
|
||||||
|
let file_id: i64 = conn.query_row("SELECT id FROM files WHERE path = ?1", [old_path], |r| {
|
||||||
|
r.get(0)
|
||||||
|
})?;
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE files SET path = ?1 WHERE id = ?2",
|
||||||
|
params![new_path, file_id],
|
||||||
|
)?;
|
||||||
|
mark_dirty(conn, file_id)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rename_directory(conn: &mut Connection, old_dir: &str, new_dir: &str) -> Result<()> {
|
||||||
|
let like_pattern = format!("{}/%", old_dir.trim_end_matches('/'));
|
||||||
|
let ids = {
|
||||||
|
let mut stmt = conn.prepare("SELECT id FROM files WHERE path LIKE ?1")?;
|
||||||
|
let rows = stmt.query_map([&like_pattern], |r| r.get::<_, i64>(0))?;
|
||||||
|
rows.collect::<StdResult<Vec<_>, _>>()?
|
||||||
|
};
|
||||||
|
let tx = conn.transaction()?;
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE files SET path = REPLACE(path, ?1, ?2) WHERE path LIKE ?3",
|
||||||
|
params![old_dir, new_dir, like_pattern],
|
||||||
|
)?;
|
||||||
|
for fid in ids {
|
||||||
|
mark_dirty(&tx, fid)?;
|
||||||
|
}
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── backup / restore helpers ────────────────────────────────────── */
|
/* ─── backup / restore helpers ────────────────────────────────────── */
|
||||||
|
|
||||||
pub fn backup<P: AsRef<Path>>(db_path: P) -> Result<PathBuf> {
|
pub fn backup<P: AsRef<Path>>(db_path: P) -> Result<PathBuf> {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -7,14 +7,46 @@ mod tests {
|
|||||||
// These are still from the watcher module
|
// These are still from the watcher module
|
||||||
use crate::db::open as open_marlin_db;
|
use crate::db::open as open_marlin_db;
|
||||||
use crate::watcher::{FileWatcher, WatcherConfig, WatcherState}; // Use your project's DB open function
|
use crate::watcher::{FileWatcher, WatcherConfig, WatcherState}; // Use your project's DB open function
|
||||||
|
use crate::Marlin;
|
||||||
|
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
// No longer need: use std::path::PathBuf;
|
// No longer need: use std::path::PathBuf;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
/// Polls the DB until `query` returns `expected` or the timeout elapses.
|
||||||
|
fn wait_for_row_count(
|
||||||
|
marlin: &Marlin,
|
||||||
|
path: &std::path::Path,
|
||||||
|
expected: i64,
|
||||||
|
timeout: Duration,
|
||||||
|
) {
|
||||||
|
let start = Instant::now();
|
||||||
|
loop {
|
||||||
|
let count: i64 = marlin
|
||||||
|
.conn()
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM files WHERE path = ?1",
|
||||||
|
[path.to_string_lossy()],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
if count == expected {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if start.elapsed() > timeout {
|
||||||
|
panic!(
|
||||||
|
"Timed out waiting for {} rows for {}",
|
||||||
|
expected,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_watcher_lifecycle() {
|
fn test_watcher_lifecycle() {
|
||||||
// Create a temp directory for testing
|
// Create a temp directory for testing
|
||||||
@@ -60,7 +92,7 @@ mod tests {
|
|||||||
thread::sleep(Duration::from_millis(200));
|
thread::sleep(Duration::from_millis(200));
|
||||||
fs::remove_file(&new_file_path).expect("Failed to remove file");
|
fs::remove_file(&new_file_path).expect("Failed to remove file");
|
||||||
|
|
||||||
thread::sleep(Duration::from_millis(500));
|
thread::sleep(Duration::from_millis(1500));
|
||||||
watcher.stop().expect("Failed to stop watcher");
|
watcher.stop().expect("Failed to stop watcher");
|
||||||
|
|
||||||
assert_eq!(watcher.status().unwrap().state, WatcherState::Stopped);
|
assert_eq!(watcher.status().unwrap().state, WatcherState::Stopped);
|
||||||
@@ -144,4 +176,97 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rename_file_updates_db() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let dir = tmp.path();
|
||||||
|
let file = dir.join("a.txt");
|
||||||
|
fs::write(&file, b"hi").unwrap();
|
||||||
|
let db_path = dir.join("test.db");
|
||||||
|
let mut marlin = Marlin::open_at(&db_path).unwrap();
|
||||||
|
marlin.scan(&[dir]).unwrap();
|
||||||
|
|
||||||
|
let mut watcher = marlin
|
||||||
|
.watch(
|
||||||
|
dir,
|
||||||
|
Some(WatcherConfig {
|
||||||
|
debounce_ms: 50,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
let new_file = dir.join("b.txt");
|
||||||
|
fs::rename(&file, &new_file).unwrap();
|
||||||
|
wait_for_row_count(&marlin, &new_file, 1, Duration::from_secs(10));
|
||||||
|
watcher.stop().unwrap();
|
||||||
|
assert!(
|
||||||
|
watcher.status().unwrap().events_processed > 0,
|
||||||
|
"rename event should be processed"
|
||||||
|
);
|
||||||
|
|
||||||
|
let count: i64 = marlin
|
||||||
|
.conn()
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM files WHERE path = ?1",
|
||||||
|
[new_file.to_string_lossy()],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rename_directory_updates_children() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let dir = tmp.path();
|
||||||
|
let sub = dir.join("old");
|
||||||
|
fs::create_dir(&sub).unwrap();
|
||||||
|
let f1 = sub.join("one.txt");
|
||||||
|
fs::write(&f1, b"1").unwrap();
|
||||||
|
let f2 = sub.join("two.txt");
|
||||||
|
fs::write(&f2, b"2").unwrap();
|
||||||
|
|
||||||
|
let db_path = dir.join("test2.db");
|
||||||
|
let mut marlin = Marlin::open_at(&db_path).unwrap();
|
||||||
|
marlin.scan(&[dir]).unwrap();
|
||||||
|
|
||||||
|
let mut watcher = marlin
|
||||||
|
.watch(
|
||||||
|
dir,
|
||||||
|
Some(WatcherConfig {
|
||||||
|
debounce_ms: 50,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
let new = dir.join("newdir");
|
||||||
|
fs::rename(&sub, &new).unwrap();
|
||||||
|
for fname in ["one.txt", "two.txt"] {
|
||||||
|
let p = new.join(fname);
|
||||||
|
wait_for_row_count(&marlin, &p, 1, Duration::from_secs(10));
|
||||||
|
}
|
||||||
|
watcher.stop().unwrap();
|
||||||
|
assert!(
|
||||||
|
watcher.status().unwrap().events_processed > 0,
|
||||||
|
"rename event should be processed"
|
||||||
|
);
|
||||||
|
|
||||||
|
for fname in ["one.txt", "two.txt"] {
|
||||||
|
let p = new.join(fname);
|
||||||
|
let cnt: i64 = marlin
|
||||||
|
.conn()
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM files WHERE path = ?1",
|
||||||
|
[p.to_string_lossy()],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cnt, 1, "{} missing", p.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user