Merge: answer Android's memory warnings, and stop reporting a lost root as an empty library
FR-PLAT-AND-5 in full, FR-PLAT-AND-2 in part -- the recovery is built and live for Nextcloud roots, the SAF cause it names does not exist yet. FR-PLAT-AND-4 and FR-PLAT-AND-6 are not here, both blocked behind the same gap: assemble-apk.sh compiles no Java, so the APK cannot carry a Service or a FileProvider. The container has JDK 17 and build-tools 36; the build step is what is missing. Verified: fmt, clippy --workspace --all-targets -D warnings, and 1043 tests across dr-catalog, dr-sync, dr-sync-folder, dr-sync-nextcloud, dr-plat and dr-ui. The aarch64 target was checked before the branch was finished but not after; no device was available. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,7 +65,7 @@ pub use query::{Query, Sort};
|
||||
pub use rating::{Judgement, MAX_RATING};
|
||||
pub use scan::{DirAction, DirState, EntryAction, ScanOutcome};
|
||||
pub use trash::{TrashedImage, TRASH_DIR};
|
||||
pub use walk::{ensure_root, scan_root, RootKind, ScanProgress, ScanReport};
|
||||
pub use walk::{ensure_root, mark_root_offline, scan_root, RootKind, ScanProgress, ScanReport};
|
||||
|
||||
/// One row of the library grid.
|
||||
///
|
||||
|
||||
@@ -432,7 +432,7 @@ fn bump_generation(conn: &Connection, root: RootId, now: i64) -> Result<i64, Cat
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-9
|
||||
/// TRACES: FR-CAT-9 | FR-PLAT-AND-2
|
||||
/// Mark every image under a root as unreachable.
|
||||
///
|
||||
/// The other half of FR-CAT-9's distinction: a source *proven absent* may leave
|
||||
@@ -445,14 +445,38 @@ fn bump_generation(conn: &Connection, root: RootId, now: i64) -> Result<i64, Cat
|
||||
/// the root now claims something about the files that is no longer known to be
|
||||
/// true, and only reading the directories again can settle it. Pruning would
|
||||
/// skip them all and leave a plugged-in library showing as offline forever.
|
||||
fn mark_root_offline(conn: &Connection, root: RootId) -> Result<(), CatalogError> {
|
||||
///
|
||||
/// # Why the ETag goes with the mtime
|
||||
///
|
||||
/// The three columns are the same fact told by three kinds of storage: a local
|
||||
/// directory proves it is unchanged with its mtime and entry count, and a
|
||||
/// remote one proves it with a propagating ETag (ARCH §6.6). Clearing two of
|
||||
/// them and leaving the third would disarm the re-listing on exactly the
|
||||
/// libraries this is most likely to be called for — a remote scan prunes on
|
||||
/// the ETag alone, so a root that came back would be walked, found unchanged
|
||||
/// at every level, pruned whole, and left with every row still marked offline
|
||||
/// and nothing that would ever clear the mark.
|
||||
///
|
||||
/// # Public, because losing a root is not only the local walk's business
|
||||
///
|
||||
/// This began as the private end of [`scan_root`]'s root-failure branches,
|
||||
/// which is the only route a library reached through [`Storage`] can take.
|
||||
/// The application does not currently take that route at all: it opens
|
||||
/// libraries through `dr-sync`'s connectors, so the discovery happens in a
|
||||
/// crate that cannot see this one's internals, and the correct response is
|
||||
/// identical (FR-PLAT-AND-2). Exported rather than reimplemented beside the
|
||||
/// caller that found out — a second copy would be a second thing to remember
|
||||
/// when the ETag rule below changes.
|
||||
///
|
||||
/// [`Storage`]: dr_plat::Storage
|
||||
pub fn mark_root_offline(conn: &Connection, root: RootId) -> Result<(), CatalogError> {
|
||||
let root_id = root.0 as i64;
|
||||
conn.execute(
|
||||
"UPDATE images SET availability = ?1 WHERE root_id = ?2 AND availability != ?1",
|
||||
rusqlite::params![availability_code(Availability::Offline), root_id],
|
||||
)?;
|
||||
conn.execute(
|
||||
"UPDATE folders SET mtime = NULL, entry_count = NULL WHERE root_id = ?1",
|
||||
"UPDATE folders SET mtime = NULL, entry_count = NULL, etag = NULL WHERE root_id = ?1",
|
||||
[root_id],
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -995,6 +1019,40 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// TRACES: FR-PLAT-AND-2 | FR-CAT-9
|
||||
#[test]
|
||||
fn marking_a_root_offline_forgets_the_remote_validator_too() {
|
||||
// The half of the marking that only a remote library can notice, and
|
||||
// the reason it has to be here rather than beside the connector: a
|
||||
// remote scan prunes on the propagating ETag alone (ARCH §6.6). Clear
|
||||
// the local mtime and leave the ETag standing and a library that came
|
||||
// back would be walked, found unchanged at every level, pruned whole,
|
||||
// and left with every row still marked offline — with nothing that
|
||||
// would ever clear the mark, because clearing it is something only a
|
||||
// listing can do.
|
||||
//
|
||||
// Written directly because this module never writes an ETag; it is
|
||||
// `ui/dr-ui/src/library.rs`'s scan that does, against the same table.
|
||||
let lib = Library::new("etag-forgotten");
|
||||
lib.file("2026/IMG.CR3", b"raw");
|
||||
lib.scan();
|
||||
lib.conn()
|
||||
.execute(
|
||||
"UPDATE folders SET etag = 'e1' WHERE root_id = ?1",
|
||||
[lib.root.0 as i64],
|
||||
)
|
||||
.expect("etag");
|
||||
assert!(lib.count("SELECT COUNT(*) FROM folders WHERE etag IS NOT NULL") > 0);
|
||||
|
||||
mark_root_offline(lib.conn(), lib.root).expect("mark");
|
||||
|
||||
assert_eq!(
|
||||
lib.count("SELECT COUNT(*) FROM folders WHERE etag IS NOT NULL"),
|
||||
0,
|
||||
"an unreachable library must be re-listed, not pruned as unchanged"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_root_that_comes_back_is_available_again() {
|
||||
// The other half: a drive plugged back in must return the library to
|
||||
|
||||
Reference in New Issue
Block a user