Commit Graph
186 Commits
Author SHA1 Message Date
nvrl d2b5916b99 Merge remote-tracking branch 'origin/main' into fix/create-landing-counted 2026-11-01 11:28:03 +01:00
nvrl 801681f747 Merge remote-tracking branch 'origin/main' into fix/create-landing-counted
# Conflicts:
#	docs/BUGS.md
2026-11-01 11:14:15 +01:00
nvrl b3ea564c6b docs(bugs): keep the index newest first 2026-10-31 19:29:29 +01:00
nvrl 463c97a14d Merge branch 'chore/sdk-0.10.1' into fix/clean-stop-runtime
# Conflicts:
#	docs/BUGS.md
2026-10-31 19:29:05 +01:00
nvrl 40aa8016ea Merge branch 'fix/sim-flakes-and-open-race' into chore/sdk-0.10.1
# Conflicts:
#	docs/BUGS.md
2026-10-31 19:26:47 +01:00
nvrl d1b7535696 Merge branch 'fix/restore-outcome-and-walk-gaps' into fix/sim-flakes-and-open-race
# Conflicts:
#	docs/BUGS.md
2026-10-31 19:26:44 +01:00
nvrl ba414c4446 Merge branch 'fix/diagnose-coverage' into fix/restore-outcome-and-walk-gaps
# Conflicts:
#	docs/BUGS.md
2026-10-31 19:26:41 +01:00
nvrl 5b7f17bdaf Merge branch 'fix/db-errors-not-offline' into fix/diagnose-coverage
# Conflicts:
#	docs/BUGS.md
2026-10-31 19:26:38 +01:00
nvrl 73068f5868 Merge branch 'fix/mount-failure-surfaced' into fix/db-errors-not-offline
# Conflicts:
#	docs/BUGS.md
2026-10-31 19:26:36 +01:00
nvrl b093010b64 Merge branch 'fix/never-synced-label' into fix/mount-failure-surfaced
# Conflicts:
#	docs/BUGS.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-31 19:26:34 +01:00
nvrl ab8fe0e61d Merge branch 'fix/sync-scheduling-watches' into fix/never-synced-label
# Conflicts:
#	docs/BUGS.md
2026-10-31 19:26:28 +01:00
nvrl b8295373f7 Merge remote-tracking branch 'origin/docs/bugs-issue-audit' into fix/sync-scheduling-watches
# Conflicts:
#	docs/BUGS.md
2026-10-31 19:21:21 +01:00
nvrl 73af39ee65 Merge remote-tracking branch 'origin/docs/bugs-issue-audit' into fix/sync-engine-containment
# Conflicts:
#	docs/BUGS.md
2026-10-31 19:11:41 +01:00
nvrl 176112a9cb fix(cli): leave the runtime running on a stop so stragglers cannot panic
A stop shut the tokio runtime down after joining the workers it owns.
Threads it never joins, such as a size lookup or a profile backup that
a request started during the stop, then panicked on their next timer
with "A Tokio 1.x context was found, but it is being shutdown". The
process exits right after a stop, so the runtime is now left for the
exit to end. A mount that is retried still shuts its runtime down with
the 2 s limit.

The acceptance journal check now counts panics, and no longer counts
the ENOENT reply errors a stop causes by aborting its FUSE connections.

docs/BUGS.md B276.
2026-10-31 19:09:23 +01:00
nvrl 7957e1ed59 chore(deps): bump proton-sdk and proton-drive-rs to 0.10.1
0.10.1 decrypts address key tokens with every account key that opens,
not only the active ones, and leaves out an address whose keys do not
open instead of failing the account. That is the likely cause of issue
#40, a mount that retried "missing key" forever (B265).
2026-10-31 13:48:01 +01:00
nvrl 53543c91f9 docs(bugs): record the slow-link conflict copy seen in CI (B275) 2026-10-31 13:16:47 +01:00
nvrl fccb3d9561 fix(fuse): retry a write open whose queued revision landed under the copy
A write open copies the queued revision's blob into its scratch file.
When the upload landed between the sample and the copy, the drain had
already deleted the blob, and the open failed with EIO (B274, seen once
in CI). The drain drops the pending entry before the blob, so a failed
copy whose entry moved on now goes around again, as the re-check after
the copy already does.

Two simulations raced the daemon rather than it:
- a_file_deleted_after_a_restart_leaves_drive_with_what_its_create_sent
  let the create's reply through before the kernel's release attached
  the second bytes. That attach resets the attempts, so the retry
  landed before the test could see the failure. It now waits for the
  bytes to be attached.
- a_file_deleted_offline_while_open_still_reads_and_takes_writes read
  the cache as the queue emptied, which is a moment before the
  eviction. It now waits for the cache to empty.
2026-10-31 12:58:11 +01:00
nvrl 6f2b4bb80a fix(fuse,cli): keep a pass going past a failed listing, report skipped restores
A listing that failed anywhere in the remote walk ended the whole pass,
so a first sync of a large folder on a flaky link walked the tree again
and again and never downloaded anything (B271, issue #46). A failed
listing below the root now records that folder as a gap: nothing under
it is deleted, the rest of the pass applies, and the report names it.

A restore that skipped every folder was logged as a successful activity
(B269). The daemon now gets the restored and skipped folders back, logs
any skip as a failed activity with the reasons, and sends the Rewatch
for folders already restored when the database insert fails partway.
`pdfs devices join` points to `pdfs activity` for the outcome.
2026-10-31 11:11:01 +01:00
nvrl a617b2b410 fix(cli): have diagnose report failing writes, a pause and folders in error
diagnose dropped most of the Status reply and never looked at the synced
folders, so it said "No problems found" while a folder would not sync
(B268, issue #53). It now warns about writes that keep failing, a write
a day old, a pause, folders in error or conflict, and a control socket
nothing listens on.
2026-10-31 10:57:34 +01:00
nvrl bdbfc0bfd4 fix(core): read database errors handed to from_api as internal, not offline
from_api took every error that was not a ProtonError for a transport
failure. Pin, unpin, the searches and node loads hand it pdfs_core
errors from the cache and the database, so a failure there said
"You're offline" (B266, the rest of B205). The source chain now
decides, and only an error with neither type reads as offline.
2026-10-31 10:53:25 +01:00
nvrl e9577f953f fix(cli,core,fuse,gui): say why the mount keeps failing, and back off
A mount that failed on every try retried every 5 s forever, and the
reason was only in the journal: the app and the tray said Connecting...
for days (B263, issue #40). The retry now doubles to a 2-minute ceiling,
and the last failure is kept in mount-failure.json for the app, the
tray, pdfs diagnose and systemctl status.

The early returns in mount left the workers they had started running
beside the next attempt (B264). They are now stopped on every error
path, without telling systemd the daemon is stopping.
2026-10-31 10:49:01 +01:00
nvrl 437d14b0f5 fix(gui): say a folder is waiting for its first sync, not up to date
A new sync folder row starts with state idle, so the app called a folder
up to date before any of it reached Drive (B261, issue #53). A mirror
folder that has never synced now reads "waiting for first sync", and
the This computer row counts such folders. All catalogs translated.
2026-10-31 10:30:52 +01:00
nvrl b40ca797d9 fix(fuse): run a burst's named folders first and retry failed watches
A poll in the same burst as a new folder's Reconcile(id) took its place,
so the new folder waited behind a pass over every other one (B260).

A recursive watch that failed partway left its watches registered, was
never retried, and its events ran passes over every folder (B262). It
is now unwatched and retried every 10 minutes, symlinks are not followed,
and watcher errors are logged. A folder removed and added again in one
burst keeps its watch under the new id (B272).
2026-10-31 10:24:32 +01:00
nvrl c64bb23e3c fix(fuse,core,cli): keep a panicking sync pass from ending the engine
One thread runs every synced folder's passes. A panic in a pass ended
it, and every later add, sync-now, file event and poll went nowhere
without a word until a restart, which fits issue #53 (B259).

Each pass now runs under catch_unwind; a panic sets the folder to error
and the next poll tries again. Diagnostics reports the engine's state,
the supervisor warns when it has ended or a pass has gone quiet, and
first passes log at info.
2026-10-31 10:14:23 +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 639ca359fa fix(fuse,core): count a new file's landing until its bytes are cached
A create that carries a file's bytes retired its op, then cached the
bytes and gave the tree the real uid. Nothing counted that window, so
`pdfs status` read empty while it ran, which B119 promises means the
file reads without the network.

A delete in that window found the placeholder uid in the tree and
trashed the node by its real uid. `queue_trash` asked the tree whether
that uid was open, found no entry, and dropped the cached bytes the open
handle reads. If the cache store ran at the same moment, its tag was
written after the eviction and never removed. CI caught that as a
stray .meta file.

The create's landing is now counted like an upload's, from the op's
retirement until the tree has the real uid. The delete and the replace
pass whether they found the file open. Opening the cache removes a tag
whose bytes are gone, which also covers a crash between an eviction's
two removals. The simulations wait for `Daemon::drained`, which counts
the landing, before they go offline.

B255 and B256 move to fixed: the account run checked both.
2026-10-27 14:44:34 +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 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 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 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 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 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 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 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 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