mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
feat(core,fuse,gui,cli): search scope and location column
Request::Search takes an optional folder scope (serde default, so older front-ends keep searching everywhere). The GUI offers a This folder toggle beside the search box and shows a Location column for hits; the CLI gains pdfs search --in. Pins report whether a pinned file is cached, so pin-derived search hits badge correctly.
This commit is contained in:
1 parent
0fbc6421ba
commit
29e5769dec
12 files changed
+204
-15
No files matched your search
@@ -298,6 +298,10 @@ pub struct Pin {
|
||||
/// which is why it is defaulted.
|
||||
#[serde(default)]
|
||||
pub is_dir: Option<bool>,
|
||||
/// A pinned file whose content is on disk right now. The daemon fills it
|
||||
/// in when it answers `ListPins`; folders and older daemons say `false`.
|
||||
#[serde(default)]
|
||||
pub cached: bool,
|
||||
}
|
||||
|
||||
/// The legacy JSON pin registry, kept only to import a pre-P5 `pins.json` into
|
||||
@@ -1854,6 +1858,7 @@ impl ContentCache {
|
||||
path: row.path,
|
||||
recursive: row.recursive,
|
||||
is_dir: row.is_dir,
|
||||
cached: false,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -245,8 +245,15 @@ pub enum Request {
|
||||
uid: Option<String>,
|
||||
},
|
||||
/// Full-text search node names against the daemon's local metadata index.
|
||||
/// `limit` caps the number of hits returned. Replies with [`Response::SearchResults`].
|
||||
Search { query: String, limit: usize },
|
||||
/// `limit` caps the number of hits returned. `scope`, a mountpoint-relative
|
||||
/// folder, restricts hits to that subtree; older front-ends omit it and
|
||||
/// search everything. Replies with [`Response::SearchResults`].
|
||||
Search {
|
||||
query: String,
|
||||
limit: usize,
|
||||
#[serde(default)]
|
||||
scope: Option<String>,
|
||||
},
|
||||
/// Search the daemon's index of *local* (non-Drive) files on this machine.
|
||||
/// Independent of [`Request::Search`] so a front-end can fire both at once and
|
||||
/// render whichever lands first. Replies with [`Response::LocalResults`].
|
||||
@@ -2342,7 +2349,7 @@ mod tests {
|
||||
serde_json::from_str(r#"{"Search":{"query":"old","limit":20}}"#).unwrap();
|
||||
assert!(matches!(
|
||||
drive,
|
||||
Request::Search { query, limit } if query == "old" && limit == 20
|
||||
Request::Search { query, limit, scope: None } if query == "old" && limit == 20
|
||||
));
|
||||
|
||||
let local: Request =
|
||||
|
||||
@@ -355,6 +355,22 @@ impl Db {
|
||||
/// than [`TRIGRAM_MIN`] fall back to a `LIKE` scan since trigram indexes
|
||||
/// nothing below 3 chars.
|
||||
pub fn search(&self, query: &str, limit: usize) -> Result<Vec<SearchHit>> {
|
||||
self.search_in(query, limit, None)
|
||||
}
|
||||
|
||||
/// [`search`](Self::search) restricted to the subtree under the
|
||||
/// mountpoint-relative folder `scope`. `None` or an empty scope searches
|
||||
/// everything.
|
||||
pub fn search_in(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
scope: Option<&str>,
|
||||
) -> Result<Vec<SearchHit>> {
|
||||
let scope = scope.map(|s| s.trim_matches('/')).filter(|s| !s.is_empty());
|
||||
// Rows whose path is not persisted yet pass the SQL filter and are
|
||||
// checked once their path is walked below.
|
||||
let scope_pat = scope.map(|s| format!("{}/%", like_escape(s)));
|
||||
let query = query.trim();
|
||||
if query.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
@@ -365,9 +381,10 @@ impl Db {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT node_json, uid, path FROM nodes
|
||||
WHERE name LIKE ?1 ESCAPE '\\' AND trashed = 0 AND node_json IS NOT NULL
|
||||
AND (?3 IS NULL OR path IS NULL OR path LIKE ?3 ESCAPE '\\')
|
||||
ORDER BY name LIMIT ?2",
|
||||
)?;
|
||||
collect_hits(stmt.query_map(params![pat, limit as i64], hit_row)?)?
|
||||
collect_hits(stmt.query_map(params![pat, limit as i64, scope_pat], hit_row)?)?
|
||||
} else {
|
||||
// Escape double quotes and quote each term, then combine with AND so
|
||||
// all terms must match but can appear in any order or position.
|
||||
@@ -380,9 +397,10 @@ impl Db {
|
||||
"SELECT n.node_json, n.uid, n.path
|
||||
FROM nodes_fts f JOIN nodes n ON n.rowid = f.rowid
|
||||
WHERE f.name MATCH ?1 AND n.trashed = 0 AND n.node_json IS NOT NULL
|
||||
AND (?3 IS NULL OR n.path IS NULL OR n.path LIKE ?3 ESCAPE '\\')
|
||||
ORDER BY f.rank LIMIT ?2",
|
||||
)?;
|
||||
collect_hits(stmt.query_map(params![phrase, limit as i64], hit_row)?)?
|
||||
collect_hits(stmt.query_map(params![phrase, limit as i64, scope_pat], hit_row)?)?
|
||||
};
|
||||
|
||||
let mut hits = Vec::with_capacity(rows.len());
|
||||
@@ -392,6 +410,13 @@ impl Db {
|
||||
Some(path) => path,
|
||||
None => walk_path_of(&conn, &uid)?,
|
||||
};
|
||||
if let Some(scope) = scope
|
||||
&& !path
|
||||
.strip_prefix(scope)
|
||||
.is_some_and(|rest| rest.starts_with('/'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
hits.push(SearchHit { node, path });
|
||||
}
|
||||
Ok(hits)
|
||||
|
||||
@@ -1083,6 +1083,40 @@ fn search_trigram_substring_and_path() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_in_scope_only_returns_hits_below_the_folder() {
|
||||
let db = Db::open_in_memory().unwrap();
|
||||
db.upsert_node(&folder("root", None, "My Files")).unwrap();
|
||||
db.upsert_node(&folder("work", Some("root"), "Work"))
|
||||
.unwrap();
|
||||
db.upsert_node(&folder("sub", Some("work"), "Old")).unwrap();
|
||||
db.upsert_node(&folder("work2", Some("root"), "Work 2"))
|
||||
.unwrap();
|
||||
db.upsert_node(&file("f1", "work", "report.pdf", 1))
|
||||
.unwrap();
|
||||
db.upsert_node(&file("f2", "sub", "report-2019.pdf", 1))
|
||||
.unwrap();
|
||||
db.upsert_node(&file("f3", "work2", "report-copy.pdf", 1))
|
||||
.unwrap();
|
||||
db.upsert_node(&file("f4", "root", "report-top.pdf", 1))
|
||||
.unwrap();
|
||||
|
||||
let mut paths: Vec<String> = db
|
||||
.search_in("report", 10, Some("Work"))
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|h| h.path)
|
||||
.collect();
|
||||
paths.sort();
|
||||
// A sibling whose name starts with the scope is not inside it.
|
||||
assert_eq!(paths, vec!["Work/Old/report-2019.pdf", "Work/report.pdf"]);
|
||||
// Short queries take the LIKE path and scope the same way.
|
||||
assert_eq!(db.search_in("re", 10, Some("Work/Old")).unwrap().len(), 1);
|
||||
// No scope, or the root, is everywhere.
|
||||
assert_eq!(db.search_in("report", 10, None).unwrap().len(), 4);
|
||||
assert_eq!(db.search_in("report", 10, Some("")).unwrap().len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_excludes_trashed_and_respects_limit() {
|
||||
let db = Db::open_in_memory().unwrap();
|
||||
|
||||
Reference in new issue
Block a user