wip: ingest

This commit is contained in:
2026-08-22 15:34:43 +02:00
parent 735683b849
commit 1526c957cf
9 changed files with 1570 additions and 26 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ pub use colour::{Chromaticities, Transfer};
pub use selector::{ColourLabel, DateSelector, FlagState, Selector, Tier};
pub use settings::{
CacheSettings, CollisionPolicy, ColourSpace, DevelopSettings, ExportFormat, ExportSettings,
ExportTarget, OutputSharpening, Settings, SizingMode,
ExportTarget, ImportSettings, OutputSharpening, Settings, SizingMode,
};
pub use time::{civil_from_unix, civil_from_unix_at, format_date, Civil};
+104
View File
@@ -52,6 +52,72 @@ pub struct Settings {
pub cache: CacheSettings,
pub export: ExportSettings,
pub develop: DevelopSettings,
pub import: ImportSettings,
}
// ---------------------------------------------------------------------------
// Import
// ---------------------------------------------------------------------------
/// TRACES: FR-CAT-10 | FR-NC-7a
/// What an import defaults to, remembered between cards.
///
/// A photographer imports the same way every time — same folders, same
/// destination, same answer about the card — and retyping a template on every
/// card is the kind of friction that makes people stop using the importer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ImportSettings {
/// The folder layout (FR-NC-7a).
///
/// **Empty means the layout module's default**, rather than this file
/// repeating `{yyyy}/{yyyy}-{mm}-{dd}`. Two spellings of a default is one
/// that goes stale, and the expander is where the token vocabulary is
/// defined — so it owns the default too, and this records only a
/// deliberate departure from it.
pub folder_template: String,
/// Where originals are copied to on this machine. Empty until chosen.
///
/// A local folder even on a library that lives on a server: FR-NC-7b has
/// the bytes land and be verified here first, and the upload follow.
pub destination: String,
/// The optional second copy (FR-CAT-10). Empty means none.
pub backup: String,
/// Whether to send imported originals on to the server (FR-NC-7b).
pub upload: bool,
/// Whether the card is to be emptied once everything is confirmed.
///
/// **Defaults to false and is the one setting worth not remembering
/// eagerly.** A move-import is the only irreversible thing this
/// application does, and a preference that quietly persists "yes, erase
/// the card" across sessions turns a considered decision into a default.
pub move_from_card: bool,
/// Whether each copy is read back and compared (FR-CAT-10).
pub verify: bool,
/// Whether photographs already in the catalog are skipped (FR-CAT-11).
pub skip_duplicates: bool,
}
/// Written out rather than derived, because three of these must not be
/// `false`.
///
/// `#[serde(default)]` uses this for a field a stored file is missing *and*
/// for the whole section when a settings file predates it — which every
/// existing file does. A derived `Default` would silently turn verification
/// off and duplicate skipping off for every user who already has one.
impl Default for ImportSettings {
fn default() -> Self {
Self {
folder_template: String::new(),
destination: String::new(),
backup: String::new(),
upload: true,
// The one irreversible thing this application does starts off.
move_from_card: false,
verify: true,
skip_duplicates: true,
}
}
}
// ---------------------------------------------------------------------------
@@ -1014,3 +1080,41 @@ mod tests {
assert_eq!(budget::label(None), "Unlimited");
}
}
#[cfg(test)]
mod import_settings_tests {
use super::*;
#[test]
fn an_existing_settings_file_gains_the_safe_defaults() {
// Every settings file on disk predates this section. Coming back with
// verification off would silently remove the check that catches a
// dying card, on precisely the machines that have been running
// longest.
let stored = r#"{"cache":{},"export":{},"develop":{}}"#;
let s: Settings = serde_json::from_str(stored).unwrap();
assert!(s.import.verify);
assert!(s.import.skip_duplicates);
assert!(s.import.upload);
// And the irreversible one stays off.
assert!(!s.import.move_from_card);
}
#[test]
fn an_unset_template_defers_to_the_layout_rather_than_repeating_it() {
// Empty is meaningful: it means "whatever the expander's default is",
// so the default lives in exactly one place.
assert_eq!(Settings::default().import.folder_template, "");
}
#[test]
fn a_deliberate_choice_survives_a_round_trip() {
let mut s = Settings::default();
s.import.folder_template = "{yyyy}/{make}".into();
s.import.move_from_card = true;
s.import.verify = false;
let text = serde_json::to_string(&s).unwrap();
let back: Settings = serde_json::from_str(&text).unwrap();
assert_eq!(back.import, s.import);
}
}