mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-13 17:27:03 +01:00
fix(core,fuse,cli,gui): refuse to mount My files over a folder with files in it
FUSE mounts over a non-empty folder without complaint, so files already in the My files folder were hidden under the mount: not uploaded, and back only once the service stopped, so they looked deleted (#67, B277). - pdfs-core: new `mountpoint` module. `not_empty` lists what is in a folder (hidden entries count); `mounted_fs_type` moves here from the CLI so the daemon can use it. `MountFailure` gains an optional `not_empty` field, which older readers ignore. - pdfs-fuse: `mount_with` checks the folder before the root fetch and before any worker starts. A dead mount is cleared first; one of our own live mounts is left to `spawn_session`, as before. - pdfs-cli: `cmd_daemon` records the folder in the failure record. The refusal backs off and retries like any mount failure, so emptying the folder is enough. - pdfs-gui: the status pages explain the refusal, and a dialog offers to open the folder or choose another one, once per run of failures. The folder picker refuses a folder with files in it. Translated in all catalogs. Docs: BUGS (B277), CHANGELOG, FAQ, TROUBLESHOOTING, CONFIGURATION.
This commit is contained in:
1 parent
321547fb77
commit
5048492a83
22 files changed
+2105
-1251
No files matched your search
+36
-21
@@ -2432,6 +2432,7 @@ fn cmd_daemon(mountpoint: Option<PathBuf>) -> Result<()> {
|
||||
since: failure.as_ref().map_or(now, |f| f.since),
|
||||
attempts,
|
||||
next_attempt: now + delay.as_secs() as i64,
|
||||
not_empty: not_empty_cause(&e),
|
||||
};
|
||||
if let Err(e) = pdfs_core::mountfail::record(&state_dir, &record) {
|
||||
tracing::warn!(error = %e, "could not record the mount failure");
|
||||
@@ -2444,6 +2445,23 @@ fn cmd_daemon(mountpoint: Option<PathBuf>) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The folder in the way, when `error` is a mount refused over a folder that
|
||||
/// already has files in it. The refusal travels inside an `io::Error`, whose
|
||||
/// `source` skips the error it wraps, so each link is unwrapped by hand.
|
||||
fn not_empty_cause(error: &anyhow::Error) -> Option<pdfs_core::mountpoint::NotEmpty> {
|
||||
error.chain().find_map(|cause| {
|
||||
cause
|
||||
.downcast_ref::<pdfs_core::mountpoint::NotEmpty>()
|
||||
.or_else(|| {
|
||||
cause
|
||||
.downcast_ref::<std::io::Error>()?
|
||||
.get_ref()?
|
||||
.downcast_ref::<pdfs_core::mountpoint::NotEmpty>()
|
||||
})
|
||||
.cloned()
|
||||
})
|
||||
}
|
||||
|
||||
/// How long to wait before mount attempt `attempts + 1`, after `attempts`
|
||||
/// failures in a row.
|
||||
///
|
||||
@@ -3738,26 +3756,6 @@ fn executable_on_path(name: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn mounted_fs_type(path: &Path) -> Option<String> {
|
||||
let wanted = path.canonicalize().ok()?;
|
||||
let mounts = std::fs::read_to_string("/proc/self/mountinfo").ok()?;
|
||||
mounts
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let fields: Vec<_> = line.split_whitespace().collect();
|
||||
let separator = fields.iter().position(|field| *field == "-")?;
|
||||
let encoded = *fields.get(4)?;
|
||||
let decoded = encoded.replace("\\040", " ").replace("\\011", "\t");
|
||||
let mountpoint = PathBuf::from(decoded);
|
||||
if mountpoint.canonicalize().ok()? == wanted {
|
||||
Some(fields.get(separator + 1)?.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next()
|
||||
}
|
||||
|
||||
/// Create, sync, and remove a new file without ever replacing an existing one.
|
||||
fn writable_probe(dir: &Path) -> std::io::Result<()> {
|
||||
let name = format!(".pdfs-diagnose-probe-{}", std::process::id());
|
||||
@@ -3920,7 +3918,7 @@ fn cmd_diagnose() -> Result<()> {
|
||||
outdated_daemon_note(&version),
|
||||
);
|
||||
}
|
||||
let live_type = mounted_fs_type(Path::new(&mountpoint));
|
||||
let live_type = pdfs_core::mountpoint::mounted_fs_type(Path::new(&mountpoint));
|
||||
let is_fuse = live_type
|
||||
.as_deref()
|
||||
.is_some_and(|kind| kind.starts_with("fuse"));
|
||||
@@ -4329,6 +4327,23 @@ mod login_wait_tests {
|
||||
assert_eq!(secs, [5, 10, 20, 40, 80, 120, 120, 120]);
|
||||
assert_eq!(mount_retry_delay(u32::MAX).as_secs(), 120);
|
||||
}
|
||||
|
||||
/// A refusal over a folder with files in it reaches the failure record
|
||||
/// through the `io::Error` and the context around it (`docs/BUGS.md` B277).
|
||||
#[test]
|
||||
fn a_mount_refused_over_files_is_recorded_as_such() {
|
||||
let found = pdfs_core::mountpoint::NotEmpty {
|
||||
path: PathBuf::from("/home/you/ProtonDrive"),
|
||||
entries: vec!["Work".into()],
|
||||
count: 1,
|
||||
};
|
||||
let error =
|
||||
anyhow::Error::new(std::io::Error::other(found.clone())).context("mount failed");
|
||||
assert_eq!(not_empty_cause(&error), Some(found));
|
||||
let other =
|
||||
anyhow::Error::new(std::io::Error::other("missing key")).context("mount failed");
|
||||
assert_eq!(not_empty_cause(&other), None);
|
||||
}
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user