USB and portable-disk backup
USB is a first-class backend, not a power-user toggle. The engine provider is usb. Desktop and iOS default to path mode. Android has to use bridge because OTG only hands you a content:// URI; POSIX open() does not work.
What iPhone actually does
iOS will not list mounted volumes (FileManager.mountedVolumeURLs is empty off macOS). The app never shows “SanDisk detected”. The path is:
- Plug the disk in until it appears in Files.
- Azalea → backup storage → new USB profile.
- Pick a folder. The app stores a security-scoped bookmark, not a raw path string.
- Every engine call later runs inside
withAccess. Using the bookmark outside that scope turns into an opaque Rust permission error.
On 21 Feb 2026, iPhone 15 + SanDisk Extreme 1 TB: after the first pick, locking the phone made the next launch report the volume missing until a manual refresh. That is not copy fluff — iOS has no attach notification we can subscribe to.
Why Android is SAF
OpenDocumentTree, then takePersistableUriPermission. Skip the persist call and the grant dies at reboot. SafFileBridge does not walk DocumentFile.findFile (O(n) per level; tens of thousands of photos stall). It prefers constructing the document id.
Yank the cable and you get DeviceDetached, not a generic I/O fail. Disk full is OutOfSpace. The UI should pause, not paint one item red and keep scanning.
What lands on the disk
<root>/.album/manifest.jsonl is append-only. Version header on line one, then logical key, physical path, bytes, MD5, tombstones. A second phone claims the same folder by reading that ledger and reversing escaped names. .album/ never shows up in list.
exFAT has no xattr. Storing MD5 the way the cloud backends do would throw ENOTSUP on every USB upload. Checksums live in the manifest instead.
Things that will not happen
- No
presign_put. - Do not format APFS and expect Android to write it.
- Two disks both named Backup are distinguished by
volumeIdentity()UUID, not the folder label.