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>
This commit is contained in:
@@ -268,6 +268,16 @@ fn spawn_login(weak: slint::Weak<AppWindow>, ctl: Rc<LaunchController>, server:
|
||||
// as "failed unexpectedly", losing the one piece of information that
|
||||
// would explain the failure. Catch it and forward the message instead.
|
||||
let panic_tx = tx.clone();
|
||||
// Logging is unreliable here: android_logger delivers lines emitted
|
||||
// during startup but nothing from this thread, so a failure that never
|
||||
// sends is otherwise completely opaque. Reporting the last step reached
|
||||
// through the channel puts it on screen, which is the one channel known
|
||||
// to work.
|
||||
let step_tx = tx.clone();
|
||||
let step = |s: &str| {
|
||||
let _ = step_tx.send(LoginMessage::Progress(s.to_string()));
|
||||
};
|
||||
step("thread started");
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
|
||||
// `enable_all()` also enables the signal driver, which wants to own
|
||||
// process-wide signal handling and is not something a worker thread
|
||||
@@ -280,12 +290,13 @@ fn spawn_login(weak: slint::Weak<AppWindow>, ctl: Rc<LaunchController>, server:
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
let _ = tx.send(LoginMessage::Failed(e.to_string()));
|
||||
let _ = tx.send(LoginMessage::Failed(format!("tokio runtime: {e}")));
|
||||
return;
|
||||
}
|
||||
};
|
||||
step("runtime built");
|
||||
|
||||
run_login_flow(rt, tx, server);
|
||||
run_login_flow(rt, tx, server, &step);
|
||||
}));
|
||||
|
||||
if let Err(panic) = result {
|
||||
@@ -308,16 +319,19 @@ fn run_login_flow(
|
||||
rt: tokio::runtime::Runtime,
|
||||
tx: std::sync::mpsc::Sender<LoginMessage>,
|
||||
server: String,
|
||||
step: &dyn Fn(&str),
|
||||
) {
|
||||
{
|
||||
rt.block_on(async {
|
||||
step("building http client");
|
||||
let client = match dr_sync_nextcloud::http_client("DarkRoom") {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
let _ = tx.send(LoginMessage::Failed(e.to_string()));
|
||||
let _ = tx.send(LoginMessage::Failed(format!("http client: {e}")));
|
||||
return;
|
||||
}
|
||||
};
|
||||
step("requesting login flow");
|
||||
|
||||
log::info!("POST {server}/index.php/login/v2");
|
||||
let flow = match auth::begin(&client, &server, "DarkRoom").await {
|
||||
@@ -361,6 +375,8 @@ fn run_login_flow(
|
||||
}
|
||||
|
||||
enum LoginMessage {
|
||||
/// The last step the worker reached, for diagnosis when it dies silently.
|
||||
Progress(String),
|
||||
AwaitingApproval(String),
|
||||
Success(Box<(dr_sync_nextcloud::AppCredentials, String)>),
|
||||
Failed(String),
|
||||
@@ -374,6 +390,9 @@ fn poll_channel(
|
||||
) {
|
||||
let timer = slint::Timer::default();
|
||||
let ctl_for_cb = ctl.clone();
|
||||
// Remembers the worker's last reported step, so a silent death names the
|
||||
// point it got to rather than saying nothing.
|
||||
let last_step = RefCell::new(String::from("nothing"));
|
||||
|
||||
timer.start(
|
||||
slint::TimerMode::Repeated,
|
||||
@@ -398,9 +417,10 @@ fn poll_channel(
|
||||
// sending anything, which `catch_unwind` in
|
||||
// spawn_login should now prevent — so say that the
|
||||
// worker stopped rather than blaming the sign-in.
|
||||
ctl.model
|
||||
.borrow_mut()
|
||||
.fail("the sign-in worker stopped without reporting why");
|
||||
ctl.model.borrow_mut().fail(format!(
|
||||
"the sign-in worker stopped after: {}",
|
||||
last_step.borrow()
|
||||
));
|
||||
render(&w, ctl);
|
||||
}
|
||||
if let Some(t) = ctl.poll_timer.borrow().as_ref() {
|
||||
@@ -412,6 +432,9 @@ fn poll_channel(
|
||||
|
||||
let mut done = false;
|
||||
match msg {
|
||||
LoginMessage::Progress(s) => {
|
||||
*last_step.borrow_mut() = s;
|
||||
}
|
||||
LoginMessage::AwaitingApproval(url) => {
|
||||
ctl.model.borrow_mut().await_approval(url);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user