Files
DarkRoom/Cargo.toml
T
dtourolleandClaude Opus 5 fa12afed18
Build and test / Desktop (Linux) (push) Failing after 1s
Build and test / Android (aarch64) (push) Failing after 0s
Build and test / Layer separation (push) Failing after 1s
Traceability / Requirement traces (push) Failing after 2s
Keep originals on this device, by pin and by use
Fills in `image_cache`, which the previous commit's "On this device" filter
read but nothing wrote. Also carries in-flight work that shared these files:
the Android TLS root store, the settings page, and a regenerated
traceability report.

# Two populations, deliberately separate

An original is kept here for one of two reasons, and conflating them produces
the exact failure the feature exists to prevent.

**Pinned** originals were asked for. Pinning a collection before a trip is a
promise, so pinned rows are never evicted and never counted against the
budget — a cap that could silently delete a pinned trip would make pinning
worthless, because it could not be relied on without checking.

**Passively cached** originals are a side effect of working: develop already
downloads the whole file, so keeping it costs no bandwidth and saves the
entire transfer next time. This population is what the budget bounds, evicted
least-recently-used, because it otherwise grows until a day of culling fills
a disk.

Sharing one budget would let a large pin starve the passive cache, or let
browsing evict a pin. They are separate.

# What was built

`dr_catalog::cache` owns the bookkeeping — held tier, size, last use, pinned
— and writes the bytes; deciding to download stays with the caller, which is
what keeps a crate with no network out of the network's business. Files are
written to a temporary and renamed, so a dropped connection cannot leave a
truncated file recorded as a complete original. They are named by image id,
not filename: `Photos/IMG_0001.CR2` and `Trips/IMG_0001.CR2` are different
photographs, and a flat cache keyed on the name would serve one for the other.

`spawn_full_fetch` became read-through. A hit is a disk read; a miss stores
what it downloads and enforces the budget. A cache that cannot be opened is a
miss, not a failure to open the photograph.

Pinning writes intent — `tier_desired` — without downloading, so the button
responds immediately, and `spawn_pin_fetch` fills it in sequentially
afterwards. Sequential because these are tens of megabytes each: the lanes
that make the thumbnail sweep fast buy little against one connection's
bandwidth and cost a great deal of memory. A pin interrupted by a lost
connection resumes from where it stopped.

Schema v5 adds `pinned` and `path`. `pinned` is a column rather than something
inferred from `pinned_by_rule`, which is ON DELETE SET NULL and so cannot
answer for an image whose rule was deleted. A v4 catalog migrates in place;
existing rows default to unpinned, the safe direction.

The budget and "keep opened originals" come from the settings page rather than
a constant, and are applied at startup rather than only on change — a cache
capped at 2 GB last session would otherwise spend this one filling to the
default. Turning off keeping leaves what is already cached readable: those
bytes are paid for, and refusing them would re-download images sitting right
there, including pinned ones.

Also removes a doubled `#[test]` introduced in the previous commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:12:01 +02:00

157 lines
6.3 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[workspace]
resolver = "2"
members = [
"core/dr-types",
"core/dr-catalog",
"core/dr-thumbs",
"core/dr-decode",
"core/dr-gpu",
"core/dr-lens",
"core/dr-pipeline",
"core/dr-sync",
"core/dr-sync-nextcloud",
"platform/dr-plat",
"ui/dr-ui",
"apps/darkroom-desktop",
"apps/darkroom-android",
"tools/traceability",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.92"
license = "GPL-3.0-or-later"
repository = "https://github.com/dtourolle/DarkRoom"
[workspace.dependencies]
# Internal
dr-types = { path = "core/dr-types" }
dr-catalog = { path = "core/dr-catalog" }
dr-thumbs = { path = "core/dr-thumbs" }
dr-decode = { path = "core/dr-decode" }
dr-gpu = { path = "core/dr-gpu" }
dr-lens = { path = "core/dr-lens" }
dr-pipeline = { path = "core/dr-pipeline" }
dr-plat = { path = "platform/dr-plat" }
dr-sync = { path = "core/dr-sync" }
dr-sync-nextcloud = { path = "core/dr-sync-nextcloud" }
dr-ui = { path = "ui/dr-ui" }
# GPU + UI
wgpu = "23"
slint = { version = "1.9", default-features = false }
slint-build = "1.9"
# UI token codegen (S2): style.yaml -> theme.slint. serde_yaml was deprecated
# by its maintainer in 2024 and serde_yml, the first fork, has since been
# deprecated too; serde_norway is the fork still receiving releases. Its
# mappings preserve insertion order, which is what lets the generated Slint
# keep the token ordering the YAML author chose.
serde_norway = "0.9"
# Foundations
anyhow = "1"
thiserror = "2"
log = "0.4"
env_logger = "0.11"
pollster = "0.4"
# Networking — no mature Nextcloud crate exists; the connector is hand-rolled
# over reqwest (D7). reqwest_dav was evaluated and is too thin to build on.
# `rustls-no-provider` rather than `rustls`: the latter defaults to the
# aws-lc-rs crypto provider, whose aws-lc-sys crate is C and fails to
# cross-compile for Android — precisely the NDK pain D1 chose Rust to avoid.
# ring is pure Rust apart from a small asm core that does build under the NDK.
#
# `rustls-tls-webpki-roots-no-provider` rather than plain `rustls-no-provider`:
# the latter verifies against rustls-platform-verifier, which reaches the
# Android trust store over JNI and panics mid-handshake unless initialised from
# Java first — the crash D7 predicted and spike S3 exists to resolve properly.
# The panic surfaces inside tokio, which catches task panics itself, so it
# reaches the UI as a worker that stopped rather than as an error.
#
# webpki-roots is the escape hatch D7 records: a root store compiled into the
# binary, no JNI, identical on both platforms. The trade is real and belongs in
# S3's scope — user-installed and enterprise CAs are not consulted, and the
# roots go stale with the release rather than with the OS.
reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "webpki-roots", "stream", "json"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
quick-xml = "0.41"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] }
url = "2.5"
async-trait = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
base64 = "0.23"
# Platform secure storage: Secret Service on Linux, Keystore on Android
# (FR-NC-2). Credentials never touch the catalog or a plain file.
# keyring 4 restructured its features: `v1` is the default set and brings
# the zbus Secret Service backend, which is what GNOME Keyring and KWallet
# (via ksecretd) both speak.
keyring = { version = "4", features = ["v1"] }
# The Android half of the same project: a keyring-core CredentialStore backed
# by AndroidKeyStore AES-GCM over SharedPreferences (FR-PLAT-AND-1). It reads
# the JavaVM and Context from ndk-context, which android-activity populates
# before `android_main` runs, so no Kotlin shim of our own is needed.
#
# This is the keyring-core API, not the v1 `Entry` API the Linux path uses;
# the two impls are deliberately separate rather than sharing a code path.
android-native-keyring-store = "1.0.0"
keyring-core = "1"
# Decode. rawler is the pure-Rust decoder (D2); zune-jpeg decodes the
# embedded previews rawler extracts.
# Catalog. `bundled` compiles SQLite from source rather than linking the
# system library — the same cross-compilation reasoning as the TLS choice
# above: no system dependency to satisfy under the Android NDK.
#
# `backup` is not optional in practice: it is what takes a consistent snapshot
# of a live WAL database for upload. A filesystem copy of `catalog.sqlite`
# while a `-wal` exists beside it uploads a torn file.
rusqlite = { version = "0.40", features = ["bundled", "backup"] }
rawler = "0.7"
zune-jpeg = "0.4.21"
# Thumbnails are stored encoded, not as raw RGBA: a 256px RGBA buffer is
# ~256 KB against ~20 KB as JPEG, and the store syncs to Nextcloud where that
# 13× is transfer cost on every client. Pure Rust, no C dependency — the same
# criterion behind the TLS and SQLite choices above.
jpeg-encoder = "0.7"
bytemuck = { version = "1", features = ["derive"] }
# Lens correction profiles. A pure-Rust port of Lensfun rather than a binding
# to the C library, for the same cross-compilation reason as the TLS and
# SQLite choices above: liblensfun would be a third C dependency to satisfy
# under the Android NDK.
#
# The database ships *inside* the crate — 56 XML files, gzipped at build time
# and decompressed on first lookup. That matters beyond convenience: Android
# gives us no filesystem path (ARCH §6.9), so a database loaded from a
# system directory would have nowhere to live there.
#
# Licence: LGPL-3.0-or-later, which upgrades cleanly into our GPLv3 (D8).
# The upstream Lensfun *database* is CC-BY-SA and is redistributed by the
# crate; attribution belongs in the about screen.
#
# Caveat worth remembering: this is a third-party port at 0.7.0, not upstream
# Lensfun. Verified working against the bundled database (interpolation
# between calibration points, and an unknown lens returning empty rather than
# panicking), but the pipeline talks to it through its own profile types so
# swapping it out is not a pipeline change.
lensfun = "0.7"
[profile.dev]
# Dependencies optimised even in dev builds — wgpu and image decoding are
# unusably slow otherwise, and they rarely need debugging.
opt-level = 0
[profile.dev.package."*"]
opt-level = 2
[profile.release]
lto = "thin"
codegen-units = 1