mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 00:47:00 +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
@@ -125,6 +125,9 @@ struct Ui {
|
||||
/// Whether this run already asked for the mailbox password of a locked
|
||||
/// account, so a refresh never re-opens a dialog the user cancelled.
|
||||
unlock_offered: Cell<bool>,
|
||||
/// The `since` of the run of mount failures whose "folder isn't empty"
|
||||
/// dialog this run already showed, so a page reload never shows it again.
|
||||
not_empty_offered: Cell<i64>,
|
||||
/// Whether the last refresh saw a live mount daemon. Gates the unpin buttons
|
||||
/// (which need the daemon to evict + re-hydrate) and every mutating action.
|
||||
mounted: RefCell<bool>,
|
||||
@@ -403,6 +406,7 @@ fn build_window(app: &adw::Application) {
|
||||
opening: RefCell::new(HashSet::new()),
|
||||
session: RefCell::new(startup_session),
|
||||
unlock_offered: Cell::new(false),
|
||||
not_empty_offered: Cell::new(0),
|
||||
mounted: RefCell::new(false),
|
||||
sidebar: sidebar_list.clone(),
|
||||
nav: split.clone(),
|
||||
@@ -1807,6 +1811,23 @@ pub(crate) fn service_unreachable(
|
||||
.ok()
|
||||
.flatten();
|
||||
match failure {
|
||||
Some(failure) if failure.not_empty.is_some() => {
|
||||
let found = failure.not_empty.as_ref().expect("checked above");
|
||||
status(
|
||||
&ui,
|
||||
"dialog-warning-symbolic",
|
||||
&gettext("Can't open your Drive"),
|
||||
// Translators: {path} is the folder My files is mounted at.
|
||||
&gettext_f(
|
||||
"“{path}” already has files in it, and My files would hide them. Proton Drive tries again on its own once the folder is empty. You can also choose another folder for My files in Preferences.",
|
||||
&[("path", &found.path.display().to_string())],
|
||||
),
|
||||
true,
|
||||
);
|
||||
if ui.not_empty_offered.replace(failure.since) != failure.since {
|
||||
offer_not_empty(&ui, found);
|
||||
}
|
||||
}
|
||||
Some(failure) => status(
|
||||
&ui,
|
||||
"dialog-warning-symbolic",
|
||||
@@ -1834,6 +1855,37 @@ pub(crate) fn service_unreachable(
|
||||
});
|
||||
}
|
||||
|
||||
/// Tell the user the daemon will not mount My files over a folder with files
|
||||
/// in it (B277), and offer the two ways out: open the folder to move them, or
|
||||
/// choose another one.
|
||||
fn offer_not_empty(ui: &Rc<Ui>, found: &pdfs_core::mountpoint::NotEmpty) {
|
||||
let path = found.path.display().to_string();
|
||||
let dialog = adw::AlertDialog::builder()
|
||||
.heading(gettext("Your Proton Drive Folder Isn't Empty"))
|
||||
// Translators: {path} is the folder My files is mounted at; {n} is how
|
||||
// many files and folders are already in it.
|
||||
.body(ngettext_f(
|
||||
"“{path}” already has {n} item in it. My files would hide it, so Proton Drive does not open there. Move the item to another folder, or choose another folder for My files.",
|
||||
"“{path}” already has {n} items in it. My files would hide them, so Proton Drive does not open there. Move the items to another folder, or choose another folder for My files.",
|
||||
found.count as u64,
|
||||
&[("path", &path)],
|
||||
))
|
||||
.build();
|
||||
dialog.add_response("close", &gettext("Not Now"));
|
||||
dialog.add_response("open", &gettext("Open Folder"));
|
||||
dialog.add_response("choose", &gettext("Choose Another Folder…"));
|
||||
dialog.set_response_appearance("choose", adw::ResponseAppearance::Suggested);
|
||||
dialog.set_default_response(Some("choose"));
|
||||
dialog.set_close_response("close");
|
||||
let ui_response = ui.clone();
|
||||
dialog.connect_response(None, move |_, response| match response {
|
||||
"open" => open_path(&path),
|
||||
"choose" => prompt_mountpoint(&ui_response),
|
||||
_ => {}
|
||||
});
|
||||
dialog.present(ui_window(ui).as_ref());
|
||||
}
|
||||
|
||||
/// How long a copy button shows its check after copying.
|
||||
const COPIED_FEEDBACK: Duration = Duration::from_millis(1500);
|
||||
|
||||
|
||||
@@ -806,7 +806,8 @@ pub(crate) fn settings_request(ui: &Rc<Ui>, req: Request, done: String, failed:
|
||||
}
|
||||
|
||||
/// Prompt for a new mountpoint folder, persist it to config, and offer to restart
|
||||
/// the mount service so the daemon picks it up.
|
||||
/// the mount service so the daemon picks it up. A folder with files in it is
|
||||
/// refused: the mount would hide them (B277).
|
||||
pub(crate) fn prompt_mountpoint(ui: &Rc<Ui>) {
|
||||
let win = ui_window(ui);
|
||||
let dialog = gtk4::FileDialog::builder()
|
||||
@@ -816,46 +817,89 @@ pub(crate) fn prompt_mountpoint(ui: &Rc<Ui>) {
|
||||
dialog.select_folder(win.as_ref(), gio::Cancellable::NONE, move |res| {
|
||||
let Ok(folder) = res else { return };
|
||||
let Some(path) = folder.path() else { return };
|
||||
let path_str = path.display().to_string();
|
||||
|
||||
// Persist the choice to config so the next mount uses it.
|
||||
let mut config = ui.dirs.load_config();
|
||||
config.mountpoint = Some(path_str.clone());
|
||||
if let Err(e) = ui.dirs.save_config(&config) {
|
||||
toast_error(&ui, &gettext("Couldn't save the folder"), &e.to_string());
|
||||
// The folder My files is mounted at lists Drive's files, not its own,
|
||||
// so picking it again is not checked.
|
||||
if path == ui.dirs.resolved_mountpoint(&ui.dirs.load_config()) {
|
||||
use_mountpoint(&ui, &path);
|
||||
return;
|
||||
}
|
||||
ui.status.mountpoint_row.set_subtitle(&path_str);
|
||||
// The Locations page shows the same path as a row title. Any other page
|
||||
// reads it again on the next visit.
|
||||
if ui.stack.visible_child_name().as_deref() == Some("locations") {
|
||||
load_locations(&ui);
|
||||
}
|
||||
|
||||
// The daemon only reads the mountpoint at mount time, so offer a restart.
|
||||
let confirm = adw::AlertDialog::builder()
|
||||
.heading(gettext("Restart Proton Drive?"))
|
||||
// Translators: {path} is the folder the drive is mounted at.
|
||||
.body(gettext_f(
|
||||
"My files now appears in “{path}” after Proton Drive restarts.",
|
||||
&[("path", &path_str)],
|
||||
))
|
||||
.build();
|
||||
confirm.add_response("later", &gettext("Later"));
|
||||
confirm.add_response("restart", &gettext("Restart Now"));
|
||||
confirm.set_response_appearance("restart", adw::ResponseAppearance::Suggested);
|
||||
confirm.set_default_response(Some("restart"));
|
||||
confirm.set_close_response("later");
|
||||
let ui_restart = ui.clone();
|
||||
confirm.connect_response(None, move |_, resp| {
|
||||
if resp == "restart" {
|
||||
restart_service_then(&ui_restart, |_| {});
|
||||
let ui = ui.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let checked = path.clone();
|
||||
let found = gio::spawn_blocking(move || pdfs_core::mountpoint::not_empty(&checked))
|
||||
.await
|
||||
.unwrap_or_else(|_| Err(std::io::Error::other("the check did not finish")));
|
||||
match found {
|
||||
Ok(None) => use_mountpoint(&ui, &path),
|
||||
Ok(Some(found)) => refuse_mountpoint(&ui, &found),
|
||||
Err(e) => toast_error(&ui, &gettext("Couldn't read the folder"), &e.to_string()),
|
||||
}
|
||||
});
|
||||
confirm.present(ui_window(&ui).as_ref());
|
||||
});
|
||||
}
|
||||
|
||||
/// Explain why the folder `found` cannot hold My files.
|
||||
fn refuse_mountpoint(ui: &Rc<Ui>, found: &pdfs_core::mountpoint::NotEmpty) {
|
||||
let path = found.path.display().to_string();
|
||||
let dialog = adw::AlertDialog::builder()
|
||||
.heading(gettext("Choose an Empty Folder"))
|
||||
// Translators: {path} is the folder the user picked; {n} is how many
|
||||
// files and folders are already in it.
|
||||
.body(ngettext_f(
|
||||
"“{path}” already has {n} item in it. My files would hide it while Proton Drive runs, and it would not be uploaded.",
|
||||
"“{path}” already has {n} items in it. My files would hide them while Proton Drive runs, and they would not be uploaded.",
|
||||
found.count as u64,
|
||||
&[("path", &path)],
|
||||
))
|
||||
.build();
|
||||
dialog.add_response("close", &gettext("Close"));
|
||||
dialog.set_default_response(Some("close"));
|
||||
dialog.set_close_response("close");
|
||||
dialog.present(ui_window(ui).as_ref());
|
||||
}
|
||||
|
||||
/// Make `path` the folder My files is mounted at, and offer the restart that
|
||||
/// moves it there.
|
||||
fn use_mountpoint(ui: &Rc<Ui>, path: &std::path::Path) {
|
||||
let path_str = path.display().to_string();
|
||||
|
||||
// Persist the choice to config so the next mount uses it.
|
||||
let mut config = ui.dirs.load_config();
|
||||
config.mountpoint = Some(path_str.clone());
|
||||
if let Err(e) = ui.dirs.save_config(&config) {
|
||||
toast_error(ui, &gettext("Couldn't save the folder"), &e.to_string());
|
||||
return;
|
||||
}
|
||||
ui.status.mountpoint_row.set_subtitle(&path_str);
|
||||
// The Locations page shows the same path as a row title. Any other page
|
||||
// reads it again on the next visit.
|
||||
if ui.stack.visible_child_name().as_deref() == Some("locations") {
|
||||
load_locations(ui);
|
||||
}
|
||||
|
||||
// The daemon only reads the mountpoint at mount time, so offer a restart.
|
||||
let confirm = adw::AlertDialog::builder()
|
||||
.heading(gettext("Restart Proton Drive?"))
|
||||
// Translators: {path} is the folder the drive is mounted at.
|
||||
.body(gettext_f(
|
||||
"My files now appears in “{path}” after Proton Drive restarts.",
|
||||
&[("path", &path_str)],
|
||||
))
|
||||
.build();
|
||||
confirm.add_response("later", &gettext("Later"));
|
||||
confirm.add_response("restart", &gettext("Restart Now"));
|
||||
confirm.set_response_appearance("restart", adw::ResponseAppearance::Suggested);
|
||||
confirm.set_default_response(Some("restart"));
|
||||
confirm.set_close_response("later");
|
||||
let ui_restart = ui.clone();
|
||||
confirm.connect_response(None, move |_, resp| {
|
||||
if resp == "restart" {
|
||||
restart_service_then(&ui_restart, |_| {});
|
||||
}
|
||||
});
|
||||
confirm.present(ui_window(ui).as_ref());
|
||||
}
|
||||
|
||||
/// Connect the Files/Photos "Retry" buttons (shown by [`browser_unreachable`] /
|
||||
/// [`gallery_unreachable`] when the mount is down): restart the systemd unit and
|
||||
/// reload the page.
|
||||
|
||||
Reference in new issue
Block a user