Merge branch 'claude/vault-creation-sharing-62c0b6'
ci / build and test (push) Successful in 1m38s
ci / android head (push) Failing after 9s
ci / api image (push) Successful in 28s

# Conflicts:
#	README.md
This commit is contained in:
2026-08-04 10:10:40 +02:00
13 changed files with 1035 additions and 63 deletions
+119
View File
@@ -1327,3 +1327,122 @@ rather than two. There is no confirmation prompt, deliberately.
**Failure means:** a phone that still unlocks itself after this is the local half not happening, which is the
half that matters when the handset is the thing that was lost.
---
## Phase 14 — Moving files to and from the phone's remote
Every check here needs a real Android device or emulator, a host with SFTP or a bucket, and at least one
document on the phone to send. What is automated is what can be: `TransferQueueingTests` says a staged
upload obeys the same rules as any other and that a delivered download refuses a directory before the
picker's damage is done. What cannot be automated is everything below — the two system pickers are another
application, and the staging copies, the delivery, the notification and the resume are all things only a
running phone does.
### 14.1 ADD FILES opens the system picker, and takes more than one
Connect to a host on SFTP, navigate somewhere writable, press **ADD FILES**, and choose two documents in one
go — long-press to multi-select in Android's picker.
**Pass:** two rows appear in the queue with the names the picker showed, and both land in the directory the
breadcrumb names. The pane's listing shows them after **↻**.
**Failure means:** one row from a two-document pick is `PickAsync` losing the rest, and two rows with one
name is the per-file staging directory having gone — that is the overwrite `DocumentStaging` documents, and
it silently uploads the same bytes twice.
### 14.2 The name that arrives is the name that was picked · **the one most likely to be wrong**
Pick a document whose display name has a space and a non-Latin character in it, and one from a cloud
provider — Drive, or the Downloads shortcut — rather than local storage.
**Pass:** the file on the host is called what the picker called it. A cloud document uploads too, or fails
with the provider's own message in the status line rather than a crash.
**Failure means:** a mangled name is `SafeName` over-reaching. A name that reaches the host with a `/` in it
is `SafeName` under-reaching, and that one writes to a path nobody chose. A cloud document that hangs is
the copy being made on the interface thread — the whole reason `CopyInAsync` leaves it.
### 14.3 The queue is bounded, and the buttons stay reachable
Queue five or six files at once, on a small phone if there is one.
**Pass:** the queue scrolls inside its own region and **ADD FILES**, **DELETE** and **CLOSE** are all still
on screen. Every button is a thumb's size.
**Failure means:** buttons pushed off the bottom is the `MaxHeight` gone from the queue's scroller, and it
makes the screen unusable exactly when somebody has queued the most work.
### 14.4 A stopped upload resumes rather than starting again
Start a large upload, press **STOP** part way, then press **RESUME**.
**Pass:** it carries on from roughly where it stopped rather than from zero — the progress text is the thing
to read.
**Failure means:** restarting from zero means the staged copy was deleted at the stop, which is precisely
what `QueueStagedUploads` does not do and why it does not. A failure saying the file cannot be found is the
same bug, one step further along.
### 14.5 The copies do not accumulate · **the one nothing else would catch**
Note the app's storage in Android Settings → Apps → DodoSSH → Storage. Upload a large file, let it finish,
and look again. Then stop an upload part way, leave it stopped, force-stop the app and relaunch it.
**Pass:** storage returns to about what it was after the successful upload — the copy is deleted the moment
the transfer completes. After the stopped one, the cache is bigger while the app stays open (the copy is
being kept for RESUME) and back to its old size after the relaunch, which is `DocumentStaging.Sweep`.
**Failure means:** growth after a successful upload is `ReleaseStaged` not firing, and every file sent
leaves a second copy on the phone until Android reclaims the cache. Growth that survives a relaunch is the
sweep not running.
### 14.6 The notification is up while it transfers, and gone afterwards
Queue several files in each direction, put the phone to sleep with the screen off, and wait.
**Pass:** the foreground notification is up, the transfers finish while the screen is off, and the
notification goes away when the last one does — with no shell open. With a shell open it stays, because that
is what it was already for.
**Failure means:** an upload that stalls with the screen off is the count not reaching
`SessionForegroundService`, and Android has stopped the process mid-transfer. A notification left up
afterwards is `ActivityChanged` not being subscribed — the other end of the same wire.
### 14.7 SAVE FILE writes where you pointed it, and the file opens
Select a file on the host — something with a viewer, an image or a PDF — press **SAVE FILE**, and put it
somewhere reachable: Downloads, or a folder in Drive. When the transfer finishes, open it from the phone's
own Files app.
**Pass:** the status line says it was saved, the file is where the picker was pointed under the name shown
there, and it opens with the right contents. The queue row says DONE.
**Failure means:** a row that says DONE with nothing at the destination is `DeliverAsync` never running —
the delivery is registered per transfer id, and losing it makes the download look like a success while the
bytes sit in a cache nobody can reach. A file that is there but empty or truncated is the copy out, not the
transfer: check the `SetLength(0)` and that the write stream is being disposed before the status is written.
### 14.8 The button is dead until a file is chosen, and refuses a directory
With nothing selected, look at **SAVE FILE**. Then select a directory row.
**Pass:** disabled in both cases — it needs a connected remote and a selected *file*, which is the desktop's
own `CanDownload`.
**Failure means:** an enabled button over a directory reaches `QueueDeliveredDownload`'s refusal, which is
the right answer arriving too late: the save picker has already created an empty file, so the person is left
with a file they did not want and a message saying nothing happened.
### 14.9 A download that fails leaves the empty file it warned about
Point SAVE FILE at a destination for a large file, then break the transfer — turn off Wi-Fi and mobile data
while it runs.
**Pass:** the row goes to FAILED with the reason, the status line does not claim it was saved, and there is
an empty file at the destination. Reconnect, press **RETRY**, and the same destination fills in — the
delivery survives the failure because it is held against the transfer rather than the attempt.
**Failure means:** a retry that succeeds but leaves the destination empty is the delivery having been
dropped on the failure. An error saying the staged file is missing is the copy having been deleted at the
stop, which is what `QueueDeliveredDownload` documents it does not do.