Split library.rs into library/ by area of behaviour
library.rs was 7,729 lines wiring together everything "open a remote library" touches: scanning, pulling other devices' judgements out of sidecars found along the way, writing local edits back out to the sidecar outbox, pushing/reloading XMP by hand, fetching and prefetching thumbnails and originals, generating thumbnails locally, the metadata and thumbnail background sweeps, on-disk paths for the catalog and model files, and reading the grid's cells, spans and rating filter. Same motivation as the develop.rs split (docs/dev/code-health.md CH-1): a pure, no-behaviour-change move into one file per area, each under about 1,500 lines. Tracing actual call sites rather than trusting the file's physical layout mattered here: `persist`, `load_folder_etags`, `pull_sidecars`, `load_sidecar_etags`, `record_sidecar_read` and `apply_judgement` sit textually beside the XMP push/reload functions but are called only from `run_scan` (pulling a device's own past judgements out of the sidecars a scan just walked), so they went to scan.rs and not xmp.rs. `cells` came out at over 1,800 lines once its tests moved with it and split further into cells.rs (windowed reads, trash, ordinals) and spans.rs (collection scope, manual reordering, the capture-time histogram) -- ten submodules rather than the nine first planned. Previously-private items reached from a sibling module became `pub(super)`, narrower than the whole-crate reachability one file gave them. Tests moved with the code they test; the two test fixtures used across more than one file (`scanned`, and develop.rs's `session_with_a_left_half_subject` in the matching commit) joined the shared `test_support` module alongside the existing `entry`/ `with_images`/`image_ids` helpers. `mod.rs` re-exports every module's public items under `library::`, including the `pub(crate)` `test_support` module `repairs.rs` reads its fixtures from, so no file outside `library` needed a change. The previous commit split develop.rs the same way; taken alone it left dr-ui without library.rs, so that intermediate commit does not build on its own. This one restores it.
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
//! Pushing local judgements out to XMP sidecars, and reloading sidecars a
|
||||
//! person chose to trust by hand.
|
||||
|
||||
use dr_catalog::Catalog;
|
||||
use dr_sync::{Connection, RemoteBackend, RemoteId, RemotePath};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
/// TRACES: FR-CAT-13 | NFR-R4
|
||||
/// One image's ratings, label and keywords, on their way to the `.xmp`
|
||||
/// beside it.
|
||||
///
|
||||
/// Distinct from [`SidecarWrite`], which amends DarkRoom's own document
|
||||
/// through the cache and the outbox. This is best-effort in the other
|
||||
/// direction: the catalog and the `.drsc` are authoritative, the `.xmp` is a
|
||||
/// courtesy to whatever else reads the folder, and a write that cannot
|
||||
/// happen now is written again — from the catalog, whole — the next time
|
||||
/// anything about the photograph is judged. So nothing is queued.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct XmpWrite {
|
||||
pub image_path: String,
|
||||
pub record: dr_xmp::Xmp,
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-13 | NFR-R4
|
||||
/// Write each record into the XMP sidecar beside its image.
|
||||
///
|
||||
/// An existing sidecar of either spelling is rewritten in place, which is the
|
||||
/// whole point of `dr_xmp::rewrite`: only the properties DarkRoom owns move,
|
||||
/// and another application's settings, comments and namespaces come through
|
||||
/// byte for byte. A photograph with neither gets a new file under Lightroom's
|
||||
/// name. Reported once at the end, as the judgement writes are.
|
||||
pub fn spawn_xmp_writes(conn: Connection, writes: Vec<XmpWrite>) -> Receiver<XmpMessage> {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let mut written = 0usize;
|
||||
let mut failed = 0usize;
|
||||
let mut last_error = None;
|
||||
let rt = match crate::net_runtime::build() {
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
let _ = tx.send(XmpMessage::Finished {
|
||||
written: 0,
|
||||
failed: writes.len(),
|
||||
last_error: Some(e.to_string()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
rt.block_on(async {
|
||||
let backend = match crate::remote::connect(&conn) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
failed = writes.len();
|
||||
last_error = Some(e.to_string());
|
||||
return;
|
||||
}
|
||||
};
|
||||
for w in &writes {
|
||||
match write_one_xmp(&*backend, w).await {
|
||||
Ok(()) => written += 1,
|
||||
Err(e) => {
|
||||
log::warn!("xmp sidecar for {}: {e}", w.image_path);
|
||||
last_error = Some(e);
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let _ = tx.send(XmpMessage::Finished {
|
||||
written,
|
||||
failed,
|
||||
last_error,
|
||||
});
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
/// The outcome of a batch of XMP writes.
|
||||
#[derive(Debug)]
|
||||
pub enum XmpMessage {
|
||||
Finished {
|
||||
written: usize,
|
||||
failed: usize,
|
||||
last_error: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Read-modify-write one image's XMP sidecar on the server.
|
||||
pub(super) async fn write_one_xmp(backend: &dyn RemoteBackend, w: &XmpWrite) -> Result<(), String> {
|
||||
let [darktable, lightroom] = crate::xmp_sync::candidate_paths(&w.image_path);
|
||||
// Whichever exists is the one rewritten; neither existing means the
|
||||
// Lightroom spelling is created. An existing file this build cannot
|
||||
// parse is left alone rather than replaced — it is somebody else's
|
||||
// document, and a refusal is recoverable where an overwrite is not.
|
||||
let mut target = lightroom.clone();
|
||||
let mut existing: Option<String> = None;
|
||||
for path in [&darktable, &lightroom] {
|
||||
if let Ok(bytes) = backend
|
||||
.get(&RemoteId::Path(RemotePath::new(path.clone())), None)
|
||||
.await
|
||||
{
|
||||
if !bytes.is_empty() {
|
||||
target = path.clone();
|
||||
existing = Some(String::from_utf8_lossy(&bytes).into_owned());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let text = match existing {
|
||||
Some(text) => {
|
||||
// The file's caption, copyright and hierarchy come through: the
|
||||
// catalog has nowhere to keep them, and a rewrite that said
|
||||
// nothing about them would remove them.
|
||||
let theirs = dr_xmp::Xmp::parse(&text)
|
||||
.map_err(|e| format!("{target} is not a sidecar this build can read: {e}"))?;
|
||||
let mut record = w.record.clone();
|
||||
crate::xmp_sync::carry_through(&mut record, &theirs);
|
||||
record
|
||||
.rewrite(&text)
|
||||
.map_err(|e| format!("{target} is not a sidecar this build can rewrite: {e}"))?
|
||||
}
|
||||
None => w.record.to_text(),
|
||||
};
|
||||
backend
|
||||
.put(&RemotePath::new(target), text.into_bytes(), None)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-13
|
||||
/// The offered reload: re-read the sidecars a person chose to trust, with
|
||||
/// the sidecar winning. One fetch per path, the catalog opened on this
|
||||
/// thread as the scan opens it.
|
||||
pub fn spawn_xmp_reload(
|
||||
conn: Connection,
|
||||
root: String,
|
||||
catalog_path: PathBuf,
|
||||
paths: Vec<String>,
|
||||
) -> Receiver<XmpMessage> {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let mut written = 0usize;
|
||||
let mut failed = 0usize;
|
||||
let mut last_error = None;
|
||||
let outcome: Result<(), String> = (|| {
|
||||
let catalog = Catalog::open(&catalog_path).map_err(|e| e.to_string())?;
|
||||
let root_id: i64 = catalog
|
||||
.connection()
|
||||
.query_row(
|
||||
"SELECT id FROM roots WHERE label = ?1 AND kind = 'remote'",
|
||||
[&root],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rt = crate::net_runtime::build().map_err(|e| e.to_string())?;
|
||||
rt.block_on(async {
|
||||
let backend = crate::remote::connect(&conn).map_err(|e| e.to_string())?;
|
||||
for path in &paths {
|
||||
let fetched = backend
|
||||
.get(&RemoteId::Path(RemotePath::new(path.clone())), None)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|bytes| {
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
crate::xmp_sync::reload(catalog.connection(), root_id, path, &text)
|
||||
});
|
||||
match fetched {
|
||||
Ok(taken) => written += taken.changed,
|
||||
Err(e) => {
|
||||
log::warn!("reloading {path}: {e}");
|
||||
last_error = Some(e);
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})();
|
||||
if let Err(e) = outcome {
|
||||
failed = paths.len();
|
||||
last_error = Some(e);
|
||||
}
|
||||
let _ = tx.send(XmpMessage::Finished {
|
||||
written,
|
||||
failed,
|
||||
last_error,
|
||||
});
|
||||
});
|
||||
rx
|
||||
}
|
||||
Reference in New Issue
Block a user