Commit Graph
602 Commits
Author SHA1 Message Date
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
Nils Pukropp 3d96cdbab1 chore(aur): update packages to 3.4.1 2026-10-15 23:47:24 +00:00
nvrl 18cc2ea498 chore(release): 3.4.1 v3.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
Nils Pukropp 2633b4ed72 chore(aur): update packages to 3.4.0 2026-10-12 16:21:56 +00:00
nvrl b46abfc07c chore(release): 3.4.0 v3.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
Nils Pukropp 8b7d7ae87f chore(aur): update packages to 3.3.0 2026-10-11 20:08:31 +00:00
nvrl 160626b29d chore(release): 3.3.0 v3.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
nvrl 87361a4573 fix(core,fuse,gui): a photo upload the SDK refuses no longer says offline
Every SDK error that was not an API reply was classified as Offline.
Only a transport failure is. An upload on an account without a photo
library fails in the SDK before any request, and the app said "You're
offline" while every other call worked (issue #38).

Classify by variant: Transport is Offline, InvalidOperation is Invalid,
Serialization is Remote, Crypto and KeysLocked are Internal. The daemon
logs a failed photo upload, and the Photos page greys out Upload when
the account has no photo library.

docs/BUGS.md B205.
2026-10-11 19:26:53 +02:00
Nils Pukropp 6d0b6ad553 chore(aur): update packages to 3.2.1 2026-10-11 17:24:37 +00:00
nvrl 170c3f01ff chore(release): 3.2.1 v3.2.1 2026-10-11 19:19:36 +02:00
nvrl 976ad2aa04 docs(changelog): list the issue #35 fixes under Unreleased
The CPU spin while offline, the expired session shown as offline, the
lost refresh tokens and the keyring-refused rotation were fixed without
a changelog entry.
2026-10-11 18:56:55 +02:00
nvrl b73cd087aa test(core): time the B12 index write as the best of five runs
A single timed run lost the CPU to the rest of the suite running
alongside it and read as the full-scan regression (171 ms against
701 ms). The fastest of five runs still catches the 6x scaling of the
old path.
2026-10-11 18:56:55 +02:00
nvrl e402981708 fix(packaging): ship the Adwaita icons with the Nix package
GTK ships only a few stock icons itself. On a NixOS session without
Adwaita installed, such as Hyprland, the Photos and Computers sidebar
entries and about twenty other icons showed as missing (#37).
adwaita-icon-theme in buildInputs puts it on the wrapper's XDG_DATA_DIRS.
2026-10-11 18:55:00 +02:00
Claude 453c439b74 fix(packaging): give the Nix package TLS for WebKitGTK
On NixOS the CAPTCHA window failed with "TLS support is not available":
WebKitGTK's GIO had no TLS backend. With glib-networking in buildInputs,
wrapGAppsHook4 puts its GIO modules on GIO_EXTRA_MODULES in every
wrapper.

Refs #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuyxByBpK3fSWtyPkugeHz
2026-10-10 08:23:21 +00:00
Claude ceba0f5849 feat(packaging): add a Nix flake and NixOS module
Builds the four binaries with rustPlatform.buildRustPackage from
Cargo.lock, installs the desktop entries, icon, user service and
translations as the other packages do, and wraps the binaries with
exiftool and ffmpeg on PATH. The NixOS module installs the package,
pulls proton-drive.service into the graphical session with the
fusermount3 wrapper on its PATH, and enables FUSE and GNOME Keyring.

A Nix workflow builds the flake when its recipe, the lock files or the
code change.

Refs #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuyxByBpK3fSWtyPkugeHz
2026-10-09 22:07:20 +00:00
Claude 3e0db8cf46 fix(fuse): fill an incomplete queued revision's gaps on a write open
A partial write whose base was not in the content cache queued an
incomplete revision, and every write open over it was refused with EIO
until the drain uploaded it. CI's one-client-lan simulation hit this on
seed 1: a create over a file whose partial edit had not drained yet.

The write open now copies the blob and fills its gaps from the remote
base with Core::fill_gaps, as the drain does, and fails only when that
fill fails (B204).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuyxByBpK3fSWtyPkugeHz
2026-10-09 11:36:46 +00:00
nvrl 2a86830e72 Merge branch 'fix/issue-35-dead-session'
Issue #35: a refused session showed as offline forever, the drain
workers spun while offline, and refreshed tokens could be lost or
overwritten (B200-B203, B61). Needs proton-sdk 0.8.2.
2026-10-09 12:34:38 +02:00
nvrl 43031ecf99 chore(deps) updated sdk to 0.8.2 2026-10-09 12:34:02 +02:00
nvrl 2b6c37f543 fix(fuse,core,cli,gui): say when the keyring refuses the refreshed sign-in
A rotation the keyring refused was retried and kept in the kernel
keyring, but only the log said so, and a reboot before the keyring took
it still signed the user out.

`Response::Status` carries `tokens_unsaved` while one waits. The tray
says "Sign-in not saved — unlock your keyring" and needs attention, the
app's status says the same, `pdfs status` prints a Keyring line and
`pdfs diagnose` warns. New strings are translated in every catalog
(B61).
2026-10-09 12:22:02 +02:00
nvrl 3458e2e72d fix(core): keep refused token rotations in the kernel keyring
A rotation the Secret Service refused lived only in the daemon's
memory, so a crash or SIGKILL before the retry landed lost the only live
refresh token.

Keep it in the kernel keyring as well until it is written. The next
resume, by the daemon or `pdfs unlock`, puts it back in when the stored
blob still holds the refresh token it spent, and writes it; one left
over from an older login is dropped. `kernelkey` now holds named keys,
the passphrases and the unsaved tokens (B61).
2026-10-09 12:14:25 +02:00
nvrl 0ac5928138 fix(fuse,core,cli): resume a session another process refreshed
`pdfs unlock` resumes the daemon's session and may refresh it. The
daemon then holds a spent refresh token, and since B202 a refusal of it
read as an expired session although the keyring held a live one.

Before calling a refused session expired, compare the keyring with what
this process last stored. When it holds a newer rotation or another
login, end the mount with `MountOutcome::ResumeStored` and mount again
from the keyring. A rotation another process already wrote counts as
this session's, so the daemon keeps writing its later ones (B203).
2026-10-09 12:08:17 +02:00
nvrl beb3b2e4e4 fix(core): retry rotated tokens the keyring refused
A failed keyring write after a refresh was only a warning, and the one
live refresh token then lived in memory only (B61). The sink now keeps
the rotation as unsaved and retries it in the background after 1 s, 5 s,
30 s and then every 5 minutes, until it is written or the blob has moved
on. The refusal is logged as an error naming `pdfs login`.

docs/BUGS.md B61.
2026-10-09 11:29:31 +02:00
nvrl bf43476e88 fix(fuse,core,cli,gui): show a refused session as expired, not offline
A refresh token Proton refused, or a deleted or disabled account, was
treated as a lost connection: the mount came up "offline", the probe
retried forever and logged only at debug, and nothing told the user that
only signing in again helps (issue #35).

session_revoked() names those errors. The root fetch, probe, drain,
event poll, event seed and photos root now set session_expired, log one
warning naming `pdfs login`, and stop retrying. Cached files stay
readable and changes stay queued.

Response::Status carries session_expired (serde default false). The
tray shows "Session expired — sign in again" with a Sign In Again…
item that opens the app's sign-in page; the app's status and Sync card
say the same with a button. `pdfs status` and `pdfs diagnose` report it.
New strings are translated in every catalog.

docs/BUGS.md B202.
2026-10-09 11:27:09 +02:00
nvrl 6363125fec fix(core,cli): write rotated tokens only into their own session's blob
Proton refresh tokens are single-use. store_tokens replaced the tokens of
whatever blob was stored, so after `pdfs login` restarted the daemon, the
old daemon's shutdown persist wrote its spent tokens into the new
session's blob, and the new daemon failed with InvalidRefreshToken
(issue #35). `pdfs unlock` and the legacy migration had the same shape.

Each session now writes through a TokenSink that only touches a blob
with its session id that still holds the refresh token it last left
there. Shutdown only flushes a rotation the keyring refused. unlock and
migrate_legacy re-read the blob after verifying.

docs/BUGS.md B201.
2026-10-09 11:08:40 +02:00
nvrl 51d114a8da fix(fuse,core): idle drain workers no longer spin while offline
Offline or paused, a drain worker claims nothing and then slept until the
earliest queued op fell due. Every queued op was already due and nothing
attempted it, so the wait was zero and all 16 workers spun on the queue
for the whole outage (issue #35: 17 min of CPU in 2 min 16 s).

A worker that claims nothing now sleeps the full idle poll; a reconnect
or resume wakes it. earliest_due_at also uses the claim's exclusions, so
an op held behind a claimed op on the same node no longer counts as due.

docs/BUGS.md B200.
2026-10-09 11:05:12 +02:00
Nils Pukropp 31be27f30e chore(aur): update packages to 3.2.0 2026-10-09 01:14:52 +00:00
nvrl 4c498d559c Merge pull request #32 from narrrl/fix/captcha-in-browser
fix(gui,cli,core): offer the CAPTCHA in the browser when the web view…
v3.2.0
2026-10-09 03:04:10 +02:00
nvrl 7fd3439b60 Merge remote-tracking branch 'origin/main' into pr32-merge
# Conflicts:
#	docs/CHANGELOG.md
#	po/de.po
#	po/en_GB.po
#	po/es.po
#	po/fr.po
#	po/it.po
#	po/nl.po
#	po/pdfs.pot
#	po/pl.po
#	po/pt_BR.po
2026-10-09 03:03:31 +02:00
nvrl c4847c4d52 chore(release): 3.2.0 2026-10-09 02:56:26 +02:00
nvrl ad49e32de9 fix(gui): context menus size to fit instead of scrolling
A popover is sized and placed again when its parent's layout manager
allocates it. Context menus were parented to the outermost list view,
and the keyboard menu to the ScrolledWindow; neither has a layout
manager, so a menu kept the size it was first given and often opened
too short, with a scrollbar.

The host is now the first widget from the list view up that has a
layout manager, which still keeps menus out of list items and the theme
rules that style their insides.
2026-10-09 02:44:00 +02:00
nvrl 19854b2d76 fix(gui): tidy the Share dialog
Send Invitations and Create Public Link sat alone in boxed-list rows,
which read as broken settings. They now sit under their section at its
right edge. The role picker moves into the address row it applies to,
people get avatars, a member needs no subtitle, and invitations say they
are pending. The People and Public link sections show a loading row
until the share is read, and one line instead of nothing when it can't
be. The page is an AdwPreferencesPage, for the standard spacing.

Translated in every catalog.
2026-10-09 02:41:07 +02:00
nvrl 085335ca14 feat(gui): one main menu, on the sidebar's account row
The sidebar header had a hamburger beside the brand, and the account row
in the footer had its own ⋮ menu; both offered Preferences. The header
button sat next to the page's own tools and read as one of them.

The account row is now the main menu's button, as in Proton's own apps:
Preferences, Keyboard Shortcuts and About, then Sign Out. The whole row
opens it, upwards, and F10 still does as the primary menu.
2026-10-09 02:37:25 +02:00