mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-14 01:27:03 +01:00
fix(fuse): keep files offline in on-demand synced folders
`pdfs pin` and `pdfs unpin` resolved every path against the primary mount with `rel_to_mount`. An absolute path in an on-demand synced folder such as ~/Pictures was refused as "not under the mountpoint", and a relative path named a file in My files instead. A synced folder's file is a different Drive node from a same-named file in My files, so keeping one offline did nothing for the other, and every program reading the synced copy downloaded it. Pin and unpin now route through `route_to_mount`, like ls, refresh, rm and mv since B86, and run on the mount that owns the path. A path in a mirror folder gets an error saying its files are already on this computer. A pin made through an on-demand folder records the absolute local path, so `pdfs pins`, the prompt and the HUD open the file where it was kept; pins under the primary mount still record the mount-relative path. Fixes B281.
This commit is contained in:
1 parent
ad648ab5cc
commit
3f45e89391
6 files changed
+143
-13
No files matched your search
@@ -204,18 +204,42 @@ fn route_to_mount(core: &Core, mountpoint: &Path, path: &str) -> CoreResult<(Cor
|
||||
/// B99), so resolving one needs this too. A mirror folder has no mount to run
|
||||
/// against: a file there is an ordinary local file.
|
||||
fn route_change(core: &Core, mountpoint: &Path, path: &str) -> CoreResult<(Core, PathBuf)> {
|
||||
route_within(core, mountpoint, path, |path| {
|
||||
format!("{path} is in a mirrored folder; change the local file there instead")
|
||||
})
|
||||
}
|
||||
|
||||
/// [`route_to_mount`] for keeping a node offline or releasing it. A file in an
|
||||
/// on-demand synced folder is a different Drive node from any file of the
|
||||
/// same name under the primary mount, and keeping one offline did nothing for
|
||||
/// the other: a program reading the synced folder's copy downloaded it
|
||||
/// (`docs/BUGS.md` B281). A mirror folder already holds every file on disk.
|
||||
fn route_keep(core: &Core, mountpoint: &Path, path: &str) -> CoreResult<(Core, PathBuf)> {
|
||||
route_within(core, mountpoint, path, |path| {
|
||||
format!(
|
||||
"{path} is in a mirrored folder, which keeps all its files on this computer already"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// [`route_to_mount`], refusing a path that leaves the mount it routed to and
|
||||
/// answering a path in a mirror folder, which has no mount, with `mirrored`.
|
||||
fn route_within(
|
||||
core: &Core,
|
||||
mountpoint: &Path,
|
||||
path: &str,
|
||||
mirrored: impl FnOnce(&str) -> String,
|
||||
) -> CoreResult<(Core, PathBuf)> {
|
||||
let routed = route_to_mount(core, mountpoint, path).map_err(|e| {
|
||||
let p = Path::new(path);
|
||||
let mirrored = core
|
||||
let in_mirror = core
|
||||
.db
|
||||
.sync_folder_list()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.any(|folder| folder.mode == "mirror" && p.starts_with(&folder.local_path));
|
||||
if mirrored {
|
||||
CoreError::invalid(format!(
|
||||
"{path} is in a mirrored folder; change the local file there instead"
|
||||
))
|
||||
if in_mirror {
|
||||
CoreError::invalid(mirrored(path))
|
||||
} else {
|
||||
e
|
||||
}
|
||||
@@ -231,6 +255,18 @@ fn route_change(core: &Core, mountpoint: &Path, path: &str) -> CoreResult<(Core,
|
||||
}
|
||||
}
|
||||
|
||||
/// The path the pin registry keeps for a pin made through `routed`: the
|
||||
/// mount-relative path on the primary mount, as it always was, and the
|
||||
/// absolute path the request named on an on-demand folder's mount, since
|
||||
/// that node is not under the primary mountpoint (`docs/BUGS.md` B281).
|
||||
fn pin_record(routed: &Core, rel: &Path, path: &str) -> PathBuf {
|
||||
if routed.primary {
|
||||
rel.to_path_buf()
|
||||
} else {
|
||||
Path::new(path).components().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// [`route_change`] for a request that trashes or renames the node it names.
|
||||
/// A mount's root is My files or a whole synced folder. An on-demand folder's
|
||||
/// root has a parent on Drive, so nothing further down would stop `pdfs rm
|
||||
@@ -521,8 +557,8 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
|
||||
Ok(CtlRequest::Subscribe) => {
|
||||
CtlResponse::error(CoreError::invalid("a subscription is served on its own"))
|
||||
}
|
||||
Ok(CtlRequest::Pin { path }) => match rel_to_mount(mountpoint, &path) {
|
||||
Ok(rel) => match core.pin(&rel) {
|
||||
Ok(CtlRequest::Pin { path }) => match route_keep(core, mountpoint, &path) {
|
||||
Ok((routed, rel)) => match routed.pin(&rel, &pin_record(&routed, &rel, &path)) {
|
||||
Ok(name) => {
|
||||
core.touch_profile();
|
||||
CtlResponse::Ok {
|
||||
@@ -533,8 +569,8 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
|
||||
},
|
||||
Err(e) => CtlResponse::error(e),
|
||||
},
|
||||
Ok(CtlRequest::Unpin { path }) => match rel_to_mount(mountpoint, &path) {
|
||||
Ok(rel) => match core.unpin(&rel) {
|
||||
Ok(CtlRequest::Unpin { path }) => match route_keep(core, mountpoint, &path) {
|
||||
Ok((routed, rel)) => match routed.unpin(&rel) {
|
||||
Ok(name) => {
|
||||
core.touch_profile();
|
||||
CtlResponse::Ok {
|
||||
|
||||
@@ -4270,7 +4270,12 @@ impl Core {
|
||||
/// recursive pin and downloads every descendant file on a background thread,
|
||||
/// reported as a job: a big folder far outlasts the socket read timeout.
|
||||
/// Returns a human message.
|
||||
fn pin(&self, rel: &Path) -> CoreResult<String> {
|
||||
///
|
||||
/// `recorded` is the path the pin registry keeps for the front ends to
|
||||
/// open the pin by: `rel` itself on the primary mount, which they join to
|
||||
/// the mountpoint, and the absolute local path on an on-demand folder's
|
||||
/// mount, whose node is not reachable under the primary mountpoint at all.
|
||||
fn pin(&self, rel: &Path, recorded: &Path) -> CoreResult<String> {
|
||||
let (ino, uid) = self.resolve(rel)?;
|
||||
let (name, is_folder, mtime, size) = {
|
||||
let st = self.state();
|
||||
@@ -4294,7 +4299,7 @@ impl Core {
|
||||
// Record the recursive pin first so every descendant is eviction-
|
||||
// exempt before we start filling the cache with the subtree.
|
||||
self.cache
|
||||
.add_pin(&uid, rel, true)
|
||||
.add_pin(&uid, recorded, true)
|
||||
.map_err(|e| CoreError::from_api(&e, "pin"))?;
|
||||
let core = self.clone();
|
||||
let folder = name.clone();
|
||||
@@ -4306,7 +4311,7 @@ impl Core {
|
||||
}
|
||||
self.fetch_content(&uid, &name, mtime, size)?;
|
||||
self.cache
|
||||
.add_pin(&uid, rel, false)
|
||||
.add_pin(&uid, recorded, false)
|
||||
.map_err(|e| CoreError::from_api(&e, "pin"))?;
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
@@ -3447,6 +3447,56 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
|
||||
fn pinning_a_file_in_an_on_demand_folder_keeps_that_folders_copy() {
|
||||
// Keeping a file offline took only paths under the primary mount, so
|
||||
// the copy in an on-demand synced folder could not be kept, and a
|
||||
// program reading it there downloaded it every time (B281).
|
||||
let drive = FakeDrive::new();
|
||||
let dir = scratch("pin-in-ondemand");
|
||||
let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap();
|
||||
let ondemand = dir.join("ondemand");
|
||||
sync_folder(&daemon, &ondemand, "ondemand");
|
||||
|
||||
let content = b"not a real png, but kept all the same".repeat(64);
|
||||
let file = ondemand.join("photo.png");
|
||||
std::fs::write(&file, &content).unwrap();
|
||||
assert!(wait_until(Duration::from_secs(30), || {
|
||||
daemon.pending().is_ok_and(|items| items.is_empty())
|
||||
}));
|
||||
daemon.request(&Request::PurgeCache).unwrap();
|
||||
|
||||
let path = file.canonicalize().unwrap().to_string_lossy().into_owned();
|
||||
let pinned = daemon
|
||||
.request(&Request::Pin { path: path.clone() })
|
||||
.unwrap();
|
||||
assert!(matches!(pinned, Response::Ok { .. }), "{pinned:?}");
|
||||
let pins = || match daemon.request(&Request::ListPins) {
|
||||
Ok(Response::Pins { pins }) => pins,
|
||||
other => panic!("pins: {other:?}"),
|
||||
};
|
||||
let listed = pins();
|
||||
assert!(
|
||||
listed.iter().any(|pin| pin.path == path && pin.cached),
|
||||
"the pin opens by the path it was made through: {listed:?}"
|
||||
);
|
||||
|
||||
// With the link gone, only a kept copy can answer the read.
|
||||
daemon.client.set_online(false);
|
||||
assert_eq!(std::fs::read(&file).unwrap(), content);
|
||||
daemon.client.set_online(true);
|
||||
|
||||
let unpinned = daemon
|
||||
.request(&Request::Unpin { path: path.clone() })
|
||||
.unwrap();
|
||||
assert!(matches!(unpinned, Response::Ok { .. }), "{unpinned:?}");
|
||||
assert!(pins().is_empty());
|
||||
|
||||
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// The synced folder `id` as the daemon lists it.
|
||||
fn listed_folder(daemon: &Daemon, id: i64) -> Option<control::SyncFolderInfo> {
|
||||
match daemon.request(&Request::ListSyncFolders) {
|
||||
|
||||
Reference in new issue
Block a user