The drain retired an upload's op first, and only then read the node back
and linked the staged blob into the content cache. For that one round
trip, the status reported an empty queue for a file that wasn't cached
yet, so anything that waited for an empty queue before reading the file
could still need the network. The account run of 2026-10-03 hit it over
a slow link: "regression B119" failed in the same second the upload
landed.
`Core::landing_uploads` now counts an upload from just before its op is
retired until its bytes are cached. The status adds it to
`pending_uploads`, and reads the queue before the count, so it always
sees the upload in one of them. A drop guard uncounts it on every exit
path.
The op still goes first. An op restored after a crash would re-send a
revision this daemon hasn't recorded as its own yet, and
`revision_conflict` would keep it as a conflict copy of the user's file.
The SDK caches each node, including the name hash it sends with the next
rename or move, and only an event evicts it. Renames and moves didn't
update that copy. So after a rename, the next change to the same node
went out with the old name hash, Drive refused it as out of date (422),
and the retry kept sending the stale hash until the event feed came
round, up to 10 seconds later.
- Every remote rename and move goes through `Core::rename_remote` or
`Core::move_remote`. Both drop the SDK's copy of the node after a
successful change and before each out-of-date retry, so the retry
sends the current hash.
- A plain move was not retried at all, so a move right after a rename
failed with EIO through the mount and with "out of date" from
`pdfs move`. It now retries like a rename.
- The drain's combined move-and-rename also drops the SDK's copy once
it lands.
- A rename that also changes folders through the mount replies only
once both halves have landed. When the move fails, the file keeps
its new name in the source folder, the tree is told so, and the
rename reports EIO. A dropped link still queues the change.
B111's cause in docs/BUGS.md is corrected to the same SDK cache. The
acceptance case "regression B121" renames and moves a file at once
through the mount and through `pdfs`, and checks names and bytes.
New live cases:
- regression B48, B80, B94, B95, B98, B102 and B111 replay the bugs
their entries describe. B48 and B111 also run against the reference.
- regression B114 to B117 replace the open B114 and B115 cases, and
fail with `check` when the bug comes back.
- regression B119: files written through the mount stay cached once
they are uploaded.
- regression B86: after a mirror folder switches to on-demand, the
mount lists what the mirror uploaded just before.
`--journal-check` also counts three warnings as errors: a conflict
copy (B113), a FUSE job held for a long time (B110) and an authority
re-read because the local tree lacked it (B117, B120).
A timeout no longer ends a target when the mount still answers a stat
and a listing. Cases that make many round trips get twice the limit.
A write is queued when the kernel releases the file, after close(2)
has returned, so cases now wait for the ops they expect
(`Daemon.queued`). "regression B102" saw one revision of two because
it read the queue once.
On flaky Wi-Fi, the run crashed twice on a read that the daemon
correctly failed with EIO once its read budget ran out. The end-of-run
read-back and the mode matrix's file checks now record a result
instead. The mode matrix tries such a read once more after the daemon
is back online, and notes it.
- B114: `pdfs rm`, `pdfs rename` and `pdfs move` tell each kernel
session that the old name is gone, so it stops resolving at once
instead of for up to 30 seconds (`Core::forget_everywhere`,
`Core::relocate_everywhere`).
- B115: `pdfs ls` and the app's file browser wait up to 10 seconds for
the listing's size fetch, as `stat` does, so they show a file's real
size instead of its encrypted one.
- B116: trashing a folder also drops the creates queued in its
subfolders. A parked one stayed queued with its bytes, and a due one
would have landed in the root.
- B117: `pdfs move` and `pdfs rename` move the node in each mount
instead of forgetting it, so its queued upload keeps its path in
`pdfs sync queue` and is not put off once after the resume.
- B118: when a create or mkdir reads back nothing, the mount asks again
and then queues it as it does offline, instead of failing with ENOENT
for something Drive has made.
- B119: a landed upload keeps its staged bytes as the file's cached
content, unless it is larger than a sixteenth of the cache, so
reading a file just written does not need the network.
- B120: a landed folder's row passes to its real uid in the same
transaction, so a file queued inside it is not put off for five
seconds.
docs/BUGS.md records B117 to B120. The account run of 2026-10-03
verified B114 to B117 and B119 in My files.
New live cases:
- regression B113: files renamed and moved while sync is paused upload
without a conflict copy.
- regression B88: pdfs mkdir, rename and rm by local path, the
mount-root guard, a path outside every mount, and the refusal in a
mirrored folder.
- pdfs output into a closed pipe exits 0 and prints nothing on stderr.
- open B114 and open B115 check two bugs that are still open. While
one reproduces, its case ends as "known", which is listed but does
not fail the run.
The console output names the pdfs version, the storage and the daemon
state of each target, and prints one aligned line per case, with its
details beneath. A failed case points to its docs/BUGS.md entry. The
run ends with a summary table, the slowest cases, the known issues
and the numbered failures. A queue wait now fails at once on a parked
op instead of waiting out its timeout.
docs/BUGS.md records B114, B115 and B116, which the first live run
found, and marks B113 and B88 as verified live.
`pdfs ls | head` ended in a "Broken pipe" panic once `head` had its
lines, because std's `println!` panics on a failed write.
The CLI now shadows `println!` and `print!` with versions that exit 0 on
a closed pipe and report any other write error, such as a full disk,
with exit 1. Restoring the default SIGPIPE would also stop the panic,
but the same binary runs the daemon, which must not die when a socket's
peer goes away.
`pdfs rm ~/Downloads/x` answered "is not under the mountpoint" while
`pdfs move` took the same path: the Delete, Rename and CreateFolder
handlers still read paths against the primary mount only (B88).
They now go through `route_change`, the router ResolveConflict already
used, renamed from `route_conflict`. It answers the request in the
inode space of the mount that owns the path.
An on-demand folder's root has a parent on Drive, so `pdfs rm
~/Downloads` would have trashed the whole synced folder from under its
mount. rm and rename refuse a path that names a mount's root.
The write handle a create opens was given no revision id, so the drain's
conflict check fell back to (mtime, size). Drive advances a node's mtime
on a move or rename, so a new file moved before its first upload drained
landed as a (sync-conflict) copy next to its own empty original. Take the
minted node's revision id, and rebase handles still open on a queued
create onto the node it landed as (B113).
makepkg reuses a cached source file of the same name, so the unversioned
proton-drive.service from an older build failed the 2.8.2 checksum. Carry
$pkgver in every file fetched from the tagged tree, like LICENSE already
did, and bump pkgrel to 2.
0.7.2 resolves each shared root with its own key. With 0.7.1, when
several accepted shares came from the same owner's volume, only the first
appeared under Shared with me and the rest were skipped with "missing
key" (B112, proton-sdk-rs#2).
default.target is reached before the desktop exports DISPLAY/WAYLAND_DISPLAY
to the user manager, so After=graphical-session.target never applied. The
daemon's first keyring call D-Bus-activated the Secret Service without a
display; on KDE ksecretd aborted, the first start failed, and every login
left a coredump.
Install into graphical-session.target instead, and use reenable when the
front-ends enable the unit so existing default.target.wants links are
replaced rather than kept alongside the new one.
Fixes#23
73e60f1 pointed url at proton-drive.narl.io, and all three AUR PKGBUILDs
built their source URLs from $url, so the release tarball, the binary
archive and the git clone were fetched from the homepage and 404ed. The
sources now come from a separate _repo on GitHub; url stays the homepage.
"pending upload landed" now carries upload_ms, the time on the wire, and
total_ms, the whole op including the conflict check and gap-fill. A slow
drain then shows where its time goes without a profiler.
The mount only ever went from offline to online: nothing marked it
offline again, so after the Wi-Fi dropped every create, mkdir, delete and
rename called Drive, waited out the SDK's retries for up to two minutes
and answered EIO. Background loops kept polling the dead link.
Connectivity now lives in pdfs-fuse's new link module and goes both
ways. A call that gets no answer (a transport error or a request
timeout) marks the mount offline; a probe thread checks every 2 s,
backing off to 30 s, and on recovery re-lists folders served from the
database, retries failed operations and wakes the drain. Interactive
calls get a 20 s deadline, so a dead link costs one slow call instead of
many.
While offline:
- create, mkdir, unlink, rmdir and rename fall back to the offline
queue instead of answering EIO;
- folders list from the database even when not marked listed, and are
re-listed once the link returns;
- reads of uncached data wait up to 30 s for the link, then retry once
within the 120 s read deadline;
- statfs serves the last quota instead of fetching, and quota refreshes
are single-flight;
- the event and photo event loops wait for the link instead of polling.
A create whose request timed out may have landed anyway. When the drain
replays it and finds a same-named twin of the same kind, created within
the last two minutes and still empty, it adopts that node instead of
making a conflict copy. The drain also leaves alone a name still held
by a queued trash.
0.7.1 sends TCP keepalives on its connections, so a link that dies
silently is noticed instead of leaving a request waiting on a dead
socket until its timeout.
A create that answered ENOENT left no trace in the log. It now warns
when the parent folder is no longer known, and when the parent left the
tree while its children were being listed, naming the parent inode and
the new name.
The acceptance script's metadata throughput floor was 20 operations per
second, which a FUSE mount cannot reach: each create and unlink waits for
Drive before it returns, about 350 ms apiece. On a FUSE mount the floor
is now 3 operations per second; other paths keep 20. The mount type is
read from /proc/self/mountinfo.
A read that settles a provisional size invalidated the file's page cache
while the kernel held that read's page lock, so the worker waited on
itself forever (B110). Size upgrades now invalidate attributes only.
Renaming a node right after renaming it could be refused by Drive as out
of date, because the name hash read just before still held the old name.
Every remote rename now goes through Core::rename_remote, which retries
InvalidRequirements for up to ten seconds (B111).
B109: an online create seals an empty file on Drive, and until the last
close the written bytes live only in the open handle. A re-listing of
the parent, a size upgrade or a create landing re-interned the node with
Drive's empty size, the kernel capped reads at that size, and SQLite
reported "database disk image is malformed". State::keep_open_write_size
now carries the handle's size and local mtime over in all three places.
The acceptance script waits for My files to mount after a daemon restart
instead of failing at once.
Fixes from the account acceptance runs:
- B104: a reader open across a rename-over keeps working; the replaced
node is unlinked like any other open file instead of forgotten.
- B105: a drain worker holding a revision withdrawn by an unlink no
longer uploads it as a recovered-… conflict copy (Db::op_exists).
- B106: an open victim's queued ops are withdrawn at trash time, so a
repeated editor save no longer fails with AlreadyExists; the staged
blob goes with the last close.
- B107: a remote read gives up after 120 s with EIO instead of hanging
its caller; the timer is built inside the runtime.
- B108: a mirror upload whose local file was deleted after the scan is
skipped rather than counted as an error.
The acceptance script reports the failing line for non-assert errors,
ignores queue ops that predate the run, purges with delete-forever
first, and diagnoses a malformed SQLite database against a local replay.
The FUSE acceptance suite now runs on the signed-in account by default and
cleans up after itself; its first live runs found three daemon bugs.
- B101: a pwrite past EOF now authors the hole it leaves, so truncated bytes
no longer come back from the base. A write attached to a create while that
create uploads becomes a revision of the real uid instead of being dropped.
- B102: removing a folder drops the queued revisions of files below it, found
through the node tree, with their blobs and in-flight uploads. `pdfs rm`
now drops queued ops too.
- B103: each server call of a trash refresh times out after 120 s, so one hung
call can no longer freeze the trash listing until restart.
Acceptance suite: account mode with a manifest, lock and reaper; cleanup by
uid; queued ops naming a test root count as leftovers; new edge-case,
throughput and B100/B101 cases; the wide-directory case is split from the
deep tree and gets a doubled budget. Adds `pdfs trash --json`.
A file listed by the cheap enumeration reports its ciphertext size until
the size upgrade lands. Since B84 a read on that size fails with EIO, and
a write open started its handle at it. Reads and write opens now fetch
the real size first. A folder past the size-upgrade cap is queued instead
of dropped, so a large copy no longer leaves most folders provisional
(B100).
The About dialog's website link, the README, and the package metadata
(Cargo, PKGBUILDs, AUR .SRCINFO, .deb, .rpm) now use the project
homepage. Source code, issue, and release links stay on GitHub.
Every managed mode pair now also moves trees between the two folders in both
directions, moves several sources in one command, and passes a tree through
My files and out again. Each case waits for Drive before and after, then
checks the tree is only at the destination, byte for byte, with no conflict
copies. Refusal cases cover a taken name, a folder into itself, a mirror
source holding a symlink, and an on-demand file still open for writing.
- A mirror download first copies the file out of the content cache when the
same revision is cached whole or block by block, so moving a file out of an
on-demand folder into a mirror costs no traffic for what was already read.
- `pdfs move a b c DEST` moves every source, reports each failure and exits
non-zero if any failed.
- Mirror-to-mirror moves rename with RENAME_NOREPLACE, so a name that
appeared at the destination after the check is never overwritten.
- The local copy a move out of a mirror drops is checked again just before
it is deleted; anything written into it meanwhile keeps the copy and is
uploaded as new by the next pass.
A move request with two absolute paths is resolved against every
location (My files, on-demand and mirror synced folders) and made as a
server-side move_node. The local side is levelled afterwards: mounts
forget and re-list, a mirror folder renames its copy and baseline along
or drops the copy once Drive holds it.
Anything only this disk holds is refused up front: queued ops, open
writes, unsynced or ignored mirror files and symlinks. The GUI Move
dialog gains a top-level list of locations.
The transfer label did not ellipsize, so a long file name set the
column's minimum width and pushed it past the clamp. The page width
changed with every file.
The label now ellipsizes in the middle, and the Sync page drops the
clamp so Overview and Folders fill the space next to the sidebar.
Every visit to the Photos page loaded the timeline, and a finished
timeline load always shows the timeline. Leaving Albums or Places and
coming back therefore showed the timeline under the Albums or Places
toggle.
A visit now reloads the view the toggle selects. An open album or place
still loads its photos as before.
--preload registers the resident GTK launcher and realizes its window
without showing it, so the first shortcut opens it as fast as later
ones. When a launcher is already running it exits without activating
it.
The application hold now happens in startup, which only the primary
instance runs. Taken before registration, it also kept every remote
invocation alive after it had forwarded its activation.
The release job never hit a cache: its rust-cache key contained the job
name, tags cannot restore caches of earlier tags, and rpmbuild rebuilt
the whole workspace under its own compiler flags.
CI on main now saves shared debug and release caches that the release
workflow restores. Quality gates and the package build run in parallel,
and the GitHub release is created once both pass. The RPM spec skips
its cargo build when the caller passes --define "prebuilt 1".
A wrapping label asks for its whole text on one line as its natural
width, so a long location in the preview pane widened it, squeezed the
results list and stretched the window. Cap the preview labels' natural
width, limit detail values to three lines with a tooltip for the full
location, and keep the pane from taking extra width.
The launcher (pdfs-prompt) now follows the app's theme: the theme code
moved into a shared theme.rs, the prompt's stylesheet lives in
resources/prompt.css on the libadwaita palette, the Proton-purple accent
pin is gone, and every activation re-reads the Proton theme and open_with
settings so changes in the app apply without restarting the launcher.
The window is rebuilt in src/hud/:
- one list with Recent, Pinned and Best matches sections; a click
selects, a double-click or Enter opens, focus stays in the entry
- rows show a thumbnail, the name with its matched characters in bold,
location, sync badge, size and age
- a preview pane with a large thumbnail, type, size, modified date,
location and availability, plus Open (Enter), Show in Folder
(Ctrl+Enter, via FileManager1) and Copy Path (Ctrl+C / Ctrl+Shift+C)
- thumbnails from the freedesktop cache, local decoding (written back
to the shared cache) or the daemon, on two worker threads
- a history of opened files that fills the Recent section and adds a
frecency bonus to search ranking
- the mount check before opening a Drive file runs off the main thread
with a 2 s timeout, and Escape cancels a slow open
- rows can be dragged out as files
- the search debounce drops to 80 ms
New strings are translated in every catalog.
Replace the Photos screenshot with one of placeholder stock photos, add
Sync, conflict history and Computers screenshots, and drop the retired
Activity, Settings, Shared and restore-devices images from the README.
Split the README into a short front page and task-oriented pages under
docs/: installation, user guide, CLI reference, configuration and
troubleshooting, with an index in docs/README.md. Rewrite RECOVERY.md
around Continue This Backup Here and Restore Folders, and DEVELOPMENT.md
around running, debugging and releasing from source.
Bring ARCHITECTURE.md up to date with the read path, push events, the
thread map, conflict copies and the machine profile, and move the feature
design notes there. Collect planned work in ROADMAP.md. Link the threat
model from SECURITY.md and the translation rules from CONTRIBUTING.md.
Add scripts/publish-wiki.sh, which mirrors the user pages to the GitHub
wiki with rewritten links and only pushes with --push.