mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
prompt refactor
This commit is contained in:
1 parent
39ab4aae48
commit
ea56818a80
5 files changed
+161
-2
No files matched your search
@@ -678,6 +678,11 @@ pub struct SearchHit {
|
||||
pub pinned: bool,
|
||||
/// Node uid in `volume~link` form.
|
||||
pub uid: String,
|
||||
/// Absolute path through the most specific active on-demand mount, when the
|
||||
/// node is covered by one. Clients should prefer this for folders and
|
||||
/// streamable media, and fall back to `mountpoint/path` when absent.
|
||||
#[serde(default)]
|
||||
pub mounted_path: Option<String>,
|
||||
/// Relevance assigned by the daemon. Higher scores sort first. Legacy
|
||||
/// daemons omit it, in which case clients retain the daemon's result order.
|
||||
#[serde(default)]
|
||||
@@ -1369,6 +1374,7 @@ mod tests {
|
||||
modified: 123,
|
||||
pinned: true,
|
||||
uid: "vol~link".into(),
|
||||
mounted_path: Some("/home/me/Videos/report.pdf".into()),
|
||||
score: 985,
|
||||
}],
|
||||
local_hits: vec![LocalHit {
|
||||
|
||||
@@ -160,6 +160,40 @@ impl Db {
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Resolve `uid` relative to an ancestor node.
|
||||
///
|
||||
/// This is used when a remote subtree has its own local sync mount: the
|
||||
/// sync folder stores the ancestor UID, while search results identify the
|
||||
/// selected descendant. Returning `None` for an unrelated or incomplete
|
||||
/// chain keeps callers from accidentally joining a Drive-wide path onto the
|
||||
/// wrong mountpoint. The ancestor itself resolves to the empty path.
|
||||
pub fn path_relative_to(&self, ancestor_uid: &str, uid: &str) -> Result<Option<String>> {
|
||||
let conn = self.conn.lock();
|
||||
let mut stmt = conn.prepare(
|
||||
"WITH RECURSIVE anc(uid, parent_uid, name, depth) AS (
|
||||
SELECT uid, parent_uid, name, 0 FROM nodes WHERE uid = ?1
|
||||
UNION ALL
|
||||
SELECT n.uid, n.parent_uid, n.name, anc.depth + 1
|
||||
FROM nodes n JOIN anc ON n.uid = anc.parent_uid
|
||||
WHERE anc.depth < 1024
|
||||
)
|
||||
SELECT uid, name FROM anc ORDER BY depth",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![uid], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})?;
|
||||
let mut parts = Vec::new();
|
||||
for row in rows {
|
||||
let (current_uid, name) = row?;
|
||||
if current_uid == ancestor_uid {
|
||||
parts.reverse();
|
||||
return Ok(Some(parts.join("/")));
|
||||
}
|
||||
parts.push(name);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Full-text search over node names, newest schema's trigram index giving
|
||||
/// substring (not just prefix) matches. Returns up to `limit` non-trashed
|
||||
/// hits, each with its mountpoint-relative path resolved. Queries shorter
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::control::{ActivityEntry, ActivityKind};
|
||||
use crate::localindex::LocalEntry;
|
||||
use proton_drive_rs::proton_sdk::ids::NodeUid;
|
||||
use proton_drive_rs::{Node, NodeKind};
|
||||
use serde_json::json;
|
||||
|
||||
/// `Db::open` applies the pragmas that `open_in_memory` skips, so the WAL
|
||||
/// settings can only be checked against a real file.
|
||||
@@ -1181,6 +1182,96 @@ fn pending_mode_is_queued_until_the_mode_is_reached() {
|
||||
assert_eq!(folder.pending_mode, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_path_resolves_relative_to_a_sync_root_uid() {
|
||||
let db = Db::open_in_memory().unwrap();
|
||||
let root = node_from(serde_json::Value::Null, "root", "My Files", json!("Folder"));
|
||||
let videos = node_from(json!(uid("root")), "videos", "Videos", json!("Folder"));
|
||||
let anime = node_from(json!(uid("videos")), "anime", "anime", json!("Folder"));
|
||||
let show = node_from(json!(uid("anime")), "show", "Oshi no Ko", json!("Folder"));
|
||||
let episode = node_from(
|
||||
json!(uid("show")),
|
||||
"episode",
|
||||
"episode 01.mkv",
|
||||
json!({"File": {
|
||||
"media_type": "video/x-matroska",
|
||||
"total_size_on_storage": 42,
|
||||
"claimed_size": 42,
|
||||
"claimed_modification_time": null
|
||||
}}),
|
||||
);
|
||||
db.upsert_nodes(&[root, videos, anime, show, episode])
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
db.path_relative_to(&uid("videos").to_string(), &uid("episode").to_string())
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("anime/Oshi no Ko/episode 01.mkv")
|
||||
);
|
||||
assert_eq!(
|
||||
db.path_relative_to(&uid("videos").to_string(), &uid("videos").to_string())
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("")
|
||||
);
|
||||
assert_eq!(
|
||||
db.path_relative_to(&uid("anime").to_string(), &uid("episode").to_string())
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("Oshi no Ko/episode 01.mkv")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_path_rejects_unrelated_missing_and_broken_ancestor_chains() {
|
||||
let db = Db::open_in_memory().unwrap();
|
||||
let root = node_from(serde_json::Value::Null, "root", "My Files", json!("Folder"));
|
||||
let videos = node_from(json!(uid("root")), "videos", "Videos", json!("Folder"));
|
||||
let episode = node_from(
|
||||
json!(uid("videos")),
|
||||
"episode",
|
||||
"episode.mkv",
|
||||
json!({"File": {
|
||||
"media_type": "video/x-matroska",
|
||||
"total_size_on_storage": 42,
|
||||
"claimed_size": 42,
|
||||
"claimed_modification_time": null
|
||||
}}),
|
||||
);
|
||||
let unrelated = node_from(
|
||||
json!(uid("root")),
|
||||
"documents",
|
||||
"Documents",
|
||||
json!("Folder"),
|
||||
);
|
||||
let orphan = node_from(
|
||||
json!(uid("missing-parent")),
|
||||
"orphan",
|
||||
"orphan.mkv",
|
||||
json!({"File": {
|
||||
"media_type": "video/x-matroska",
|
||||
"total_size_on_storage": 42,
|
||||
"claimed_size": 42,
|
||||
"claimed_modification_time": null
|
||||
}}),
|
||||
);
|
||||
db.upsert_nodes(&[root, videos, episode, unrelated, orphan])
|
||||
.unwrap();
|
||||
|
||||
for (ancestor, descendant) in [
|
||||
("documents", "episode"),
|
||||
("videos", "missing-node"),
|
||||
("videos", "orphan"),
|
||||
] {
|
||||
assert_eq!(
|
||||
db.path_relative_to(&uid(ancestor).to_string(), &uid(descendant).to_string())
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_index_touch_access_and_lru_order() {
|
||||
let db = Db::open_in_memory().unwrap();
|
||||
|
||||
@@ -1784,6 +1784,24 @@ impl Core {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Return an absolute path through the most specific live on-demand mount
|
||||
/// covering `uid`. A node may be below several configured roots, so prefer
|
||||
/// the root leaving the shortest descendant path.
|
||||
fn mounted_search_path(&self, uid: &str) -> Option<String> {
|
||||
let live_mounts = self.mounts.lock();
|
||||
let folders = self.db.sync_folder_list().ok()?;
|
||||
folders
|
||||
.into_iter()
|
||||
.filter(|folder| folder.mode == "ondemand" && live_mounts.contains_key(&folder.id))
|
||||
.filter_map(|folder| {
|
||||
let relative = self.db.path_relative_to(&folder.remote_uid, uid).ok()??;
|
||||
let depth = relative.split('/').count();
|
||||
Some((depth, PathBuf::from(folder.local_path).join(relative)))
|
||||
})
|
||||
.min_by_key(|(depth, _)| *depth)
|
||||
.map(|(_, path)| path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// Full-text search node names against the local SQLite index, mapping each
|
||||
/// DB hit to the wire [`SearchHit`] (resolving live pin state from the cache,
|
||||
/// which the DB doesn't track). Pure local lookup — never hits the network.
|
||||
@@ -1802,6 +1820,7 @@ impl Core {
|
||||
modified: h.node.modification_time,
|
||||
pinned: self.cache.is_pinned(&h.node.uid),
|
||||
uid: h.node.uid.to_string(),
|
||||
mounted_path: self.mounted_search_path(&h.node.uid.to_string()),
|
||||
score: 0,
|
||||
})
|
||||
.collect())
|
||||
@@ -1865,6 +1884,7 @@ impl Core {
|
||||
modified: hit.node.modification_time,
|
||||
pinned,
|
||||
uid: hit.node.uid.to_string(),
|
||||
mounted_path: self.mounted_search_path(&hit.node.uid.to_string()),
|
||||
score,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -756,6 +756,7 @@ impl Ui {
|
||||
modified: 0,
|
||||
pinned: true,
|
||||
uid: pin.uid,
|
||||
mounted_path: None,
|
||||
score: 0,
|
||||
})
|
||||
.collect(),
|
||||
@@ -1090,14 +1091,20 @@ impl Ui {
|
||||
// this handles non-recursively pinned folders without mistaking
|
||||
// them for files and sending an invalid OpenFile request.
|
||||
if self.rendered_query.borrow().is_empty() {
|
||||
let path = mounted_path(&self.mountpoint.borrow(), &drive.path);
|
||||
let path = drive.mounted_path.as_deref().map_or_else(
|
||||
|| mounted_path(&self.mountpoint.borrow(), &drive.path),
|
||||
PathBuf::from,
|
||||
);
|
||||
xdg_open(&path);
|
||||
self.dismiss();
|
||||
return;
|
||||
}
|
||||
match drive_activation(&drive.name, drive.is_dir) {
|
||||
DriveActivation::Folder | DriveActivation::MountedMedia => {
|
||||
let path = mounted_path(&self.mountpoint.borrow(), &drive.path);
|
||||
let path = drive.mounted_path.as_deref().map_or_else(
|
||||
|| mounted_path(&self.mountpoint.borrow(), &drive.path),
|
||||
PathBuf::from,
|
||||
);
|
||||
xdg_open(&path);
|
||||
self.dismiss();
|
||||
return;
|
||||
@@ -1442,6 +1449,7 @@ mod tests {
|
||||
modified: 1,
|
||||
pinned: false,
|
||||
uid: "v~a".into(),
|
||||
mounted_path: None,
|
||||
score: 100,
|
||||
}),
|
||||
Hit::Local(LocalHit {
|
||||
|
||||
Reference in new issue
Block a user