feat(cli,fuse,core): copy files and folders on Proton Drive

`pdfs copy PATH... NEW_PARENT` sends a new control request, Copy, which
copies a file with its content or a folder with its subtree on Proton
Drive through proton-drive-rs's copy_node. Nothing is downloaded or
uploaded. A name already taken in the destination gets the next free
" (n)" suffix through get_available_name.

A copy cannot wait in the queue, because Drive copies what it holds.
So it is refused offline, and while the mount still owes Drive part of
the node: a local placeholder, a queued op, unsaved writes, or, for a
folder, any queued op at all. Local-first does not change this.

DriveApi gains copy_node and get_available_name, and the fake drive
copies subtrees, so a FUSE sim test covers the copy, the suffix, the
folder subtree and both refusals. The activity feed logs a new Copy
kind, translated in every catalog.

Needs the proton-drive-rs release that adds copy_node; until then, build
with a [patch.crates-io] pointing at proton-sdk-rs.
This commit is contained in:
nvrl committed 2026-10-25 21:10:50 +01:00
1 parent a73ed34421
commit c1d9538669
20 files changed
+1073 -600

No files matched your search

+77 -65
View File
@@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: proton-drive-linux 2.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-10-17 01:23+0200\n"
"POT-Creation-Date: 2026-10-25 20:52+0100\n"
"PO-Revision-Date: 2026-09-23 12:00+0200\n"
"Last-Translator: Proton Drive for Linux contributors\n"
"Language-Team: Dutch\n"
@@ -58,7 +58,7 @@ msgid "%A, %B %-d, %Y, %H:%M"
msgstr "%A %-d %B %Y, %H:%M"
#: crates/pdfs-gui/src/app/main.rs:856
#: crates/pdfs-gui/src/app/pages/activity_text.rs:539
#: crates/pdfs-gui/src/app/pages/activity_text.rs:543
#: crates/pdfs-gui/src/app/pages/browser.rs:454
#: crates/pdfs-gui/src/app/pages/browser.rs:833
#: crates/pdfs-gui/src/app/pages/browser.rs:1239
@@ -821,189 +821,201 @@ msgstr "{name} verplaatst"
msgid "Couldn't move {name}"
msgstr "Kon {name} niet verplaatsen"
#. Translators: {name} is a folder name.
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:183
#, rust-format
msgid "Copied {name}"
msgstr "{name} gekopieerd"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:185
#, rust-format
msgid "Couldn't copy {name}"
msgstr "Kon {name} niet kopiëren"
#. Translators: {name} is a folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:187
#, rust-format
msgid "Created folder {name}"
msgstr "Map {name} aangemaakt"
#. Translators: {name} is a folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:185
#: crates/pdfs-gui/src/app/pages/activity_text.rs:189
#, rust-format
msgid "Couldn't create folder {name}"
msgstr "Kon map {name} niet aanmaken"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:187
#: crates/pdfs-gui/src/app/pages/activity_text.rs:191
#, rust-format
msgid "Moved {name} to the Trash"
msgstr "{name} naar de prullenbak verplaatst"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:189
#: crates/pdfs-gui/src/app/pages/activity_text.rs:193
#, rust-format
msgid "Couldn't move {name} to the Trash"
msgstr "Kon {name} niet naar de prullenbak verplaatsen"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:191
#: crates/pdfs-gui/src/app/pages/activity_text.rs:195
#, rust-format
msgid "Restored {name}"
msgstr "{name} teruggezet"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:193
#: crates/pdfs-gui/src/app/pages/activity_text.rs:197
#, rust-format
msgid "Couldn't restore {name}"
msgstr "Kon {name} niet terugzetten"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:195
#: crates/pdfs-gui/src/app/pages/activity_text.rs:199
#, rust-format
msgid "Deleted {name} forever"
msgstr "{name} definitief verwijderd"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:197
#: crates/pdfs-gui/src/app/pages/activity_text.rs:201
#, rust-format
msgid "Couldn't delete {name}"
msgstr "Kon {name} niet verwijderen"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:198
#: crates/pdfs-gui/src/app/pages/activity_text.rs:202
msgid "Emptied the Trash"
msgstr "Prullenbak geleegd"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:199
#: crates/pdfs-gui/src/app/pages/activity_text.rs:203
msgid "Couldn't empty the Trash"
msgstr "Kon de prullenbak niet legen"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:201
#: crates/pdfs-gui/src/app/pages/activity_text.rs:205
#, rust-format
msgid "Shared {name}"
msgstr "{name} gedeeld"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:203
#: crates/pdfs-gui/src/app/pages/activity_text.rs:207
#, rust-format
msgid "Couldn't share {name}"
msgstr "Kon {name} niet delen"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:205
#: crates/pdfs-gui/src/app/pages/activity_text.rs:209
#, rust-format
msgid "Created a link to {name}"
msgstr "Link naar {name} aangemaakt"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:207
#: crates/pdfs-gui/src/app/pages/activity_text.rs:211
#, rust-format
msgid "Couldn't create a link to {name}"
msgstr "Kon geen link naar {name} aanmaken"
#. Translators: {name} is a file or folder name.
#. Translators: {name} is the item's name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:209
#: crates/pdfs-gui/src/app/pages/activity_text.rs:213
#: crates/pdfs-gui/src/app/pages/shared_by_me.rs:559
#, rust-format
msgid "Stopped sharing {name}"
msgstr "Delen van {name} gestopt"
#. Translators: {name} is a file or folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:211
#: crates/pdfs-gui/src/app/pages/activity_text.rs:215
#, rust-format
msgid "Couldn't stop sharing {name}"
msgstr "Kon het delen van {name} niet stoppen"
#. Translators: {name} is the name of the conflicting copy.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:213
#: crates/pdfs-gui/src/app/pages/activity_text.rs:217
#, rust-format
msgid "Conflicting copy {name}"
msgstr "Conflictkopie {name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:236
#: crates/pdfs-gui/src/app/pages/activity_text.rs:240
msgid "Removed on Proton Drive"
msgstr "Verwijderd in Proton Drive"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:237
#: crates/pdfs-gui/src/app/pages/activity_text.rs:241
msgid "Removed on this computer"
msgstr "Verwijderd op deze computer"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:239
#: crates/pdfs-gui/src/app/pages/activity_text.rs:243
msgid "Replaced by a renamed file"
msgstr "Vervangen door een hernoemd bestand"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:243
#: crates/pdfs-gui/src/app/pages/activity_text.rs:247
msgid ""
"The destination folder no longer exists, so the file stayed where it was"
msgstr "De doelmap bestaat niet meer, dus het bestand is gebleven waar het was"
#. Translators: {name} is the folder an item was moved into.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:262
#: crates/pdfs-gui/src/app/pages/activity_text.rs:266
#, rust-format
msgid "To {name}"
msgstr "Naar {name}"
#. Translators: {name} is an item's previous name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:266
#: crates/pdfs-gui/src/app/pages/activity_text.rs:270
#, rust-format
msgid "Previously {name}"
msgstr "Eerder {name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:269
#: crates/pdfs-gui/src/app/pages/activity_text.rs:273
msgid "Restored an earlier version"
msgstr "Een eerdere versie teruggezet"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:272
#: crates/pdfs-gui/src/app/pages/activity_text.rs:276
msgid "Deleted an earlier version"
msgstr "Een eerdere versie verwijderd"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:279
#: crates/pdfs-gui/src/app/pages/activity_text.rs:283
#, rust-format
msgid "Saved an earlier version to {path}"
msgstr "Een eerdere versie opgeslagen in {path}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:286
#: crates/pdfs-gui/src/app/pages/activity_text.rs:290
#, rust-format
msgid "Kept both; the copy is now {name}"
msgstr "Beide behouden; de kopie heet nu {name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:296
#: crates/pdfs-gui/src/app/pages/activity_text.rs:300
#, rust-format
msgid "Kept {name}; the copy is in the Trash"
msgstr "{name} behouden; de kopie staat in de prullenbak"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:301
#: crates/pdfs-gui/src/app/pages/activity_text.rs:305
msgid "Kept the conflicting copy"
msgstr "Conflictkopie behouden"
#. Translators: {name} is the file that was kept.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:305
#: crates/pdfs-gui/src/app/pages/activity_text.rs:309
#, rust-format
msgid "Kept {name}"
msgstr "{name} behouden"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:310
#: crates/pdfs-gui/src/app/pages/activity_text.rs:314
#, rust-format
msgid "Same as {name}, so it was removed"
msgstr "Hetzelfde als {name}, dus verwijderd"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:317
#: crates/pdfs-gui/src/app/pages/activity_text.rs:321
#, rust-format
msgid "The name was taken, so it was moved as {name}"
msgstr "De naam was al in gebruik, dus verplaatst als {name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:324
#: crates/pdfs-gui/src/app/pages/activity_text.rs:328
#, rust-format
msgid "The name was taken on Proton Drive, so it was renamed to {name}"
msgstr "De naam was al in gebruik in Proton Drive, dus hernoemd naar {name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:331
#: crates/pdfs-gui/src/app/pages/activity_text.rs:335
#, rust-format
msgid "The name was taken on Proton Drive, so it was created as {name}"
msgstr "De naam was al in gebruik in Proton Drive, dus aangemaakt als {name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:340
#: crates/pdfs-gui/src/app/pages/activity_text.rs:344
#, rust-format
msgid ""
"Its folder was moved to the Trash on Proton Drive, so it was created at the "
@@ -1012,7 +1024,7 @@ msgstr ""
"De map ervan is in Proton Drive naar de prullenbak verplaatst, dus "
"aangemaakt op het hoogste niveau als {name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:350
#: crates/pdfs-gui/src/app/pages/activity_text.rs:354
#, rust-format
msgid ""
"It no longer exists on Proton Drive, so your changes were uploaded as {name}"
@@ -1020,7 +1032,7 @@ msgstr ""
"Het bestaat niet meer in Proton Drive, dus je wijzigingen zijn geüpload als "
"{name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:356
#: crates/pdfs-gui/src/app/pages/activity_text.rs:360
#, rust-format
msgid ""
"It was moved to the Trash on Proton Drive, so your changes were uploaded as "
@@ -1029,7 +1041,7 @@ msgstr ""
"Het is in Proton Drive naar de prullenbak verplaatst, dus je wijzigingen "
"zijn geüpload als {name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:362
#: crates/pdfs-gui/src/app/pages/activity_text.rs:366
#, rust-format
msgid "It was changed on Proton Drive, so your changes were uploaded as {name}"
msgstr ""
@@ -1037,12 +1049,12 @@ msgstr ""
#. Translators: {name} is the copy the changes made on this
#. computer were uploaded as.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:367
#: crates/pdfs-gui/src/app/pages/activity_text.rs:371
#, rust-format
msgid "Your changes were uploaded as {name}"
msgstr "Je wijzigingen zijn geüpload als {name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:377
#: crates/pdfs-gui/src/app/pages/activity_text.rs:381
#, rust-format
msgid ""
"It no longer exists on Proton Drive, so your changes were kept in {path}"
@@ -1050,7 +1062,7 @@ msgstr ""
"Het bestaat niet meer in Proton Drive, dus je wijzigingen zijn bewaard in "
"{path}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:383
#: crates/pdfs-gui/src/app/pages/activity_text.rs:387
#, rust-format
msgid ""
"It was moved to the Trash on Proton Drive, so your changes were kept in "
@@ -1059,7 +1071,7 @@ msgstr ""
"Het is in Proton Drive naar de prullenbak verplaatst, dus je wijzigingen "
"zijn bewaard in {path}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:389
#: crates/pdfs-gui/src/app/pages/activity_text.rs:393
#, rust-format
msgid "It was changed on Proton Drive, so your changes were kept in {path}"
msgstr ""
@@ -1067,59 +1079,59 @@ msgstr ""
#. Translators: {path} is the file on this computer that holds the
#. changes which could not be uploaded.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:394
#: crates/pdfs-gui/src/app/pages/activity_text.rs:398
#, rust-format
msgid "Your changes were kept in {path}"
msgstr "Je wijzigingen zijn bewaard in {path}"
#. Translators: {name} is the file the conflicting copy was made from.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:399
#: crates/pdfs-gui/src/app/pages/activity_text.rs:403
#: crates/pdfs-gui/src/app/pages/locations.rs:989
#, rust-format
msgid "Differs from {name}"
msgstr "Verschilt van {name}"
#. Translators: {name} is the file the conflicting copy was made from.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:406
#: crates/pdfs-gui/src/app/pages/activity_text.rs:410
#, rust-format
msgid "{name} no longer exists"
msgstr "{name} bestaat niet meer"
#. Translators: {name} is the file the conflicting copy was made from.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:413
#: crates/pdfs-gui/src/app/pages/activity_text.rs:417
#, rust-format
msgid "Same as {name}"
msgstr "Hetzelfde als {name}"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:420
#: crates/pdfs-gui/src/app/pages/activity_text.rs:424
#, rust-format
msgid "{n} person can view"
msgid_plural "{n} people can view"
msgstr[0] "{n} persoon kan bekijken"
msgstr[1] "{n} personen kunnen bekijken"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:421
#: crates/pdfs-gui/src/app/pages/activity_text.rs:425
#, rust-format
msgid "{n} person can edit"
msgid_plural "{n} people can edit"
msgstr[0] "{n} persoon kan bewerken"
msgstr[1] "{n} personen kunnen bewerken"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:422
#: crates/pdfs-gui/src/app/pages/activity_text.rs:426
#, rust-format
msgid "{n} person invited"
msgid_plural "{n} people invited"
msgstr[0] "{n} persoon uitgenodigd"
msgstr[1] "{n} personen uitgenodigd"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:427
#: crates/pdfs-gui/src/app/pages/activity_text.rs:431
#, rust-format
msgid "{n} file kept as a conflicting copy"
msgid_plural "{n} files kept as conflicting copies"
msgstr[0] "{n} bestand behouden als conflictkopie"
msgstr[1] "{n} bestanden behouden als conflictkopieën"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:435
#: crates/pdfs-gui/src/app/pages/activity_text.rs:439
#, rust-format
msgid "{n} item failed and will be tried again"
msgid_plural "{n} items failed and will be tried again"
@@ -1127,8 +1139,8 @@ msgstr[0] "{n} item mislukt en wordt opnieuw geprobeerd"
msgstr[1] "{n} items mislukt en worden opnieuw geprobeerd"
#. Translators: part of a list summing up a sync pass.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:473
#: crates/pdfs-gui/src/app/pages/activity_text.rs:504
#: crates/pdfs-gui/src/app/pages/activity_text.rs:477
#: crates/pdfs-gui/src/app/pages/activity_text.rs:508
#, rust-format
msgid "{n} uploaded"
msgid_plural "{n} uploaded"
@@ -1136,7 +1148,7 @@ msgstr[0] "{n} geüpload"
msgstr[1] "{n} geüpload"
#. Translators: part of a list summing up a sync pass.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:475
#: crates/pdfs-gui/src/app/pages/activity_text.rs:479
#, rust-format
msgid "{n} downloaded"
msgid_plural "{n} downloaded"
@@ -1144,7 +1156,7 @@ msgstr[0] "{n} gedownload"
msgstr[1] "{n} gedownload"
#. Translators: part of a list summing up a sync pass.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:477
#: crates/pdfs-gui/src/app/pages/activity_text.rs:481
#, rust-format
msgid "{n} folder created"
msgid_plural "{n} folders created"
@@ -1152,7 +1164,7 @@ msgstr[0] "{n} map aangemaakt"
msgstr[1] "{n} mappen aangemaakt"
#. Translators: part of a list summing up a sync pass.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:479
#: crates/pdfs-gui/src/app/pages/activity_text.rs:483
#, rust-format
msgid "{n} deleted"
msgid_plural "{n} deleted"
@@ -1160,7 +1172,7 @@ msgstr[0] "{n} verwijderd"
msgstr[1] "{n} verwijderd"
#. Translators: part of a list summing up a sync pass.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:481
#: crates/pdfs-gui/src/app/pages/activity_text.rs:485
#, rust-format
msgid "{n} in conflict"
msgid_plural "{n} in conflict"
@@ -1169,7 +1181,7 @@ msgstr[1] "{n} in conflict"
#. Translators: part of a list summing up a sync pass; the files are
#. still open in another app.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:484
#: crates/pdfs-gui/src/app/pages/activity_text.rs:488
#, rust-format
msgid "{n} still open"
msgid_plural "{n} still open"
@@ -1177,28 +1189,28 @@ msgstr[0] "{n} nog geopend"
msgstr[1] "{n} nog geopend"
#. Translators: part of a list summing up a sync pass.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:486
#: crates/pdfs-gui/src/app/pages/activity_text.rs:509
#: crates/pdfs-gui/src/app/pages/activity_text.rs:490
#: crates/pdfs-gui/src/app/pages/activity_text.rs:513
#, rust-format
msgid "{n} failed"
msgid_plural "{n} failed"
msgstr[0] "{n} mislukt"
msgstr[1] "{n} mislukt"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:498
#: crates/pdfs-gui/src/app/pages/activity_text.rs:502
msgid "Cancelled"
msgstr "Geannuleerd"
#. Translators: photos an import skipped because they were already
#. in Proton Drive.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:507
#: crates/pdfs-gui/src/app/pages/activity_text.rs:511
#, rust-format
msgid "{n} already there"
msgid_plural "{n} already there"
msgstr[0] "{n} al aanwezig"
msgstr[1] "{n} al aanwezig"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:508
#: crates/pdfs-gui/src/app/pages/activity_text.rs:512
#: crates/pdfs-gui/src/app/pages/albums.rs:107
#: crates/pdfs-gui/src/app/pages/albums.rs:694
#, rust-format