Commit Graph
100 Commits
Author SHA1 Message Date
nvrl 48aaad23ce Merge pull request #55 from narrrl/fix/journald-priority
fix(cli): give journald each log line's priority
2026-10-31 19:09:37 +01:00
nvrl 8d4a005aa2 fix(cli): give journald each log line's priority
journald stored every daemon line at info, so journalctl -p warning,
which TROUBLESHOOTING.md asks reporters for, was always empty (B258).
When fd 1 is the journal stream, prefix lines with <N> and drop colours.
2026-10-31 09:54:07 +01:00
nvrl 9460c93b50 docs(bugs): record the audit of issues #53, #46, #40 and #38
B258 to B273: why a stuck sync, a failing mount and a silent restore
are invisible, plus the sync, mount and restore bugs behind them.
B165, B202, B205 and B210 point at the entries that finish them.
2026-10-31 09:47:54 +01:00
nvrl 27201914ee Merge pull request #51 from narrrl/fix/emptied-folder-direct-switch
fix(fuse): let an emptied mirror folder switch to on-demand at once
2026-10-27 12:05:50 +01:00
nvrl 2775f0681d test(acceptance): count a purge of a listed trash entry that then vanishes
The reaper found an abandoned root in the trash and sent the delete. The
reply did not confirm it, and the next listing no longer had the node,
but the retry loop only counted deletes it sent itself. It waited for a
node that was already gone, reported it as never trashed, and the
leftover check stopped the run before any case ran.

Callers that just found the node in the trash now say so, and the error
of the first delete is kept for the warning.
2026-10-27 01:22:07 +01:00
nvrl 97277d41dd fix(fuse): let an emptied mirror folder switch to on-demand at once
A switch asked for before any pass saw the folder empty found it still
idle, so the check for what only this computer has ran at once and
refused it for good: every synced path read as "deleted here but not on
Proton Drive yet". The account run hit it in the B255 case.

A local copy gone in full is the wipe guard's case, which the push pass
of a queued switch settles without trashing anything on Drive. The
switch now returns NotNow for it, so the request is queued and that pass
runs first. A partial deletion is still refused as before.
2026-10-27 01:22:07 +01:00
nvrl d49738bf0b Merge pull request #50 from narrrl/test/size-batch-clock-before-spawn
test(fuse): start the size batch clock before the worker
2026-10-27 01:03:49 +01:00
nvrl 48945b9c97 Merge pull request #49 from narrrl/test/b256-case-own-folder
test(acceptance): give the B256 case a copy folder of its own
2026-10-27 01:03:40 +01:00
nvrl b860ac3b49 test(fuse): start the size batch clock before the worker
The worker thread's sleeps could begin before the test read the clock, so the elapsed time came out a few milliseconds under the 90 ms bound on a busy runner. Reading the clock first makes the bound hold.
2026-10-27 01:02:29 +01:00
nvrl 42f3a18177 test(acceptance): give the B256 case a copy folder of its own
Every case of a run shares one root, and the pdfs copy case before it
had already made "copies" there, so the B256 case failed on its mkdir
with EEXIST before it checked anything.
2026-10-27 00:56:35 +01:00
nvrl 6f504d2e83 Merge pull request #48 from narrrl/test/acceptance-new-features
3.6.0: acceptance cases for the new features, --json for members/du/public-link
2026-10-27 00:44:30 +01:00
nvrl 8c1c37a928 test(acceptance): cover copy, folder size, link updates and sync recovery
New cases for what 3.6.0 adds:

- copy_file_range over several calls, into the middle of a file, past
  its end, at the source's end, and from a file whose writes are not
  released yet; on a live mount also read back from Drive.
- pdfs copy of a tree and of a file beside itself, read back as listed
  and after a refresh, with nothing queued; refused while a file has
  writes Drive lacks, and never the version before a save (B256).
- pdfs du counts every item below a folder, trash included.
- pdfs public-link update keeps the URL and the options it was not
  asked to change, and refuses a link id that is not the folder's.
- In a sync folder of their own: Sync From Scratch changes nothing that
  already matched, and an emptied mirror folder still switches to
  on-demand with every file intact on Drive (B255).

docs/TESTING.md gains a checklist for the new screens in the app. The
CHANGELOG moves these into 3.6.0, which is not tagged yet.
2026-10-27 00:42:44 +01:00
nvrl e4d2340bbf feat(cli): --json for members, du and public-link create/update
Scripts, the acceptance suite among them, can now read a link's id,
role, password flag and expiry, and a folder's size and item count,
instead of parsing the human-readable lines.
2026-10-27 00:42:44 +01:00
nvrl c1b72e455e test(fuse): time the short-outage reconnect from the mount coming back
The probe that sees the link return runs 2 s after the drop, then 6 s.
A loaded run could turn the link back on after the first probe, and the
next one came after the test's 3 s were up. The test now checks that the
drain takes the mount offline within 2 s, which is the B189 fix itself,
and counts the 3 s from the mount reporting it is back online.
2026-10-27 00:32:49 +01:00
nvrl 2e3b7748e6 Merge pull request #47 from narrrl/feat/server-copy
Release 3.6.0: server copy, link settings, folder size, sync fixes (#46)
2026-10-26 22:47:47 +01:00
nvrl 7f4f11ef18 chore(release): 3.6.0 2026-10-26 22:45:36 +01:00
nvrl cc9f7c6d8a Merge remote-tracking branch 'origin/main' into feat/server-copy
# Conflicts:
#	Cargo.lock
#	Cargo.toml
2026-10-26 22:41:40 +01:00
nvrl 9add185c1f Merge pull request #43 from narrrl/feat/diagnose-keys
feat(cli,core): trace the keys that open My Files in pdfs diagnose
2026-10-26 22:35:35 +01:00
nvrl 0e7ffe1261 fix(gui,cli): count what a folder's first scan has checked (#46)
The first pass over a synced folder has no baseline to estimate from, so
its scan showed only "checking for changes…" in the app and "scanning…"
in `pdfs sync list`. For a large folder that scan reads every item on
Proton Drive before anything downloads, which takes many minutes and
looked stuck. Both now show how many items the scan has checked so far.
2026-10-26 22:28:46 +01:00
nvrl a5acae41b3 fix(sync,gui,cli): get a synced folder unstuck when its local copy is gone (#46)
A mirror folder whose files were all gone from this computer stayed in
error for good. The wipe guard rightly refuses to trash the whole folder
on Drive, but nothing got the folder out of that state: a queued switch
to on-demand waits for a pass that ends idle, and its push pass hit the
same guard. Only removing the folder from sync cleared the baseline that
made the empty folder read as "delete everything".

- The push pass for a switch to on-demand forgets the baseline rows of a
  local copy that is gone in full instead of trashing them, so the
  switch goes ahead and the mount shows what Drive has.
- New Request::ResetSyncFolder, `pdfs sync reset ID` and "Sync From
  Scratch…" in a synced folder's menu clear a mirror folder's baseline
  (and a queued switch to on-demand, whose push pass would upload a
  duplicate of every file). The next pass compares both sides as on the
  first sync: one-sided files are copied across, files that differ keep
  both versions, nothing is deleted.
- The wipe guards' messages name the reset; restore logs why it skipped
  a folder.

Two simulations cover it. The on-demand one failed in exactly the
reported state (mirror, pending ondemand, error) before the fix.
All 8 catalogs translated.
2026-10-26 17:45:22 +01:00
nvrl ea10a43216 fix(fuse): a copy right after a save waits for the edit (B256)
close(2) returns before the kernel sends release, and release takes the
write handle out of the open writes a few milliseconds before the
revision is in the queue. A `pdfs copy` checked in that gap found the
edit nowhere and copied the version Drive had. A folder copy looked only
at the queue, so it also missed files below it still open with unsaved
writes.

release now keeps the inode in State::releasing until the revision is
queued. A copy refuses a file or folder with unsaved writes, or a
release still queueing, at it or below it, and a move checks the same
set.
2026-10-26 17:45:14 +01:00
nvrl 917e7268f9 build(deps): proton-sdk and proton-drive-rs 0.10.0
0.10.0 adds copy_node, get_folder_size, update_public_link and a link's custom password.
2026-10-26 17:03:33 +01:00
nvrl 9089c8d13c feat(fuse): answer copy_file_range from bytes already on this machine
A copy inside the mount went through the kernel's read/write loop, one
FUSE round trip per page, because copy_file_range answered ENOSYS. The
daemon now copies a range whose source bytes are local (an open write
handle, a queued write, the cache) into the destination's scratch file,
the same way write stages them. A range it would have to download gets
EOPNOTSUPP, which makes the kernel copy that one call itself and keeps
the operation enabled. The copy still uploads: Drive cannot copy into a
file cp has already created.

In the simulation, a warm cp of a 256 MiB file went from about
100 MiB/s to about 450 MiB/s (debug build).
2026-10-26 16:57:06 +01:00
nvrl 071a11ef65 Merge remote-tracking branch 'origin/main' into feat/diagnose-keys
# Conflicts:
#	docs/CHANGELOG.md
#	docs/TROUBLESHOOTING.md
2026-10-26 16:45:36 +01:00
nvrl 4edcff3f87 feat(sharing,gui,cli): change public links and show folder sizes
The share dialog gets Link settings under an existing link: role, password
and expiry apply at once and keep the URL. The daemon fills in the options a
change keeps from the link as it stands, and refuses to keep a password it
cannot read rather than dropping it. pdfs public-link update does the same
from a terminal, and pdfs members shows the expiry.

Details shows a folder's size and item count as Proton Drive counts it,
trash included, and pdfs du prints it. Both are refused offline and for a
folder that is not on Drive yet.

Needs proton-drive-rs with update_public_link, get_folder_size and
PublicLink::custom_password.
2026-10-25 22:18:11 +01:00
nvrl ee59e338b6 feat(gui): make a copy and copy to another folder in My files
The entry menu gains Make a Copy, which copies an item next to itself, and
Copy To…, which reuses the move folder picker limited to My files; Copy
To… is also in the selection menu. Both send Request::Copy, so the copy is
made on Proton Drive.

The move and copy reports share one request loop. Docs describe the new
actions, and the changelog notes that saved public links now show their
names once the SDK decrypts them.
2026-10-25 21:41:21 +01:00
nvrl c1d9538669 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.
2026-10-25 21:10:50 +01:00
nvrl a73ed34421 Merge pull request #45 from narrrl/docs/wiki
docs: publish docs/ to the GitHub wiki and reorganise the user guide
2026-10-17 14:06:46 +02:00
nvrl 9c30ff8276 docs: publish docs/ to the GitHub wiki and reorganise the user guide
- scripts/publish-wiki.sh publishes every page listed in SECTIONS, copies
  images, writes a sectioned sidebar and footer, and removes stale pages.
  --check fails on links to missing files or headings and on docs pages
  that are neither published nor excluded.
- New wiki.yml runs the check on pull requests and pushes to main, and
  publishes when release.yml calls it or when started by hand.
- Split USER_GUIDE.md into topic pages (files, synced folders, photos,
  sharing, computers, launcher and tray); add Getting started and FAQ.
- Make the COPR repository the main Fedora path, with update and
  uninstall steps for every package type.
- Fix the stale CAPTCHA note in CLI.md, document pdfs unlock and lock,
  and drop the broken AGENTS.md link from CONTRIBUTING.md.
2026-10-17 13:57:55 +02:00
nvrl d1d4d23dd5 Merge pull request #44 from narrrl/automate-updates
feat: COPR repository and opt-in update check
2026-10-17 13:13:49 +02:00
nvrl f717a692ff fix(packaging): COPR project belongs to narl-rs
The COPR project lives under the FAS account narl-rs, not narrrl.

Refs #42
2026-10-17 12:38:09 +02:00
nvrl e065586d7e build(deps): proton-sdk and proton-drive-rs 0.9.2
0.9.2 adds the key report pdfs diagnose uses.
2026-10-17 12:23:19 +02:00
nvrl fd80c34152 feat(cli,core): trace the keys that open My Files in pdfs diagnose
When the session is stored and unlocked but the daemon does not answer,
pdfs diagnose asks Proton for a key report and prints a Keys section:
each account key, each address key and the account key its token is
for, and the address key the My Files share is encrypted to. It holds
key IDs only, so it can be posted in an issue.

Refs #40
2026-10-17 12:13:27 +02:00
nvrl e66308498b feat(gui,core): opt-in check for new releases
Preferences gets a switch that asks GitHub once a day for the latest
release and shows a banner over the sidebar when it is newer. Off by
default. Packages whose package manager brings updates install the
marker share/proton-drive-linux/managed-updates, which hides the switch.

Refs #42
2026-10-17 11:45:09 +02:00
nvrl 29b87ece72 feat(packaging): COPR repository for Fedora
The spec builds from the tag archive and a vendored crates tarball when
git_dir is not defined, so COPR can build it without network access.
release.yml publishes the vendor tarball and submits the build to COPR
after the release is published.

Refs #42
2026-10-17 11:44:33 +02:00
nvrl eb25c16fe6 Merge pull request #41 from narrrl/release/3.5.0
Release/3.5.0
2026-10-16 19:18:13 +02:00
nvrl 8f771fdcc4 fix(fuse): read the size again for a baseline row without a revision id
A mirror upload that Drive did not list yet when it was read back left a
baseline row with no revision id. The next walk reused the recorded size
whenever the modification time matched, and a row without an id never
counts as moved. So a revision another computer saved in the same second
looked unchanged and never arrived (B254). The B250 simulation hit this
about one run in five on CI.

A row without an id now reuses its size only while Drive gives no id
either. Otherwise the walk reads the size, and an unchanged file gets
Drive's revision id written into its row, so each row costs one extra
read. Rows from before schema 44 are read once after upgrading.
2026-10-16 18:44:51 +02:00
nvrl c1911ea1de docs: window size, keyring banner and the mailbox password per session
Add the window size and the narrow layout to the 3.5.0 changelog with
B253, document the `window` config field, and explain in the
troubleshooting guide why a two-password account asks for its mailbox
password after every reboot, and how to keep it unlocked.
2026-10-16 17:21:46 +02:00
nvrl 179b6555c4 fix(gui): keep a saved sign-in when the keyring can't be read
The app read the session with `auth::load().ok()`, so a locked keyring, a
Secret Service that was not up yet or any D-Bus error showed the sign-in
form as if nobody were signed in. The session was still saved (B253).

Keyring errors are now kept apart from "not signed in". The login page
shows a banner saying the saved sign-in could not be read, with a Retry
button. The app tries again after 2, 5, 10 and 30 seconds and opens on
Files once the keyring answers. The login page's header also loses the
back button the collapsed split gave it.

Translated into every shipped language.
2026-10-16 17:21:46 +02:00
nvrl e6971dd702 fix(gui): let the main window shrink to fit small screens
While signed in, the sidebar never collapsed. The window could not get
narrower than the sidebar plus the widest page, about 950 pixels, so on a
small or scaled screen the minimize, maximize and close buttons ended up
off-screen.

- The page stack is no longer homogeneous, so only the page on screen sets
  the minimum width.
- The selection bars of My files and Photos scroll sideways instead of
  holding the window at their full width, even while hidden.
- Below 760sp the sidebar and the page take turns, with a back button,
  and the My files search entry narrows. Picking a sidebar row opens the
  page.

Every page, in English and in German, now fits in a window 480 pixels
wide with its window buttons visible.
2026-10-16 17:21:46 +02:00
nvrl 770d4fda38 feat(core,gui): remember the main window's size
The app always opened at 980x680. It now keeps the unmaximized size and
whether the window was maximized in config.json under `window`, written
when the window closes and when the app quits with it still open. A
missing field, or a saved size below 480x400, opens at the old default.
2026-10-16 17:21:46 +02:00
nvrl 11d03091a7 docs: syncing several computers into one device; changelog for 3.5.0
Add B248 to B252 to the bug ledger, the shared-device feature and schema 44
to the 3.5.0 changelog, and describe the join flow, per-machine profiles,
conflict copy names and the mirror's multi-writer rules in the user guide,
recovery guide, CLI and configuration references and the architecture notes.
2026-10-16 16:18:14 +02:00
nvrl 7a0f1ebd6f feat(cli,gui): sync with another computer's device
Two or more computers can now sync the same folders through one device in
Proton Drive. `pdfs devices join UID` and "Sync With This Computer…" on
another computer's row adopt that device as this machine's and then offer its
folders for syncing here, like a restore. Unlike "Continue This Backup Here",
the other computer goes on syncing, and the confirmation says that changes
and deletions on one reach the other.

Translated into every shipped language.
2026-10-16 16:04:26 +02:00
nvrl 950056b48d feat(core,fuse): one profile per machine on a shared device
Every computer that synced into one device wrote the same profile.json, so
each save replaced the other computers' folders with its own (B252). Each
machine now writes profile-<machine id>.json, where the id is random, made on
first use and kept in machine-id next to config.json. No host identifier goes
to Drive.

Restore reads every profile in .proton-drive-linux, including the shared one
older clients wrote. A folder's proposed path and mode come from this
computer's own profile, otherwise from the newest one that has the folder.
The legacy file is left as it is, so a client before 3.5.0 still finds it.
2026-10-16 14:55:13 +02:00
nvrl 05b84bf796 fix(fuse,core): keep a mirrored folder safe when another computer syncs it
A mirrored folder assumed it was the only client writing to its folder on
Drive. With a second computer syncing the same folder, edits could be lost:

- The first sync took a same-size file at the same path for the same file.
  It now records the baseline only if the local file's SHA-1 matches Drive's
  digest, and otherwise keeps a conflict copy (B248).
- An upload replaced a revision another client had committed since the pass
  looked. The upload now checks Drive's active revision first, and an edit
  that read back with someone else's digest is left for the next pass to keep
  as a conflict copy (B249).
- A download recorded a revision committed while it ran as synced, so that
  edit never arrived. The baseline now records the revision the pass planned
  to download (B250).
- Conflict copies now carry the computer's hostname, so two computers do not
  make the same name on Drive (B251).

The baseline gains the revision id it was synced at (schema 44), and a new
revision counts as a remote change even at the same mtime and size.
2026-10-16 14:48:32 +02:00
nvrl 7cd516a164 chore(release): 3.5.0 2026-10-16 12:58:06 +02:00
nvrl c3c536892e docs(changelog): add B245 to B247 2026-10-16 12:31:51 +02:00
nvrl cc9db51b78 fix(gui): translate the tray's transfer status lines (B247) 2026-10-16 12:31:40 +02:00
nvrl 7384d8e97d fix(gui): read the new session off the main thread after sign-in (B246) 2026-10-16 12:31:40 +02:00
nvrl 4bf176a631 fix(packaging): accept watchdog pings from the daemon only (B245) 2026-10-16 12:17:53 +02:00
nvrl aa0c60f5a2 docs(changelog): list the second round of 3.5.0 fixes 2026-10-16 12:14:17 +02:00
nvrl 47934b7ddb fix(core,fuse,gui): date photos by their EXIF when nothing else does
A Takeout photo with no sidecar and no date in its name was filed under
the day of the import, and the app dated uploads by modification time.
A new `exiftime` module reads DateTimeOriginal (then DateTimeDigitized)
with its offset. Takeout uses it for undated photos it copies out for a
thumbnail, and the daemon uses it for app uploads before falling back to
the modification time (B244).
2026-10-16 12:12:41 +02:00
nvrl 48c8910e7b fix(gui,core): ask the mount off the main thread when opening, reap children
Opening a file or a location stat'ed the mount on the GTK main thread, so
a hung mount hung the app. The browser now uses the launcher's bounded
worker check (`present_within`, 2 s), and opening a location runs on a
thread. Every child the GUI starts is also waited for on a thread, so it
no longer stays a zombie (B243).
2026-10-16 12:07:48 +02:00
nvrl 6ec8043934 fix(gui): decode viewer photos and read their EXIF off the main thread
The viewer decoded each full-size photo, read its EXIF and stat'ed it on
the GTK main thread, freezing the app for a large photo. That now runs in
gio::spawn_blocking, and a result for a photo the user has left is
dropped (B242).
2026-10-16 12:04:05 +02:00
nvrl 99707c48fa fix(gui): free the photo viewer and the share dialog when they close
The viewer's handlers held strong references to the viewer and its
window, which own them, so every closed viewer stayed in memory with its
full-size photo. The share dialog's row handlers had the same cycle with
the dialog state. The handlers now hold weak references, and the viewer's
one strong handle is dropped on close with the photo (B241).
2026-10-16 12:03:02 +02:00
nvrl 5557aa66d5 ci: run the simulations on pull requests and lint with --locked 2026-10-16 11:58:56 +02:00
nvrl 6456f503f1 fix(cli,core): keep secret-holding commands out of core dumps
Only `pdfs daemon` marked itself non-dumpable, though `mount`, `login`,
`unlock` and `logout` hold the same passphrases or tokens. All five now
do. The file-manager script also passes each path after `--`, so a file
named like an option stays a path (B240).
2026-10-16 11:58:28 +02:00
nvrl 4836c5a84a fix(cli,fuse,gui): make --wait fail when an import or re-date did
`import-google-photos --wait` and `redate-photos --wait` exited 0 however
the run ended, and a run that failed as a whole left no summary, so the
CLI said no import had run and the app said it finished. Failed runs now
leave a summary with the reason, `--wait` exits 1 on failure, cancel or
failed photos, and the app shows the reason (B239).
2026-10-16 11:56:22 +02:00
nvrl 937a0a6824 perf(core): index queued ops by kind and name (schema 43)
The name checks the drain makes before each create, rename and trash,
and the wake of ops waiting for a name, read the whole queue, which made
a large offline batch quadratic to drain. Schema 43 adds an index on
pending_op(kind, name) (B238).
2026-10-16 11:50:56 +02:00
nvrl fcbb15cb4b fix(core,fuse): back failed uploads off properly and spread their retries
A reconnect made every failed op due at once, quota and access refusals
included, and ops that failed together retried in the same second. The
doubling also stopped at 128 s, short of the 300 s maximum. Reconnects
now move up only unrefused ops, and the backoff reaches its maximum with
a per-op spread of up to a fifth (B237).
2026-10-16 11:48:15 +02:00
nvrl 8aca7836f9 fix(fuse): upload thumbnails with imported, re-dated and app photos
Takeout imports, re-dates and the app's photo upload all sent an empty
thumbnail list, so the photos showed blank in Proton's apps. Re-dates and
app uploads now make thumbnails from their file. A Takeout photo that can
have one is copied out of the archive to the private staging folder,
thumbnailed and uploaded from the copy (B236).
2026-10-16 11:46:17 +02:00
nvrl 60a5a98174 perf(core): find Takeout sidecars by prefix lookup, not by ranking all
Each photo was ranked against every sidecar in its folder, so a folder
of 10,000 photos took about 30 s to scan. The sidecars are now indexed
by stem and each photo is ranked only against the stems a few range
lookups return: 0.1 s for the same folder, with the same matches (B235).
2026-10-16 11:41:02 +02:00
nvrl 51800c77d7 fix(core,fuse): refuse sync roots that overlap home, the mount or app data
Nothing stopped `/`, the home folder, the Proton Drive mount, the app's
own state or cache, or a folder nested in another root from becoming a
sync root, and a restore took a path from another machine's profile as
it was. One validator, `syncroot::problem`, now runs where roots are
added and restored, on paths resolved through symlinks with `..`
refused (B234).
2026-10-16 11:30:40 +02:00
nvrl 518763c5c9 fix(core,cli): end the session on Proton when signing out
Logout only deleted the keyring entry, so the session's tokens stayed
valid on Proton's side. It now ends the session first (DELETE auth/v4,
bounded to 10 s) and signs out locally either way. `pdfs logout` says
when Proton could not be reached (B233).
2026-10-16 11:26:29 +02:00
nvrl ef0f046939 fix(gui): show remote names as text, not Pango markup
Rows, toasts and status page descriptions parse their text as markup by
default. Shared item names, inviter addresses, device names, link URLs,
paths, EXIF values and daemon errors went in unescaped, so a name with
`&` showed as an empty row and other names were drawn as markup. Those
rows and toasts now use plain text, and status page descriptions are
escaped (B232).
2026-10-16 11:24:43 +02:00
nvrl 519ba0591a fix(fuse): give uploads under a low cap the time their share needs
The upload deadline assumed at least 32 KiB/s. Under an upload cap shared
by every drain worker and sync task, a large file ran out of time on every
attempt, took the mount offline and restarted from byte 0. The deadline
now counts the upload's share of the cap as the slowest rate when that
share is lower (B231).
2026-10-16 11:21:57 +02:00
nvrl 42b2f69fa0 fix(core,fuse): keep queued writes durable and their retries their own
- A gap-filled revision blob is recorded as complete once filled, so a
  retry after a lost upload answer recognises its own bytes on Drive
  instead of forking a conflict copy (B228).
- Commits that accept a change into the queue run at synchronous=FULL,
  so an op `close()` reported as done survives a power cut (B229).
- A queued create's adoption window starts at its first send, not when
  it was queued, so a create queued offline no longer takes another
  device's empty node of the same name. A send record that does not
  parse is kept instead of overwritten (B230).
2026-10-16 11:21:48 +02:00
nvrl 0204f69e9e docs(changelog): list the 3.5.0 fixes under Unreleased 2026-10-16 11:07:23 +02:00
nvrl d4ef9116c8 fix(fuse): download the base of a file open for writing on a worker
A read on a write handle was answered on the dispatch loop to keep its
order with the handle's writes, and when the bytes it asked for were
neither written nor cached it downloaded them there, holding every request
of the mount for up to the read deadline. The loop now answers from the
scratch file and the local caches only, and hands a read that needs a
download to a transfer worker with the handle as it was when the read came
in (B227).
2026-10-16 10:47:58 +02:00
nvrl f8c60c9753 build: add a release profile that strips the binaries
No package ships a separate debug package, and the unstripped binaries were
43, 24 and 12 MB. LTO stays off, as the packaging comments already said the
release profile kept it, and unwinding stays on for the workers that catch
panics.
2026-10-16 10:32:05 +02:00
nvrl 607972cf1e fix(fuse): turn generated thumbnails the way their EXIF orientation says
Thumbnails were decoded without the EXIF orientation, so a portrait photo
stored as landscape pixels plus a tag got a sideways tile, and the drain
uploaded that thumbnail to Drive for every client to show (B226).
2026-10-16 10:32:05 +02:00
nvrl 96b70600f0 fix(core): match a Takeout sidecar by prefix only where Google cut its name
The prefix rule for sidecars whose names Google cut at 51 characters also
matched names that were never cut, so IMG_1.jpg's sidecar gave its title
and date to an IMG_12.jpg that had none. It now applies only to a sidecar
name at the cap. Edited copies, which have no sidecar of their own, take
their original's date by a rule of their own and keep their own name
(B225).
2026-10-16 10:32:05 +02:00
nvrl b0ff6ee441 fix(core): never write defaults over a config file that does not parse
load_config gives defaults for a file it cannot read, and every setter
loads, changes one field and saves. The first settings change after the file
was damaged therefore wrote defaults over the mountpoint, the adopted device
and the ignore rules. save_config now refuses while the file on disk does
not parse, and the first-run default is written atomically with mode 0600
(B223).
2026-10-16 10:32:05 +02:00
nvrl bca22056b0 fix(fuse): report write errors at close, and take fsync and fallocate as POSIX does
flush answered ok at once and release synced the scratch file, but close(2)
only returns what flush answers, so a full disk at close was reported to
nobody. flush now syncs a handle with writes on a worker and returns ENOSPC,
EDQUOT or EIO; release then finds the pages on disk instead of writing a
whole file back on the dispatch loop. fsync on a descriptor opened for
reading no longer fails with EBADF (B222).

fallocate passed any mode through and tracked only a punched hole or
growth: a zeroed range came back from the remote base at commit, and a
collapse or insert corrupted the file. Unsupported modes now get EOPNOTSUPP,
zeroed ranges count as written, the real errno is returned, and the state
lock is not held across the syscall (B224).
2026-10-16 10:32:05 +02:00
nvrl 830cfd9faa fix(fuse): allocate a listing's inodes before taking the state lock
A cold listing allocated the local id of every new child in a write
transaction while it held the global state lock, then marked the folder
listed, still under it. Every inline handler of every mount waited on that
transaction, and the transaction on the write connection the drain shares.
The ids are now allocated before the lock is taken, and the folder is marked
listed through the outbox after its children's rows, so a crash in between
no longer leaves it recorded as complete and empty (B221).
2026-10-16 10:05:58 +02:00
nvrl 77dac97afe fix(fuse): keep slow remote calls off the dispatch loop and the metadata workers
A shared folder's listing is fetched again once it is a minute old, and that
fetch ran on the dispatch loop whenever the old listing was still resident,
so every request on the mount waited for Drive (B219).

Thumbnail downloads, size upgrades and the Shared with me listing had no
deadline of their own. On a stalled link each held a mount thread for the
SDK's retry budget, and a folder of photos filled the three metadata workers
that every cold lookup and rename needs. Offline, an uncached thumbnail is
now answered as absent, and these calls give up after 20 or 60 seconds
(B220).
2026-10-16 10:02:35 +02:00
nvrl 2bea1f7672 fix(core): check and count the database on a read connection
PRAGMA integrity_check and the row counts behind pdfs cache inspect read
the whole file, 34 s on a 567 MiB database, and they held the write
connection while they did. Every write in the daemon waited behind them.
They now read a WAL snapshot on a pooled read connection (B218).
2026-10-16 09:56:25 +02:00
nvrl 0f279c7536 fix(core,fuse): compact the search indexes and stop pruning after partial scans
FTS5 keeps deleted entries until their segments are merged, which with
these indexes is close to never. On one machine local_fts held 317 MiB for
40,000 paths, against 20 MiB rebuilt, and search ran four times slower. The
indexes are now compacted once a day after a local scan. A scan that hit the
entry cap or lost a batch no longer prunes the paths it never reached, which
the next scan used to add back (B217).
2026-10-16 09:56:13 +02:00
nvrl d6bc921a4b fix(core): keep the upload queue when a damaged database is rebuilt
A database SQLite reported as corrupt at open was moved aside and replaced
by an empty one, on the grounds that everything in it is fetched again. The
queue of writes Drive has not received, the mirror baselines and the pins
are not, so they were lost. They are now copied out of the damaged file,
read only and table by table, into the new one (B216).
2026-10-16 09:50:53 +02:00
nvrl 3221e4aa97 fix(fuse): mirror sync no longer deletes what Drive or this disk never had
Five ways a mirror pass could lose files:

- A folder deleted on Drive was removed here with everything in it, including
  new files and edits whose uploads had just failed for want of a parent, and
  ignored content such as .git. Such a folder now stays and is created on
  Drive again (B211).
- A node the SDK could not decrypt was missing from the remote walk and so
  read as deleted on Drive. A short listing or an unreadable size now leaves
  the affected paths out of the pass, and a pass in which no synced path is
  left on Drive is refused (B212).
- A file Drive deleted was removed here even when it had been edited since
  the scan. It is now checked again first, and kept (B213).
- A folder deleted here was trashed on Drive although another device had just
  put new files in it. It now stays on Drive (B214).
- A decrypted name holding / or .. became a path outside the sync folder, and
  failed the whole listing in the mount. Such names are left out (B215).
2026-10-16 09:47:08 +02:00
nvrl 18cc2ea498 chore(release): 3.4.1 2026-10-16 01:32:59 +02:00
nvrl a9cb389f04 docs: a locked volume after a password reset (B210)
Issue #40: after a password reset the daemon failed every mount with
"failed to decrypt: missing key", and the app waited on "Connecting…"
because control.sock was never created. proton-drive-rs 0.9.1 now
creates a new My Files volume when no active regular volume is left,
as the web client does.

- BUGS.md: B210 with cause, fix and the SDK tests that cover it
- TROUBLESHOOTING.md: what the log line means and how to restore the
  files of the locked volume from the web
2026-10-16 01:32:59 +02:00
nvrl 923c47acb8 chore(deps) updated sdk to 0.9.1
0.9.1 replaces a locked My Files volume, the state a password reset leaves behind, instead of failing every mount with "missing key" (#40).
2026-10-15 23:02:36 +02:00
nvrl 478a0e6cdd fix(core,gui,cli): a blocked sign-in no longer asks to update the app (B209)
Proton answers a sign-in it blocks for abuse or rate limits with code
2028 and the text "This version of the app is no longer supported". The
SDK read 2028 as Unknown, so the app passed that text through and told
users to update a current release (issues #28, #39).

- deps: proton-sdk and proton-drive-rs 0.8.3, which name 2028 Blocked
- core: error::sign_in_blocked detects it
- gui: the login page says Proton is refusing sign-in for now and that
  waiting helps, translated in every catalog
- cli: pdfs login adds the same hint to the error
- cli: pdfs diagnose warns instead of failing when cache.db does not
  exist yet; the daemon creates it after the first sign-in
- docs: BUGS.md B209, changelog, troubleshooting entry
2026-10-13 20:29:23 +02:00
nvrl b46abfc07c chore(release): 3.4.0 2026-10-12 18:12:00 +02:00
nvrl fd78f088ec test(fuse): let a held create carry its bytes in the simulation
FUSE delivers `release` after `close()` returns, and the release is what
attaches the written bytes to the queued create. A drain worker that is
free can claim the create first and send the file empty, with the bytes
following as a revision. Tests that hold a create, its reply, or lose
its reply then act on the empty create, and their assertions about the
file's content race the revision. CI saw this as an occasional failure
of a_listing_taken_before_a_create_commits_keeps_the_landed_file.

Add Daemon::write_new, which pauses sync, writes the file, waits until
the pending create carries the bytes, and resumes. Use it for every
write that directly follows a create hold.
2026-10-12 18:05:45 +02:00
nvrl 005f7ded1f fix(fuse): a file landing while its folder is pinned stays available offline (B208)
A folder pin reads each file's modification time and size before it
downloads the file. If the file's create landed during the download, the
landing set Drive's time, and the pin then stored the blob under the old
time. The file showed as kept offline but read as stale.

After each download, the pin now compares the file's time and size with
the tree's. If they changed, it downloads the file again under the new
values, at most three times.

The simulation gains FakeClient::hold_next_download to force that order.
2026-10-12 15:40:56 +02:00
nvrl 3e4886f965 fix(fuse): refuse a move between locations under a just-opened file (B207)
The account run of 3.3.0 failed "move refuses a file still being
written" on the on-demand to mirror pairing: pdfs move moved the folder
while open.txt in it was open for writing, and the write landed after
the move.

retire_create re-keys a landed create's nodes row to the real uid first
and repoints the tree and the open write handles after. A file opened in
between holds a handle with the placeholder uid. has_pending_work_under
placed each open handle by looking its uid up in the nodes table, which
no longer has the placeholder, so it saw nothing open under the folder,
and with the queue already empty the move went ahead. Open handles are
now placed in the in-memory tree by inode, which a landing create does
not change.

The simulation test holds the drain at that point with the new
FakeClient::hold_next_landing_of_any, and fails without the fix.
2026-10-12 00:57:10 +02:00
nvrl aac53e3139 test(fuse): let the simulation sync local folders
A simulated daemon could not add a synced folder, so nothing that moves
between a mount, an on-demand folder and a mirror folder could be tested
without a real account.

Listing and registering devices now go through DriveApi, and the fake
Drive keeps the account's computers, each with a root folder outside My
files. The simulated daemons also read their config.json from a scratch
XDG directory instead of the developer's own: an adopted device of the
real account made every sync folder fail to register against the fake
Drive.
2026-10-12 00:57:04 +02:00
nvrl c2bf87596f test(acceptance): remove the local-only symlink the mirror-copy case leaves
"move never loses a mirror copy Drive lacks" puts a symlink in a mirror
folder to prove a move does not lose what sync never uploads, and left it
there. The next mode pairing switches that folder to on-demand, which the
daemon refuses because the link exists only on this computer, so the
account run stopped after pairing 2 of 4. The case now removes the link
wherever the move left it, whether it passes or fails.
2026-10-12 00:24:40 +02:00
nvrl 69d4feaa07 docs: add a security model page for users
Readers asked whether the client can be trusted with their files. The
threat model in ARCHITECTURE.md is written for contributors, so
docs/SECURITY_MODEL.md summarises it for users: encryption comes from
the SDK and rPGP, sign-in uses SRP, what is stored where and how it is
protected, who can reach the daemon, every network connection, what the
client does not protect against, and how each package is built and
signed. It says that there has been no independent audit.

It is linked from the README, the documentation index and SECURITY.md.
2026-10-12 00:07:18 +02:00
nvrl d481dcc15c feat(fuse,core,cli,gui): make folders available offline from the app
The daemon has kept recursive folder pins for a long time, but the app
offered "Available offline" for files only. The context menu, the
details pane and the selection bar now offer it for folders too, and a
pinned folder carries the offline badge in the browser and the launcher.

A folder pin used to download the whole subtree inside the control
request, so a large folder outlasted the client's 120 s read timeout,
and every pinned file was read into memory before it was cached. The
daemon now records the pin, answers at once and downloads on a
background thread as a job the tray and the app show, stopping when the
folder is unpinned or the daemon stops. Pins of files and folders both
go through fetch_content, which streams to disk and skips fresh blobs.
A folder pin is refused while offline instead of looking kept.

A file kept only because a folder above it is pinned has no pin of its
own, and unpinning it silently did nothing. ListDir now reports
pinned_by_folder; the app shows such an entry as available offline with
the toggle greyed out, and the daemon refuses the unpin with a message
naming the folder. Unpinning a folder no longer evicts files that are
pinned on their own.

Requested on Reddit.
2026-10-12 00:07:18 +02:00
nvrl e0442c56b9 fix(packaging): stop the service before the package is removed
Removing the package left the user service running from the deleted
binary, with ~/ProtonDrive still mounted. A user who then deleted the
folder to clean up had every file in it moved to the Proton Drive trash,
as any delete through the mount is.

The .deb (prerm on remove), the .rpm (%preun on erase) and the Arch
packages (pre_remove) now stop proton-drive.service for every user with
a running user manager, through runuser with that user's
XDG_RUNTIME_DIR, so no systemd-machined is needed. Upgrades are left
alone. Queued writes stay in the state directory for a later install.

The pacman install script lives once in packaging/ and the AUR
directories link it; push.sh copies its content into each AUR
repository. The uninstall guide now warns not to delete the mount point
while the service runs.

Reported on Reddit by a user who restored the files from the trash.
2026-10-12 00:07:07 +02:00
nvrl ee76f4ac60 fix(packaging): recommend each keyring only on its own desktop
The RPM recommended both gnome-keyring and kwallet unconditionally, and
dnf installs weak dependencies by default, so a GNOME install on Fedora
pulled in KWallet and KDE Frameworks with it. The recommendations are
now rich dependencies: KWallet only with plasma-workspace, GNOME Keyring
only without it, and the AppIndicator extension only with gnome-shell.
RPM does not accept `unless` in a weak dependency, so the keyring uses
the equivalent `(gnome-keyring or plasma-workspace)`.

Reported on Reddit by a Fedora GNOME user.
2026-10-12 00:06:51 +02:00
nvrl f6c651b06b ci(nix): push Nix builds to a Cachix binary cache
Every Nix build of main and of each release tag now goes to
proton-drive-linux.cachix.org, so installing the flake downloads the
client instead of compiling it. The flake names the cache in nixConfig
for nix run and nix shell, and the install guide shows the nix.settings
a NixOS system build needs, since it does not read an input's
nixConfig. A fork's pull request has no token and only pulls.
2026-10-11 23:02:22 +02:00
nvrl 160626b29d chore(release): 3.3.0 2026-10-11 21:53:52 +02:00
nvrl f1f1f8ad4d ci(release): build the Nix flake at every release tag
A flake input pinned to vX.Y.Z is the Nix release, so the release
workflow now builds the flake at the tag and checks its version before
the GitHub release is published. The install guide shows how to pin the
input to a release tag, and the release checklist names the files that
carry the version.
2026-10-11 21:53:52 +02:00
nvrl bb28a18f49 Merge branch 'feat/issue-37-nix' 2026-10-11 21:48:28 +02:00
nvrl a3c0c9ee9b ci(release): take the release notes from the changelog
The release workflow created each GitHub release without a body, so 3.1.2,
3.2.0 and 3.2.1 shipped with empty notes. It now copies the version's
section of docs/CHANGELOG.md into the release, and fails when the section
is missing.
2026-10-11 21:24:39 +02:00
nvrl 91fa1614ed fix(core,fuse,cli,gui): say when an upgrade left the old daemon running
A package upgrade replaces the binaries but does not restart the user
service, so the daemon keeps the old release and its bugs until the next
login. On 3.2.0 the reporter of #33 still saw "Couldn't sync Documents:
read .../snapshots/37: Permission denied", which only a daemon older than
3.2.0 can produce: since then the scan leaves an unreadable subfolder out
and only an unreadable sync root fails the pass (B206).

The daemon now reports its release in Status; one from before the field
omits it, which reads as older. When the daemon is older than the
front-end, the tray says "Update not finished - restart Proton Drive" and
offers a restart, the sync card shows "Restart to finish updating" with a
Restart button, and pdfs status and pdfs diagnose print the systemctl
command. A newer daemon is not flagged, since then the front-end is the
stale one.

Refs #33
2026-10-11 21:20:51 +02:00