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: Spanish\n"
@@ -58,7 +58,7 @@ msgid "%A, %B %-d, %Y, %H:%M"
msgstr "%A, %-d de %B de %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,190 +821,202 @@ msgstr "Se movió {name}"
msgid "Couldn't move {name}"
msgstr "No se pudo mover {name}"
#. 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 "Se copió {name}"
#. 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 "No se pudo copiar {name}"
#. Translators: {name} is a folder name.
#: crates/pdfs-gui/src/app/pages/activity_text.rs:187
#, rust-format
msgid "Created folder {name}"
msgstr "Se creó la carpeta {name}"
#. 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 "No se pudo crear la carpeta {name}"
#. 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 "Se movió {name} a la papelera"
#. 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 "No se pudo mover {name} a la papelera"
#. 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 "Se restauró {name}"
#. 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 "No se pudo restaurar {name}"
#. 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 "Se eliminó {name} para siempre"
#. 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 "No se pudo eliminar {name}"
#: 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 "Se vació la papelera"
#: 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 "No se pudo vaciar la papelera"
#. 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 "Se compartió {name}"
#. 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 "No se pudo compartir {name}"
#. 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 "Se creó un enlace a {name}"
#. 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 "No se pudo crear un enlace a {name}"
#. 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 "Se dejó de compartir {name}"
#. 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 "No se pudo dejar de compartir {name}"
#. 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 "Copia en conflicto {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 "Se quitó en 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 "Se quitó en este equipo"
#: 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 "Sustituido por un archivo renombrado"
#: 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 ""
"La carpeta de destino ya no existe, así que el archivo se quedó donde estaba"
#. 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 "A {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 "Antes {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 "Se restauró una versión anterior"
#: 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 "Se eliminó una versión anterior"
#: 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 "Se guardó una versión anterior en {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 "Se conservaron ambos; la copia ahora es {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 "Se conservó {name}; la copia está en la papelera"
#: 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 "Se conservó la copia en conflicto"
#. 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 "Se conservó {name}"
#: 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 "Igual que {name}, así que se quitó"
#: 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 "El nombre ya existía, así que se movió como {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 "El nombre ya existía en Proton Drive, así que se renombró como {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 "El nombre ya existía en Proton Drive, así que se creó como {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 "
@@ -1013,14 +1025,14 @@ msgstr ""
"Su carpeta se movió a la papelera en Proton Drive, así que se creó en el "
"nivel superior como {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}"
msgstr ""
"Ya no existe en Proton Drive, así que sus cambios se subieron como {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 ""
"Se movió a la papelera en Proton Drive, así que sus cambios se subieron como "
"{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,19 +1049,19 @@ 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 "Sus cambios se subieron como {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}"
msgstr ""
"Ya no existe en Proton Drive, así que sus cambios se guardaron en {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 "
@@ -1058,7 +1070,7 @@ msgstr ""
"Se movió a la papelera en Proton Drive, así que sus cambios se guardaron en "
"{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 ""
@@ -1066,59 +1078,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 "Sus cambios se guardaron en {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 "Difiere de {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} ya no existe"
#. 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 "Igual que {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} persona puede ver"
msgstr[1] "{n} personas pueden ver"
#: 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} persona puede editar"
msgstr[1] "{n} personas pueden editar"
#: 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} persona invitada"
msgstr[1] "{n} personas invitadas"
#: 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} archivo se conservó como copia en conflicto"
msgstr[1] "{n} archivos se conservaron como copias en conflicto"
#: 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"
@@ -1126,8 +1138,8 @@ msgstr[0] "{n} elemento falló y se volverá a intentar"
msgstr[1] "{n} elementos fallaron y se volverán a intentar"
#. 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"
@@ -1135,7 +1147,7 @@ msgstr[0] "{n} subido"
msgstr[1] "{n} subidos"
#. 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"
@@ -1143,7 +1155,7 @@ msgstr[0] "{n} descargado"
msgstr[1] "{n} descargados"
#. 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"
@@ -1151,7 +1163,7 @@ msgstr[0] "{n} carpeta creada"
msgstr[1] "{n} carpetas creadas"
#. 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"
@@ -1159,7 +1171,7 @@ msgstr[0] "{n} eliminado"
msgstr[1] "{n} eliminados"
#. 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"
@@ -1168,7 +1180,7 @@ msgstr[1] "{n} en conflicto"
#. 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"
@@ -1176,28 +1188,28 @@ msgstr[0] "{n} aún abierto"
msgstr[1] "{n} aún abiertos"
#. 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} fallido"
msgstr[1] "{n} fallidos"
#: crates/pdfs-gui/src/app/pages/activity_text.rs:498
#: crates/pdfs-gui/src/app/pages/activity_text.rs:502
msgid "Cancelled"
msgstr "Cancelado"
#. 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} ya existía"
msgstr[1] "{n} ya existían"
#: 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