A plain rowid is the largest one plus one, so a trash queued in the same
transaction that dropped the newest op took its id. The drain retires the
op it sent by id, so a rename landing deleted the trash queued over it and
the file stayed on Drive (B137). Schema 37 rebuilds pending_op with
AUTOINCREMENT, keeping every id and index.
First step of phase 2 of docs/MILESTONE-3.0.0.md. Schema 36 rebuilds
`nodes` with `lid INTEGER PRIMARY KEY AUTOINCREMENT`. Each row takes its
old rowid as its lid, so the search index, keyed by rowid, stays valid.
AUTOINCREMENT keeps a deleted row's lid from coming back. `uid` stays
unique and becomes nullable, for nodes that are not on Drive yet.
A landed create's row now takes the real uid in place, keeping its lid.
When a listing had already stored the real node, its content is kept
under the placeholder's lid, which the node was known by first.
Nothing reads the lid yet apart from `Db::lid_of`.
The list and grid view from My files moves into widgets/file_list.rs as
FileList, so Trash, Shared with me and Shared by me share its columns,
selection, context menus, keyboard handling and list/grid switch instead of
each drawing its own rows.
Trash shows where each item came from and when it was deleted. Proton does
not expose the trash time through the SDK yet, so the daemon records when it
first sees a node in the trash (migration V35, trash_seen) and reports that;
the date becomes exact once proton-sdk-rs exposes Node::trash_time. Restoring
offers a Show action, and opening a trashed item explains that it needs to be
restored first.
Shared with me splits files and saved links into two views. Files show who
shared them, with a warning pill for unverified senders, the access they give
and when they were shared. Pending invitations sit in a banner that opens a
dialog to accept or decline each one. Several items can be left at once.
Saved links get their own list with open, copy and remove actions.
Shared by me gains a filter (all, links, people, expired links) and columns
for location, people and link state, with a copy button for live links.
Several items can stop being shared at once, the page refreshes after the
share dialog closes, and a reload rebinds only the rows that changed so the
selection survives.
All catalogs are updated and complete.
A 64-bit difference hash of each thumbnail, stored in the new
photos.similar_hash column (schema 34), and the queries the similar-photo
finder needs: what is left to hash, progress, and the sets of shots that
look alike. Sets made only of byte-identical copies are left to the
duplicate finder. A refresh keeps a hash unless the photo's bytes changed.
The daemon pass, the control request and the GUI are not built yet; see
PLAN.md.
Photos now know where they were taken. proton-drive-rs 0.7.0 reads the
Location from a photo's extended attributes, and the timeline refresh stores
it with the photo (schema v33). The migration marks every photo unresolved
once, so the next refresh backfills locations from node metadata without
downloading anything.
Towns are looked up offline: pdfs-core bundles GeoNames' cities15000 table,
trimmed by scripts/geonames-cities.py, and files each photo under the
nearest town within 50 km. The new PhotoPlaces and PlacePhotos requests list
the towns and page one town's photos.
The Photos page gets a third view, Places, with a card per town (newest
photo, town, country, count). A place opens in the gallery like an album.
Country names come translated from the iso-codes catalogs, now an optional
dependency. The About dialog credits GeoNames (CC BY 4.0).
Videos without a server thumbnail fell through to the local fallback,
which downloaded the whole file, failed to decode it as an image and
wrote the video off for good. Grab one frame with ffmpeg instead, a
second in or from the start for shorter clips. Probe for ffmpeg before
downloading, bound every run, and skip videos over 256 MiB.
Migration V32 gives the videos written off so far another try.
Those videos had no server thumbnail because our uploads never sent one.
Every upload path (bulk upload, drained mount writes, conflict copies,
new revisions and folder sync) now attaches a thumbnail rendered from
the local file, kept under the 60 KiB Proton's own clients use. Files
that are not images or videos, or cannot be decoded, upload bare as
before; a .ts file only counts as video with an MPEG-TS sync byte.
ffmpeg is a new optional dependency in the PKGBUILDs.
User-visible strings, doc comments and docs now say "Favorite" and
"color", matching the `pdfs favorite` command; UI_UX_PLAN.md §8 records
the choice. The changelog's 1.11.x and Unreleased entries said schema 30,
but V31 (`photos.resolved_at`) shipped in 1.11.0. Adds the missing
release links for 1.10.0 to 1.11.1.
A timeline refresh resolved every photo's node to learn its name, media
type, favourite tag, content hash and photo relation — 63 round-trips for
a 12,445-photo library, minutes of work, repeated every time the timeline
went stale sixty seconds later. None of that metadata changes on its own,
and when it does change the event feed names the node it changed.
`photos.resolved_at` (schema v31) records that a photo's node was read.
The refresh resolves the photos without that mark: the ones it has never
read, and the ones a remote event marked stale. An existing timeline is
backfilled as resolved, so upgrading does not re-read the library once.
Remote changes now name what they changed instead of asking for
everything. `classify_photo_events` turns a `NodeUpdated` into
`photos_mark_unresolved(uid)`, so a favourite toggled on a phone costs one
node read rather than the whole library; a local toggle is claimed as our
own echo, because the feed replays it and a `NodeUpdated` carries nothing
that says what changed. A continuity loss and `refresh photos --full` drop
every mark, which is the way back to a whole-library read.
Along the way: `photos_replace` never kept a photo's name, so a photo the
SDK could not decrypt was renamed to nothing on every refresh — and the
tab split and the RAW+JPEG grouping are both derived from the name. It is
kept now, and a row that *was* resolved is authoritative instead, so a
photo unlinked from its main elsewhere finally loses its `main_uid`.
`TIMELINE_ENRICH_CHUNK` is 150, matching the SDK's own `MAX_BATCH_COUNT`:
a chunk of 200 was split there into a full request and a 50-uid tail.
A camera that writes both puts two files on the volume, and the timeline showed
two tiles of the same photo. They are one shot to the person who took it.
Schema v30 records what ties them together: the server's own duplicate hash and
main-photo relation, plus a `group_key` every member of a shot carries. The key
is computed when the timeline is replaced, in three steps — the server relation
first, because it knows about live photos and bursts and we do not have to guess
at them; then same capture day, same file-name stem and one member raw while the
other is not, which is the pair a camera writes; otherwise the photo is its own
group. The member the grid shows is the non-raw one, since a JPEG decodes in
milliseconds and is what the person expects to see.
The gallery therefore pages by group and carries `group_size` and `has_raw` on
each entry, and the date scrubber counts what the page shows. The Raw tab is the
exception and still lists every raw file, because that tab exists to answer
"which raws do I have". Album pages are not grouped at all: an album is a list
someone made, and it shows exactly what they put in it.
`TimelineRow` is a struct now. It had grown to five positional fields and was
about to grow to seven, at which point nobody can read a call site.
The trash is a flat list of uids, but a delete has a shape: a folder, and
often items inside it that were trashed separately beforehand. Restoring
one of those in isolation is not what it means — a folder came back
without the contents deleted with it, and a file came back inside a
folder that is still trashed, which is to say nowhere the user can see.
- Schema 29: `trash.parent_uid`, filled by the trash refresh from the
node's parent, plus an index for the walk.
- `expand_restore` takes the connected piece of the trashed tree around
what was asked for (every trashed descendant, every trashed ancestor)
and orders it into waves, shallowest first: the server cannot put a
node back under a parent that is still trashed. A uid the listing does
not know about is restored on its own, because the listing is
materialised in chunks and can be stale.
- `Core::restore` restores wave by wave, still streaming per-node
outcomes, so one node the server refuses leaves the rest restored.
The SDK bump brings the 6cbf2f44 port: permanent API errors are no
longer retried on block transfers, expired block URLs are re-minted on
the whole-file and public-link download paths, and a server-supplied
Retry-After is capped at 120 s.
- Durable scratch/staging publication and preservation on failure.
- Transactional pending-operation supersession.
- Conflict copies cannot overwrite older copies or lose the source.
- Single-entry wipe protection and incomplete-scan aborts.
- Failed local deletion retains its baseline.
- Downloads are fsynced before baseline settlement.
- Future database schemas are refused.
- Event cursors advance only after successful invalidation and persistence.
- IPC frame, timeout, and concurrency limits.
- Atomic config saves; malformed configs are preserved.
- Sensitive directories require correct ownership, real directories, and 0700.
- Debian package now includes service and tray autostart files.
- Release workflow enforces versions, tests, lint, formatting, and offline acceptance.
- CI now runs offline filesystem acceptance.