feat(cli,gui): sync with another computer's device

Two or more computers can now sync the same folders through one device in
Proton Drive. `pdfs devices join UID` and "Sync With This Computer…" on
another computer's row adopt that device as this machine's and then offer its
folders for syncing here, like a restore. Unlike "Continue This Backup Here",
the other computer goes on syncing, and the confirmation says that changes
and deletions on one reach the other.

Translated into every shipped language.
This commit is contained in:
nvrl committed 2026-10-16 16:04:26 +02:00
1 parent 950056b48d
commit 7a0f1ebd6f
11 files changed
+1207 -747

No files matched your search

+30
View File
@@ -630,6 +630,16 @@ enum DeviceCmd {
#[arg(long, conflicts_with = "uid")]
clear: bool,
},
/// Sync the same folders as another computer: this machine continues that
/// computer's device alongside it, and its folders are offered for syncing
/// here. Both computers keep them in step.
Join {
/// Device uid (from `devices list`).
uid: String,
/// Skip the confirmation and accept every proposed local path.
#[arg(long)]
yes: bool,
},
/// Move another computer's folders into this computer's backup and sync
/// them here. The other computer stays registered, without those folders.
Migrate {
@@ -992,6 +1002,26 @@ fn cmd_devices(action: DeviceCmd) -> Result<()> {
let uid = if clear { None } else { uid };
ok_or_bail(control_request(CtlRequest::AdoptDevice { uid })?)?
}
DeviceCmd::Join { uid, yes } => {
if !yes {
println!(
"This computer syncs into that device alongside it. Its folders are kept\n\
in step on both computers: an edit or a deletion on one reaches the other.\n\
New synced folders here go under that device too. Folders already synced\n\
here stay where they are."
);
confirm("Sync with it?")?;
}
let response = control_request(CtlRequest::AdoptDevice { uid: Some(uid) })?;
// A script gets the one reply and runs `sync restore` itself.
if emit_json(&response)? {
return Ok(());
}
ok_or_bail(response)?;
// The device is this machine's now, so the plain restore offers its
// folders.
cmd_sync_restore(yes, None)?
}
DeviceCmd::Migrate { uid, yes } => {
if !yes {
println!(
+67
View File
@@ -444,6 +444,10 @@ fn other_computer_row(ui: &Rc<Ui>, dev: &DeviceInfo) -> adw::ActionRow {
prompt_restore_folders(&ui_c, Some((uid_c.clone(), name_c.clone())))
});
let (ui_c, uid_c, name_c) = (ui.clone(), uid.clone(), name.clone());
menu.item(&gettext("Sync With This Computer…"), move || {
prompt_join_device(&ui_c, &uid_c, &name_c)
});
let (ui_c, uid_c, name_c) = (ui.clone(), uid.clone(), name.clone());
menu.item(&gettext("Move to This Computer…"), move || {
prompt_migrate_device(&ui_c, &uid_c, &name_c)
});
@@ -1100,6 +1104,69 @@ pub(crate) fn prompt_adopt_device(ui: &Rc<Ui>, uid: &str, name: &str) {
dialog.present(win.as_ref());
}
/// Ask before syncing into another computer's device alongside it, then offer
/// its folders for syncing here.
///
/// The same adoption as "Continue This Backup Here", but the other computer
/// goes on syncing, so the dialog says what that means: the two keep the same
/// folders in step, deletions included.
pub(crate) fn prompt_join_device(ui: &Rc<Ui>, uid: &str, name: &str) {
let win = ui_window(ui);
let dialog = adw::AlertDialog::builder()
.heading(gettext("Sync With This Computer?"))
// Translators: {name} is the name of the other computer this one syncs with.
.body(gettext_f(
"This computer and “{name}” keep the same folders in step. A change or a deletion on one reaches the other.\n\nNew synced folders on this computer go under “{name}” in Proton Drive. Folders already synced here stay where they are. Next you choose which of its folders to sync here.",
&[("name", name)],
))
.build();
dialog.add_response("cancel", &gettext("Cancel"));
dialog.add_response("join", &gettext("Sync Here"));
dialog.set_response_appearance("join", adw::ResponseAppearance::Suggested);
dialog.set_default_response(Some("cancel"));
dialog.set_close_response("cancel");
let ui = ui.clone();
let uid = uid.to_string();
// Translators: {name} is the name of the other computer this one now syncs with.
let done = gettext_f("Now syncing with “{name}”", &[("name", name)]);
dialog.connect_response(None, move |_, resp| {
if resp != "join" {
return;
}
ui.busy_begin();
let rx = spawn_request(
ui.dirs.control_socket(),
Request::AdoptDevice {
uid: Some(uid.clone()),
},
);
let (ui, done) = (ui.clone(), done.clone());
glib::spawn_future_local(async move {
let result = rx.recv().await;
ui.busy_end();
let failed = gettext("Couldn't sync with that computer");
match result {
Ok(Ok(Response::Ok { .. })) => {
reload_sync_pages(&ui);
toast(&ui, &done);
// The device is this computer's now, so its own restore
// picker offers the folders.
prompt_restore_folders(&ui, None);
}
Ok(Ok(Response::Error { message, kind })) => {
toast_failure(&ui, &failed, &message, kind)
}
_ => toast_error(
&ui,
&failed,
&gettext("The Proton Drive service didn't respond."),
),
}
});
});
dialog.present(win.as_ref());
}
/// Ask before moving another computer's folders into this computer's backup,
/// then offer them for syncing here.
///