A finished merge drained the outbox and started a rescan beside it. The scan raced the upload: on a folder library the 800 MB copy was still running when the folder was listed, on Nextcloud the upload takes minutes, and either way the listing lacked the composite, recorded the folder's validator, and nothing looked again until the next sync pass. The job now reports what the catalog needs (the name, the size of the picture it opens on, the capture time it wrote into the DNG) and the library writes the row at once, in one transaction, keyed where the scan will list the file; a composite with no time of its own takes its sources' earliest. The grid reloads and shows it beside its sources. The upload then gives the row what only the server knows: after sending a file the catalog already has a row for, it lists the folder once, takes the file id the server assigned, and records it (and, once the merge makes them, the thumbnails waiting beside the payload) under that id. Every drain now rescans when something landed in the library, after the upload rather than beside it. A second merge of the same frames is no longer named over the first: the name is checked against the catalog's names in that folder, which is all that knows it once the outbox is empty.
155 lines
5.3 KiB
Rust
155 lines
5.3 KiB
Rust
//! TRACES: FR-CAT-1 | FR-CAT-4 | FR-NC-3 | NFR-P9
|
|
//! Opening a remote library: scan → catalog → grid.
|
|
//!
|
|
//! This is the wire between three pieces that already worked separately —
|
|
//! `dr_sync::scan` walks the tree, `dr_catalog` indexes it, and
|
|
//! `dr_decode::preview` turns bytes into pixels. Until now the "Open library"
|
|
//! button logged its intent and stopped.
|
|
//!
|
|
//! # Threading
|
|
//!
|
|
//! Slint's event loop is single-threaded and must never block (NFR-P9), so
|
|
//! every network and decode operation runs on a worker thread and results
|
|
//! return through an mpsc channel drained by a Slint timer. That is the same
|
|
//! shape the login flow uses; it is repeated rather than shared because the
|
|
//! message types differ and a generic version would obscure both.
|
|
//!
|
|
//! # Why thumbnails are fetched, not derived from the scan
|
|
//!
|
|
//! A scan yields paths and sizes, nothing visual. Each thumbnail costs its own
|
|
//! range request, so they are fetched **only for cells the grid actually
|
|
//! wants** — never for the whole library up front. On the reference library
|
|
//! that is the difference between a few MB and ~370 GB (ARCH §6.7).
|
|
//!
|
|
//! Split into one module per area (docs/dev/code-health.md CH-1): filtering,
|
|
//! sidecar writes, on-disk paths, scanning, XMP, thumbnail fetch and
|
|
//! generation, the background sweeps, and reading the grid's cells and spans.
|
|
//! `mod.rs` re-exports every module's public items so nothing outside
|
|
//! `library` needs to change.
|
|
|
|
mod cells;
|
|
mod composite;
|
|
mod filters;
|
|
mod paths;
|
|
mod scan;
|
|
mod sidecar;
|
|
mod spans;
|
|
mod sweep;
|
|
mod thumbnails_fetch;
|
|
mod thumbnails_gen;
|
|
mod xmp;
|
|
|
|
pub use cells::*;
|
|
pub use composite::*;
|
|
pub use filters::*;
|
|
pub use paths::*;
|
|
pub use scan::*;
|
|
pub use sidecar::*;
|
|
pub use spans::*;
|
|
pub use sweep::*;
|
|
pub use thumbnails_fetch::*;
|
|
pub use thumbnails_gen::*;
|
|
pub use xmp::*;
|
|
|
|
/// What narrows the grid, when something does.
|
|
///
|
|
/// A collection is a set of originals the photographer files; an album is the
|
|
/// photographs whose exports went into one folder. The grid reads either the
|
|
/// same way — see `spans::scope_clause`, the one place the difference is
|
|
/// spelled — and the whole library is `None`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Scope {
|
|
Collection(dr_types::CollectionId),
|
|
Album(dr_catalog::AlbumId),
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) mod test_support {
|
|
//! Fixtures shared with the modules that build on this one's catalog
|
|
//! layout -- `repairs`, whose work lists are read over the same `images`
|
|
//! and `remote` rows a scan writes.
|
|
use dr_catalog::Catalog;
|
|
use dr_sync::{RemoteEntry, RemoteId, RemotePath};
|
|
|
|
use super::scan::persist;
|
|
|
|
pub fn entry(path: &str, file_id: u64, size: u64) -> RemoteEntry {
|
|
RemoteEntry {
|
|
id: RemoteId::Stable(file_id),
|
|
path: RemotePath::new(path),
|
|
kind: dr_sync::EntryKind::File,
|
|
validator: dr_sync::Validator::new("v"),
|
|
size,
|
|
modified: None,
|
|
has_preview: false,
|
|
materialised: true,
|
|
}
|
|
}
|
|
|
|
/// Write a scan's listing of `images` under `root`, as a scan would.
|
|
pub fn scanned_listing(catalog: &Catalog, root: &str, images: Vec<RemoteEntry>) {
|
|
persist(
|
|
catalog,
|
|
root,
|
|
&dr_sync::ScanResult {
|
|
images,
|
|
directories: Vec::new(),
|
|
progress: Default::default(),
|
|
sidecars: Vec::new(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// A catalog with `n` images, ready to file into collections.
|
|
pub fn with_images(n: usize) -> Catalog {
|
|
let catalog = Catalog::in_memory().unwrap();
|
|
let images: Vec<RemoteEntry> = (0..n)
|
|
.map(|i| entry(&format!("PhotosRaw/img{i:03}.CR2"), i as u64, 1000))
|
|
.collect();
|
|
let result = dr_sync::ScanResult {
|
|
images,
|
|
directories: vec![(RemotePath::new("PhotosRaw"), dr_sync::Validator::new("e"))],
|
|
progress: Default::default(),
|
|
sidecars: Vec::new(),
|
|
};
|
|
persist(&catalog, "PhotosRaw", &result).unwrap();
|
|
catalog
|
|
}
|
|
|
|
/// A catalog with `n` scanned images, none trashed.
|
|
///
|
|
/// Shared with `spans`, whose collection-membership tests build on the
|
|
/// same "n images, no folders beyond the root" catalog `cells`'s trash
|
|
/// tests do.
|
|
pub fn scanned(n: u64) -> Catalog {
|
|
let catalog = Catalog::in_memory().unwrap();
|
|
let images = (1..=n)
|
|
.map(|i| entry(&format!("PhotosRaw/IMG_{i:04}.CR2"), 1000 + i, 30_000_000))
|
|
.collect();
|
|
persist(
|
|
&catalog,
|
|
"PhotosRaw",
|
|
&dr_sync::ScanResult {
|
|
images,
|
|
directories: vec![(RemotePath::new("PhotosRaw"), dr_sync::Validator::new("e1"))],
|
|
progress: Default::default(),
|
|
sidecars: Vec::new(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
catalog
|
|
}
|
|
|
|
pub fn image_ids(catalog: &Catalog) -> Vec<dr_types::ImageId> {
|
|
let mut stmt = catalog
|
|
.connection()
|
|
.prepare("SELECT id FROM images ORDER BY source_ref")
|
|
.unwrap();
|
|
stmt.query_map([], |r| Ok(dr_types::ImageId(r.get::<_, i64>(0)? as u64)))
|
|
.unwrap()
|
|
.map(Result::unwrap)
|
|
.collect()
|
|
}
|
|
}
|