Make the CI checks say what they mean, and format the workspace
The Android job's "Verify minimum API level" step has never verified the minimum API level. It took the first `*.so` anywhere under the target directory, which is a host proc-macro from debug/deps — an x86-64 object built by the runner's gcc, whose .comment section cannot mention Android and so can never contradict the expected value. It now reads the artifact under the target triple, compares against MIN_API parsed from the Dockerfile rather than a second copy of the number, and fails on a mismatch. Both sides are checked non-empty first: two failed parses would otherwise compare equal and pass, which is the same silent success in a new costume. The Android image installs one SDK package per layer and keeps the output. sdkmanager is a JVM program that aborts when it cannot get memory, and the single `> /dev/null` step reported that as a bare "exit code 134" while a retry re-downloaded everything that had already succeeded. tools/ci-local.sh runs all four jobs — desktop, android, layering, traceability — against the host toolchain, which is pinned to the same 1.92.0 CI installs. Its matrix check compares regeneration against the working tree rather than against HEAD: CI starts from a clean checkout, so git's answer is the right one there and reports every local run stale here. The rest is rustfmt across the workspace, and the clippy findings that surfaced once it did: manual_contains in dr-thumbs and collections_ui, a map iterated as pairs for its keys, an index loop over a slice, and two runtime assertions on a constant now made at compile time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+101
-22
@@ -156,7 +156,9 @@ impl Cache {
|
||||
let ext = source_ref
|
||||
.rsplit_once('.')
|
||||
.map(|(_, e)| e.to_ascii_lowercase())
|
||||
.filter(|e| !e.is_empty() && e.len() <= 8 && e.chars().all(|c| c.is_ascii_alphanumeric()))
|
||||
.filter(|e| {
|
||||
!e.is_empty() && e.len() <= 8 && e.chars().all(|c| c.is_ascii_alphanumeric())
|
||||
})
|
||||
.unwrap_or_else(|| "bin".to_string());
|
||||
format!("{}.{ext}", image.0)
|
||||
}
|
||||
@@ -257,7 +259,10 @@ impl Cache {
|
||||
// The file is gone but the row says it is here. Believing the
|
||||
// row would report the image as locally available for ever
|
||||
// while every open failed.
|
||||
log::debug!("cached original {} missing, forgetting it: {e}", abs.display());
|
||||
log::debug!(
|
||||
"cached original {} missing, forgetting it: {e}",
|
||||
abs.display()
|
||||
);
|
||||
self.forget(conn, &[image])?;
|
||||
Ok(None)
|
||||
}
|
||||
@@ -357,7 +362,11 @@ impl Cache {
|
||||
)?;
|
||||
let mut usage = Usage::default();
|
||||
let rows = stmt.query_map(rusqlite::params![Tier::Original.stored()], |r| {
|
||||
Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?, r.get::<_, i64>(2)?))
|
||||
Ok((
|
||||
r.get::<_, i64>(0)?,
|
||||
r.get::<_, i64>(1)?,
|
||||
r.get::<_, i64>(2)?,
|
||||
))
|
||||
})?;
|
||||
for row in rows {
|
||||
let (pinned, count, bytes) = row?;
|
||||
@@ -522,9 +531,7 @@ mod tests {
|
||||
rusqlite::params![format!("Photos/img{i:03}.CR2")],
|
||||
)
|
||||
.unwrap();
|
||||
ids.push(ImageId(
|
||||
catalog.connection().last_insert_rowid() as u64
|
||||
));
|
||||
ids.push(ImageId(catalog.connection().last_insert_rowid() as u64));
|
||||
}
|
||||
let dir = tempdir();
|
||||
let cache = Cache::open(&dir, budget).unwrap();
|
||||
@@ -557,9 +564,15 @@ mod tests {
|
||||
let (cat, cache, _dir, ids) = fixture(3);
|
||||
// 400 each against a 1000 budget: storing the third puts it 200 over.
|
||||
let bytes = vec![0u8; 400];
|
||||
cache.store(cat.connection(), ids[0], "a.CR2", &bytes, false, 10).unwrap();
|
||||
cache.store(cat.connection(), ids[1], "b.CR2", &bytes, false, 20).unwrap();
|
||||
cache.store(cat.connection(), ids[2], "c.CR2", &bytes, false, 30).unwrap();
|
||||
cache
|
||||
.store(cat.connection(), ids[0], "a.CR2", &bytes, false, 10)
|
||||
.unwrap();
|
||||
cache
|
||||
.store(cat.connection(), ids[1], "b.CR2", &bytes, false, 20)
|
||||
.unwrap();
|
||||
cache
|
||||
.store(cat.connection(), ids[2], "c.CR2", &bytes, false, 30)
|
||||
.unwrap();
|
||||
|
||||
// Touch the oldest so it is no longer the least recently used.
|
||||
cache.load(cat.connection(), ids[0], 40).unwrap();
|
||||
@@ -578,9 +591,15 @@ mod tests {
|
||||
let (cat, cache, _dir, ids) = fixture(3);
|
||||
let bytes = vec![0u8; 800];
|
||||
|
||||
cache.store(cat.connection(), ids[0], "a.CR2", &bytes, true, 10).unwrap();
|
||||
cache.store(cat.connection(), ids[1], "b.CR2", &bytes, false, 20).unwrap();
|
||||
cache.store(cat.connection(), ids[2], "c.CR2", &bytes, false, 30).unwrap();
|
||||
cache
|
||||
.store(cat.connection(), ids[0], "a.CR2", &bytes, true, 10)
|
||||
.unwrap();
|
||||
cache
|
||||
.store(cat.connection(), ids[1], "b.CR2", &bytes, false, 20)
|
||||
.unwrap();
|
||||
cache
|
||||
.store(cat.connection(), ids[2], "c.CR2", &bytes, false, 30)
|
||||
.unwrap();
|
||||
|
||||
cache.enforce(cat.connection()).unwrap();
|
||||
|
||||
@@ -597,10 +616,24 @@ mod tests {
|
||||
// pinned.
|
||||
let (cat, cache, _dir, ids) = fixture(2);
|
||||
cache
|
||||
.store(cat.connection(), ids[0], "a.CR2", &vec![0u8; 5000], true, 10)
|
||||
.store(
|
||||
cat.connection(),
|
||||
ids[0],
|
||||
"a.CR2",
|
||||
&vec![0u8; 5000],
|
||||
true,
|
||||
10,
|
||||
)
|
||||
.unwrap();
|
||||
cache
|
||||
.store(cat.connection(), ids[1], "b.CR2", &vec![0u8; 500], false, 20)
|
||||
.store(
|
||||
cat.connection(),
|
||||
ids[1],
|
||||
"b.CR2",
|
||||
&vec![0u8; 500],
|
||||
false,
|
||||
20,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Pinned use is far past the 1000 budget, but the passive 500 fits.
|
||||
@@ -617,7 +650,14 @@ mod tests {
|
||||
let (cat, cache, _dir, ids) = fixture_with(2, Budget::unlimited());
|
||||
for (i, id) in ids.iter().enumerate() {
|
||||
cache
|
||||
.store(cat.connection(), *id, "a.CR2", &vec![0u8; 100_000], false, i as i64)
|
||||
.store(
|
||||
cat.connection(),
|
||||
*id,
|
||||
"a.CR2",
|
||||
&vec![0u8; 100_000],
|
||||
false,
|
||||
i as i64,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(cache.enforce(cat.connection()).unwrap(), 0);
|
||||
@@ -692,7 +732,14 @@ mod tests {
|
||||
|
||||
// But now it is a candidate.
|
||||
cache
|
||||
.store(cat.connection(), ids[1], "b.CR2", &vec![0u8; 800], false, 20)
|
||||
.store(
|
||||
cat.connection(),
|
||||
ids[1],
|
||||
"b.CR2",
|
||||
&vec![0u8; 800],
|
||||
false,
|
||||
20,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cache.enforce(cat.connection()).unwrap(), 1);
|
||||
assert!(!cache.holds_original(cat.connection(), ids[0]));
|
||||
@@ -718,8 +765,12 @@ mod tests {
|
||||
#[test]
|
||||
fn storing_the_same_image_twice_is_one_entry() {
|
||||
let (cat, cache, _dir, ids) = fixture(1);
|
||||
cache.store(cat.connection(), ids[0], "a.CR2", b"first", false, 10).unwrap();
|
||||
cache.store(cat.connection(), ids[0], "a.CR2", b"second try", false, 20).unwrap();
|
||||
cache
|
||||
.store(cat.connection(), ids[0], "a.CR2", b"first", false, 10)
|
||||
.unwrap();
|
||||
cache
|
||||
.store(cat.connection(), ids[0], "a.CR2", b"second try", false, 20)
|
||||
.unwrap();
|
||||
|
||||
let usage = cache.usage(cat.connection()).unwrap();
|
||||
assert_eq!(usage.passive_count, 1);
|
||||
@@ -737,10 +788,24 @@ mod tests {
|
||||
// other, which is the worst failure this cache could have.
|
||||
let (cat, cache, _dir, ids) = fixture(2);
|
||||
cache
|
||||
.store(cat.connection(), ids[0], "Photos/IMG_0001.CR2", b"first", false, 10)
|
||||
.store(
|
||||
cat.connection(),
|
||||
ids[0],
|
||||
"Photos/IMG_0001.CR2",
|
||||
b"first",
|
||||
false,
|
||||
10,
|
||||
)
|
||||
.unwrap();
|
||||
cache
|
||||
.store(cat.connection(), ids[1], "Trips/IMG_0001.CR2", b"second", false, 20)
|
||||
.store(
|
||||
cat.connection(),
|
||||
ids[1],
|
||||
"Trips/IMG_0001.CR2",
|
||||
b"second",
|
||||
false,
|
||||
20,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
@@ -760,7 +825,14 @@ mod tests {
|
||||
let (cat, cache, _dir, ids) = fixture(3);
|
||||
for (i, id) in ids.iter().enumerate() {
|
||||
cache
|
||||
.store(cat.connection(), *id, "a.CR2", &vec![0u8; 400], false, i as i64)
|
||||
.store(
|
||||
cat.connection(),
|
||||
*id,
|
||||
"a.CR2",
|
||||
&vec![0u8; 400],
|
||||
false,
|
||||
i as i64,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
// 1200 held against 1000: dropping one 400-byte entry suffices.
|
||||
@@ -772,7 +844,14 @@ mod tests {
|
||||
fn an_extensionless_source_still_gets_a_path() {
|
||||
let (cat, cache, _dir, ids) = fixture(1);
|
||||
cache
|
||||
.store(cat.connection(), ids[0], "Photos/no-extension", b"bytes", false, 10)
|
||||
.store(
|
||||
cat.connection(),
|
||||
ids[0],
|
||||
"Photos/no-extension",
|
||||
b"bytes",
|
||||
false,
|
||||
10,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
cache.load(cat.connection(), ids[0], 20).unwrap().as_deref(),
|
||||
|
||||
@@ -316,9 +316,8 @@ fn remove_within(
|
||||
}
|
||||
let mut removed = 0;
|
||||
{
|
||||
let mut stmt = conn.prepare(
|
||||
"DELETE FROM collection_members WHERE collection_id = ?1 AND image_id = ?2",
|
||||
)?;
|
||||
let mut stmt = conn
|
||||
.prepare("DELETE FROM collection_members WHERE collection_id = ?1 AND image_id = ?2")?;
|
||||
for img in images {
|
||||
removed += stmt.execute(rusqlite::params![id.0 as i64, img.0 as i64])?;
|
||||
}
|
||||
@@ -397,7 +396,8 @@ pub fn save_smart(
|
||||
}
|
||||
}
|
||||
|
||||
let json = serde_json::to_string(selector).map_err(|e| CatalogError::BadSelector(e.to_string()))?;
|
||||
let json =
|
||||
serde_json::to_string(selector).map_err(|e| CatalogError::BadSelector(e.to_string()))?;
|
||||
let n = conn.execute(
|
||||
"UPDATE collections SET selector_json = ?2, kind = 1 WHERE id = ?1 AND deleted = 0",
|
||||
rusqlite::params![id.0 as i64, json],
|
||||
@@ -470,7 +470,9 @@ pub fn collections_for_image(
|
||||
ORDER BY c.name",
|
||||
)?;
|
||||
let rows = stmt
|
||||
.query_map([image.0 as i64], |r| Ok(CollectionId(r.get::<_, i64>(0)? as u64)))?
|
||||
.query_map([image.0 as i64], |r| {
|
||||
Ok(CollectionId(r.get::<_, i64>(0)? as u64))
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -492,9 +494,8 @@ pub fn descendants(
|
||||
let mut frontier = vec![root];
|
||||
let mut guard = 0usize;
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id FROM collections WHERE parent_id = ?1 AND deleted = 0",
|
||||
)?;
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT id FROM collections WHERE parent_id = ?1 AND deleted = 0")?;
|
||||
|
||||
while let Some(next) = frontier.pop() {
|
||||
guard += 1;
|
||||
@@ -507,7 +508,9 @@ pub fn descendants(
|
||||
}
|
||||
|
||||
let children = stmt
|
||||
.query_map([next.0 as i64], |r| Ok(CollectionId(r.get::<_, i64>(0)? as u64)))?
|
||||
.query_map([next.0 as i64], |r| {
|
||||
Ok(CollectionId(r.get::<_, i64>(0)? as u64))
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
for child in children {
|
||||
@@ -668,7 +671,9 @@ fn require_exists(conn: &Connection, id: CollectionId) -> Result<(), CatalogErro
|
||||
|r| r.get(0),
|
||||
)
|
||||
.optional()?;
|
||||
found.map(|_| ()).ok_or(CatalogError::NoSuchCollection(id.0))
|
||||
found
|
||||
.map(|_| ())
|
||||
.ok_or(CatalogError::NoSuchCollection(id.0))
|
||||
}
|
||||
|
||||
/// A random UUID, formatted as the canonical 8-4-4-4-12.
|
||||
@@ -685,8 +690,22 @@ fn new_uuid() -> String {
|
||||
format!(
|
||||
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-\
|
||||
{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
|
||||
b[0], b[1], b[2], b[3], b[4], b[5], v6, b[7], v8, b[9], b[10], b[11], b[12], b[13],
|
||||
b[14], b[15]
|
||||
b[0],
|
||||
b[1],
|
||||
b[2],
|
||||
b[3],
|
||||
b[4],
|
||||
b[5],
|
||||
v6,
|
||||
b[7],
|
||||
v8,
|
||||
b[9],
|
||||
b[10],
|
||||
b[11],
|
||||
b[12],
|
||||
b[13],
|
||||
b[14],
|
||||
b[15]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -797,7 +816,10 @@ mod tests {
|
||||
let rows = tree(c).unwrap();
|
||||
assert_eq!(rows.len(), 2);
|
||||
assert_eq!(rows[0].collection.name, "Trips");
|
||||
assert!(rows[0].has_children, "a parent must draw a disclosure arrow");
|
||||
assert!(
|
||||
rows[0].has_children,
|
||||
"a parent must draw a disclosure arrow"
|
||||
);
|
||||
assert_eq!(rows[1].depth, 1, "the child is indented one level");
|
||||
}
|
||||
|
||||
|
||||
@@ -472,7 +472,9 @@ mod tests {
|
||||
|
||||
let distinct: i64 = cat
|
||||
.connection()
|
||||
.query_row("SELECT count(DISTINCT uuid) FROM versions", [], |r| r.get(0))
|
||||
.query_row("SELECT count(DISTINCT uuid) FROM versions", [], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(distinct, 200);
|
||||
}
|
||||
@@ -566,7 +568,10 @@ mod tests {
|
||||
fn a_bulk_write_over_an_empty_selection_is_a_no_op() {
|
||||
let cat = with_images(3);
|
||||
assert_eq!(set_rating_many(cat.connection(), &[], 5).unwrap(), 0);
|
||||
assert_eq!(set_flag_many(cat.connection(), &[], FlagState::Pick).unwrap(), 0);
|
||||
assert_eq!(
|
||||
set_flag_many(cat.connection(), &[], FlagState::Pick).unwrap(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -491,8 +491,6 @@ mod tests {
|
||||
c
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// How many rows a named backfill touched, ignoring the others.
|
||||
///
|
||||
/// Asserting on the whole vector would couple every test to which other
|
||||
@@ -542,9 +540,11 @@ mod tests {
|
||||
|
||||
assert_eq!(backfilled(&c, "shadowed_by"), 1);
|
||||
let got: Option<i64> = c
|
||||
.query_row("SELECT shadowed_by FROM images WHERE id = ?1", [jpeg], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.query_row(
|
||||
"SELECT shadowed_by FROM images WHERE id = ?1",
|
||||
[jpeg],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(got, Some(raw));
|
||||
}
|
||||
@@ -600,11 +600,7 @@ mod tests {
|
||||
image(&c, Some(1), "a/IMG_1.JPG", "jpg");
|
||||
|
||||
assert_eq!(backfilled(&c, "shadowed_by"), 1);
|
||||
assert_eq!(
|
||||
backfilled(&c, "shadowed_by"),
|
||||
0,
|
||||
"second pass is a no-op"
|
||||
);
|
||||
assert_eq!(backfilled(&c, "shadowed_by"), 0, "second pass is a no-op");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -260,7 +260,9 @@ pub fn list(conn: &Connection, limit: usize) -> Result<Vec<TrashedImage>, Catalo
|
||||
// the current path keeps it listed and deletable rather than
|
||||
// invisible; a restore to the trash folder is a no-op the user
|
||||
// can see, where a hidden row is not.
|
||||
trashed_from: r.get::<_, Option<String>>(2)?.unwrap_or_else(|| source_ref.clone()),
|
||||
trashed_from: r
|
||||
.get::<_, Option<String>>(2)?
|
||||
.unwrap_or_else(|| source_ref.clone()),
|
||||
source_ref,
|
||||
trashed_at: r.get(3)?,
|
||||
file_id: r.get::<_, Option<i64>>(4)?.map(|v| v as u64),
|
||||
@@ -310,9 +312,7 @@ pub fn file_ids_for(conn: &Connection, images: &[ImageId]) -> Result<Vec<u64>, C
|
||||
let placeholders = std::iter::repeat_n("?", images.len())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let sql = format!(
|
||||
"SELECT file_id FROM remote WHERE image_id IN ({placeholders})"
|
||||
);
|
||||
let sql = format!("SELECT file_id FROM remote WHERE image_id IN ({placeholders})");
|
||||
let params: Vec<rusqlite::types::Value> = images
|
||||
.iter()
|
||||
.map(|i| rusqlite::types::Value::Integer(i.0 as i64))
|
||||
@@ -364,9 +364,11 @@ mod tests {
|
||||
fn do_trash(cat: &Catalog, i: u64, now: i64) -> String {
|
||||
let c = cat.connection();
|
||||
let original: String = c
|
||||
.query_row("SELECT source_ref FROM images WHERE id = ?1", [i as i64], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.query_row(
|
||||
"SELECT source_ref FROM images WHERE id = ?1",
|
||||
[i as i64],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
let to = trash_path("PhotosRaw", img(i), &original);
|
||||
record_trashed(c, &[(img(i), to.clone())], now).unwrap();
|
||||
@@ -440,7 +442,9 @@ mod tests {
|
||||
let c = cat.connection();
|
||||
do_trash(&cat, 1, 5_000);
|
||||
|
||||
let back = restore_path(c, img(1)).unwrap().expect("knows where it came from");
|
||||
let back = restore_path(c, img(1))
|
||||
.unwrap()
|
||||
.expect("knows where it came from");
|
||||
assert_eq!(back, "PhotosRaw/2019/IMG_0001.CR2");
|
||||
|
||||
record_restored(c, &[(img(1), back.clone())]).unwrap();
|
||||
@@ -471,7 +475,10 @@ mod tests {
|
||||
|
||||
do_trash(&cat, 1, 2_000);
|
||||
let second = restore_path(c, img(1)).unwrap().unwrap();
|
||||
assert_eq!(first, second, "the origin is the library path, not the trash");
|
||||
assert_eq!(
|
||||
first, second,
|
||||
"the origin is the library path, not the trash"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -506,7 +513,9 @@ mod tests {
|
||||
);
|
||||
// And its path is untouched.
|
||||
let source: String = c
|
||||
.query_row("SELECT source_ref FROM images WHERE id = 2", [], |r| r.get(0))
|
||||
.query_row("SELECT source_ref FROM images WHERE id = 2", [], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(source, "PhotosRaw/2019/IMG_0002.CR2");
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ fn main() {
|
||||
// Exactly what the sweep sees: the first HEADER_BYTES only.
|
||||
let head = &d[..d.len().min(dr_decode::HEADER_BYTES as usize)];
|
||||
match dr_decode::metadata(head) {
|
||||
Ok(m) => println!("{n}: header-only at={:?} model={:?}", m.captured_at, m.model),
|
||||
Ok(m) => println!(
|
||||
"{n}: header-only at={:?} model={:?}",
|
||||
m.captured_at, m.model
|
||||
),
|
||||
Err(e) => println!("{n}: header-only ERROR {e}"),
|
||||
}
|
||||
match dr_decode::metadata(&d) {
|
||||
|
||||
@@ -318,7 +318,9 @@ impl<'a> TiffReader<'a> {
|
||||
};
|
||||
raw[..len.min(4)].to_vec()
|
||||
} else {
|
||||
self.data.get(e.value as usize..e.value as usize + len)?.to_vec()
|
||||
self.data
|
||||
.get(e.value as usize..e.value as usize + len)?
|
||||
.to_vec()
|
||||
};
|
||||
|
||||
let s = String::from_utf8_lossy(&bytes);
|
||||
@@ -501,7 +503,6 @@ fn read_exif_entries(
|
||||
md: &mut crate::Metadata,
|
||||
fb: &mut FallbackDates,
|
||||
) {
|
||||
|
||||
for e in entries {
|
||||
match e.tag {
|
||||
exif_tag::MAKE => md.make = r.ascii(e),
|
||||
@@ -782,7 +783,6 @@ mod tests {
|
||||
assert!(locate_preview(b"\0\0\0\x18ftypcrx ", 1000).is_none());
|
||||
}
|
||||
|
||||
|
||||
/// Build a JPEG carrying an APP1 EXIF block with the given IFD entries.
|
||||
fn jpeg_with_exif(entries: &[(u16, u16, u32, u32)], extra: &[u8]) -> Vec<u8> {
|
||||
let mut tiff = Vec::new();
|
||||
|
||||
@@ -670,7 +670,11 @@ mod tests {
|
||||
height: 0.5,
|
||||
});
|
||||
|
||||
assert_eq!(f.output_size(6000, 4000), before_size, "zoom resized output");
|
||||
assert_eq!(
|
||||
f.output_size(6000, 4000),
|
||||
before_size,
|
||||
"zoom resized output"
|
||||
);
|
||||
assert!(f.crop().is_full(), "zoom altered the crop");
|
||||
}
|
||||
|
||||
|
||||
@@ -456,7 +456,10 @@ impl Sidecar {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(uuid) = line.strip_prefix("[version ").and_then(|s| s.strip_suffix(']')) {
|
||||
if let Some(uuid) = line
|
||||
.strip_prefix("[version ")
|
||||
.and_then(|s| s.strip_suffix(']'))
|
||||
{
|
||||
if let Some(v) = current.take() {
|
||||
sidecar.put(v);
|
||||
}
|
||||
@@ -495,9 +498,7 @@ impl Sidecar {
|
||||
// by a build with a wider scale, or hand-edited. An
|
||||
// out-of-range rating would sort above five stars forever and
|
||||
// no filter would reach it.
|
||||
"rating" => {
|
||||
version.rating = value.parse::<u8>().unwrap_or(0).min(MAX_RATING)
|
||||
}
|
||||
"rating" => version.rating = value.parse::<u8>().unwrap_or(0).min(MAX_RATING),
|
||||
"flag" => version.flag = value.parse::<u8>().unwrap_or(0).min(MAX_FLAG),
|
||||
_ => match key.split_once('.') {
|
||||
// An `op.param` line whose value does not parse is a
|
||||
@@ -582,11 +583,10 @@ mod tests {
|
||||
// as neutral rather than as missing.
|
||||
let v = version_of(&edited());
|
||||
assert_eq!(v.params.len(), 2);
|
||||
assert!(v.params.contains_key(&("exposure".into(), "exposure".into())));
|
||||
assert!(!v
|
||||
assert!(v
|
||||
.params
|
||||
.keys()
|
||||
.any(|(op, _)| op == colour::SATURATION_ID.0));
|
||||
.contains_key(&("exposure".into(), "exposure".into())));
|
||||
assert!(!v.params.keys().any(|(op, _)| op == colour::SATURATION_ID.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -602,12 +602,12 @@ mod tests {
|
||||
|
||||
let parsed = Sidecar::parse(&sidecar.to_text()).expect("valid");
|
||||
let mut restored = EditGraph::default_chain();
|
||||
parsed.default_version().expect("a version").apply(&mut restored);
|
||||
parsed
|
||||
.default_version()
|
||||
.expect("a version")
|
||||
.apply(&mut restored);
|
||||
|
||||
assert_eq!(
|
||||
restored.param(exposure::ID, exposure::EXPOSURE),
|
||||
Some(0.75)
|
||||
);
|
||||
assert_eq!(restored.param(exposure::ID, exposure::EXPOSURE), Some(0.75));
|
||||
assert_eq!(
|
||||
restored.param(white_balance::ID, white_balance::TEMPERATURE),
|
||||
Some(30.0)
|
||||
@@ -636,7 +636,10 @@ mod tests {
|
||||
let parsed = Sidecar::parse(&sidecar.to_text()).expect("valid");
|
||||
|
||||
let mut restored = EditGraph::default_chain();
|
||||
parsed.default_version().expect("a version").apply(&mut restored);
|
||||
parsed
|
||||
.default_version()
|
||||
.expect("a version")
|
||||
.apply(&mut restored);
|
||||
|
||||
for cap in g.capabilities() {
|
||||
for p in &cap.params {
|
||||
@@ -670,7 +673,10 @@ mod tests {
|
||||
let parsed = Sidecar::parse(&sidecar.to_text()).expect("valid");
|
||||
|
||||
let mut restored = EditGraph::default_chain();
|
||||
parsed.default_version().expect("a version").apply(&mut restored);
|
||||
parsed
|
||||
.default_version()
|
||||
.expect("a version")
|
||||
.apply(&mut restored);
|
||||
|
||||
assert_eq!(restored.crop(), g.crop());
|
||||
assert_eq!(restored.param(framing::ID, framing::ANGLE), Some(-1.5));
|
||||
@@ -1090,4 +1096,3 @@ mod tests {
|
||||
assert_eq!(format_value(0.75), "0.75");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ async fn main() {
|
||||
};
|
||||
let creds = match store.credentials(&session) {
|
||||
Ok(c) => c,
|
||||
Err(e) => { println!("credentials: {e}"); return; }
|
||||
Err(e) => {
|
||||
println!("credentials: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
println!("account: {} root={}", session.describe(), session.root);
|
||||
|
||||
@@ -45,20 +48,38 @@ async fn main() {
|
||||
// range request, or the folder?
|
||||
{
|
||||
let c = dr_sync_nextcloud::http_client("DarkRoom").unwrap();
|
||||
let base = format!("{}/remote.php/dav/files/{}",
|
||||
creds.server.trim_end_matches('/'), session.user_id);
|
||||
let base = format!(
|
||||
"{}/remote.php/dav/files/{}",
|
||||
creds.server.trim_end_matches('/'),
|
||||
session.user_id
|
||||
);
|
||||
let f = "PhotosRaw/Darktable/20230629_no_name/20230629_0030.jpeg";
|
||||
let enc: String = f.split('/').map(|seg| {
|
||||
seg.bytes().map(|b| match b {
|
||||
b'A'..=b'Z'|b'a'..=b'z'|b'0'..=b'9'|b'-'|b'_'|b'.'|b'~' => (b as char).to_string(),
|
||||
_ => format!("%{b:02X}"),
|
||||
}).collect::<String>()
|
||||
}).collect::<Vec<_>>().join("/");
|
||||
let enc: String = f
|
||||
.split('/')
|
||||
.map(|seg| {
|
||||
seg.bytes()
|
||||
.map(|b| match b {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
(b as char).to_string()
|
||||
}
|
||||
_ => format!("%{b:02X}"),
|
||||
})
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
let url = format!("{base}/{enc}");
|
||||
|
||||
for (what, range) in [("ranged 0-256k", Some("bytes=0-262143")), ("whole file", None)] {
|
||||
let mut rq = c.get(&url).basic_auth(&creds.login_name, Some(&creds.app_password));
|
||||
if let Some(r) = range { rq = rq.header("Range", r); }
|
||||
for (what, range) in [
|
||||
("ranged 0-256k", Some("bytes=0-262143")),
|
||||
("whole file", None),
|
||||
] {
|
||||
let mut rq = c
|
||||
.get(&url)
|
||||
.basic_auth(&creds.login_name, Some(&creds.app_password));
|
||||
if let Some(r) = range {
|
||||
rq = rq.header("Range", r);
|
||||
}
|
||||
match rq.send().await {
|
||||
Ok(r) => println!("GET {what}: {}", r.status()),
|
||||
Err(e) => println!("GET {what}: transport {e}"),
|
||||
@@ -68,11 +89,20 @@ async fn main() {
|
||||
|
||||
// Raw HTTP, to see the status the connector maps away.
|
||||
{
|
||||
let url = format!("{}/remote.php/dav/files/{}/{}/.darkroom-write-test",
|
||||
creds.server.trim_end_matches('/'), session.user_id, session.root);
|
||||
let url = format!(
|
||||
"{}/remote.php/dav/files/{}/{}/.darkroom-write-test",
|
||||
creds.server.trim_end_matches('/'),
|
||||
session.user_id,
|
||||
session.root
|
||||
);
|
||||
let c = dr_sync_nextcloud::http_client("DarkRoom").unwrap();
|
||||
match c.put(&url).basic_auth(&creds.login_name, Some(&creds.app_password))
|
||||
.body("probe").send().await {
|
||||
match c
|
||||
.put(&url)
|
||||
.basic_auth(&creds.login_name, Some(&creds.app_password))
|
||||
.body("probe")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) => {
|
||||
println!("RAW PUT status: {}", r.status());
|
||||
let body = r.text().await.unwrap_or_default();
|
||||
@@ -90,20 +120,33 @@ async fn main() {
|
||||
// freely elsewhere, which is a different fix from a credential problem.
|
||||
{
|
||||
let c = dr_sync_nextcloud::http_client("DarkRoom").unwrap();
|
||||
let base = format!("{}/remote.php/dav/files/{}",
|
||||
creds.server.trim_end_matches('/'), session.user_id);
|
||||
let base = format!(
|
||||
"{}/remote.php/dav/files/{}",
|
||||
creds.server.trim_end_matches('/'),
|
||||
session.user_id
|
||||
);
|
||||
for (what, url) in [
|
||||
("account root", format!("{base}/.darkroom-write-test")),
|
||||
("library root", format!("{base}/{}/.darkroom-write-test", session.root)),
|
||||
(
|
||||
"library root",
|
||||
format!("{base}/{}/.darkroom-write-test", session.root),
|
||||
),
|
||||
] {
|
||||
match c.put(&url).basic_auth(&creds.login_name, Some(&creds.app_password))
|
||||
.body("probe").send().await {
|
||||
match c
|
||||
.put(&url)
|
||||
.basic_auth(&creds.login_name, Some(&creds.app_password))
|
||||
.body("probe")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) => {
|
||||
println!("PUT {what}: {}", r.status());
|
||||
if r.status().is_success() {
|
||||
let _ = c.delete(&url)
|
||||
let _ = c
|
||||
.delete(&url)
|
||||
.basic_auth(&creds.login_name, Some(&creds.app_password))
|
||||
.send().await;
|
||||
.send()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e) => println!("PUT {what}: transport error {e}"),
|
||||
@@ -113,7 +156,10 @@ async fn main() {
|
||||
|
||||
// Write into the library root.
|
||||
let p = RemotePath::new(format!("{}/.darkroom-write-test", session.root));
|
||||
match backend.put(&p, b"darkroom write probe\n".to_vec(), None).await {
|
||||
match backend
|
||||
.put(&p, b"darkroom write probe\n".to_vec(), None)
|
||||
.await
|
||||
{
|
||||
Ok(v) => {
|
||||
println!("WRITE ok: etag={}", v.as_str());
|
||||
match backend.delete(&RemoteId::Path(p.clone()), None).await {
|
||||
|
||||
@@ -134,7 +134,8 @@ impl NextcloudBackend {
|
||||
for (i, chunk) in body.chunks(chunk_size).enumerate() {
|
||||
if i + 1 > CHUNKS.max_chunks as usize {
|
||||
return Err(RemoteError::Protocol(format!(
|
||||
"{total} bytes exceeds {} chunks", CHUNKS.max_chunks
|
||||
"{total} bytes exceeds {} chunks",
|
||||
CHUNKS.max_chunks
|
||||
)));
|
||||
}
|
||||
let resp = self
|
||||
@@ -221,9 +222,7 @@ impl NextcloudBackend {
|
||||
.map_err(map_send_error)?;
|
||||
|
||||
map_status(resp.status(), path.as_str())?;
|
||||
resp.text()
|
||||
.await
|
||||
.map_err(map_send_error)
|
||||
resp.text().await.map_err(map_send_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,18 +280,11 @@ impl RemoteBackend for NextcloudBackend {
|
||||
);
|
||||
}
|
||||
|
||||
let resp = req
|
||||
.send()
|
||||
.await
|
||||
.map_err(map_send_error)?;
|
||||
let resp = req.send().await.map_err(map_send_error)?;
|
||||
let status = resp.status();
|
||||
map_status(status, &url)?;
|
||||
|
||||
let body = resp
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(map_send_error)?
|
||||
.to_vec();
|
||||
let body = resp.bytes().await.map_err(map_send_error)?.to_vec();
|
||||
|
||||
// Nextcloud does not advertise Accept-Ranges, so support is detected
|
||||
// by the response code rather than by probing with HEAD (ARCH §6.7).
|
||||
@@ -342,11 +334,7 @@ impl RemoteBackend for NextcloudBackend {
|
||||
None => {}
|
||||
}
|
||||
|
||||
let resp = req
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(map_send_error)?;
|
||||
let resp = req.body(body).send().await.map_err(map_send_error)?;
|
||||
map_status(resp.status(), path.as_str())?;
|
||||
|
||||
resp.headers()
|
||||
@@ -371,10 +359,7 @@ impl RemoteBackend for NextcloudBackend {
|
||||
req = req.header(reqwest::header::IF_MATCH, format!("\"{}\"", v.as_str()));
|
||||
}
|
||||
|
||||
let resp = req
|
||||
.send()
|
||||
.await
|
||||
.map_err(map_send_error)?;
|
||||
let resp = req.send().await.map_err(map_send_error)?;
|
||||
map_status(resp.status(), &url)
|
||||
}
|
||||
|
||||
@@ -491,12 +476,7 @@ impl RemoteBackend for NextcloudBackend {
|
||||
}
|
||||
map_status(resp.status(), &url)?;
|
||||
|
||||
Ok(Some(
|
||||
resp.bytes()
|
||||
.await
|
||||
.map_err(map_send_error)?
|
||||
.to_vec(),
|
||||
))
|
||||
Ok(Some(resp.bytes().await.map_err(map_send_error)?.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,15 +513,15 @@ const EXTRA_ROOTS: &[(&str, &[u8])] = &[(
|
||||
/// failing here would take down every connection including the ones that never
|
||||
/// needed the extra anchor.
|
||||
fn extra_roots() -> impl Iterator<Item = reqwest::Certificate> {
|
||||
EXTRA_ROOTS.iter().filter_map(|(name, pem)| {
|
||||
match reqwest::Certificate::from_pem(pem) {
|
||||
EXTRA_ROOTS
|
||||
.iter()
|
||||
.filter_map(|(name, pem)| match reqwest::Certificate::from_pem(pem) {
|
||||
Ok(c) => Some(c),
|
||||
Err(e) => {
|
||||
log::warn!("bundled root {name} unusable: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Build an HTTP client with the crypto provider already installed.
|
||||
@@ -852,5 +832,3 @@ mod live {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -49,9 +49,7 @@ fn config_dir() -> PathBuf {
|
||||
}
|
||||
std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
PathBuf::from(std::env::var("HOME").unwrap_or_default()).join(".config")
|
||||
})
|
||||
.unwrap_or_else(|| PathBuf::from(std::env::var("HOME").unwrap_or_default()).join(".config"))
|
||||
.join("darkroom")
|
||||
}
|
||||
|
||||
|
||||
@@ -326,7 +326,9 @@ mod tests {
|
||||
let later = start + Duration::from_secs(600);
|
||||
r.observe::<()>(&Err(network()), later);
|
||||
|
||||
let elapsed = r.offline_for(later).expect("offline since the first failure");
|
||||
let elapsed = r
|
||||
.offline_for(later)
|
||||
.expect("offline since the first failure");
|
||||
assert!(
|
||||
elapsed >= Duration::from_secs(600),
|
||||
"measured from the first failure, got {elapsed:?}"
|
||||
|
||||
@@ -446,7 +446,10 @@ mod tests {
|
||||
"a trashed image must not come back as an ordinary one"
|
||||
);
|
||||
// The real photographs are still found.
|
||||
assert!(result.images.iter().any(|i| i.path.as_str().ends_with("a.CR2")));
|
||||
assert!(result
|
||||
.images
|
||||
.iter()
|
||||
.any(|i| i.path.as_str().ends_with("a.CR2")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -500,9 +503,7 @@ mod tests {
|
||||
// And nothing else is swept up by it.
|
||||
assert!(!is_excluded(&RemotePath::new("Photos/2025")));
|
||||
assert!(!is_excluded(&RemotePath::new("Photos/trash")));
|
||||
assert!(!is_excluded(&RemotePath::new(
|
||||
"Photos/.darkroom-trash-old"
|
||||
)));
|
||||
assert!(!is_excluded(&RemotePath::new("Photos/.darkroom-trash-old")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
+18
-13
@@ -429,10 +429,9 @@ impl ThumbStore {
|
||||
for (file_id, shard) in in_unsealed {
|
||||
match self.open_shard(shard, false) {
|
||||
Ok(conn) => {
|
||||
if let Err(e) = conn.execute(
|
||||
"DELETE FROM thumbs WHERE file_id = ?1",
|
||||
[file_id as i64],
|
||||
) {
|
||||
if let Err(e) =
|
||||
conn.execute("DELETE FROM thumbs WHERE file_id = ?1", [file_id as i64])
|
||||
{
|
||||
log::debug!("forgetting thumbnail {file_id} in shard {shard}: {e}");
|
||||
}
|
||||
}
|
||||
@@ -502,7 +501,7 @@ impl ThumbStore {
|
||||
// fallback below assumes.
|
||||
let has_size = src
|
||||
.prepare("SELECT * FROM thumbs LIMIT 0")
|
||||
.map(|stmt| stmt.column_names().iter().any(|c| *c == "size"))
|
||||
.map(|stmt| stmt.column_names().contains(&"size"))
|
||||
.unwrap_or(false);
|
||||
|
||||
let sql = if has_size {
|
||||
@@ -594,7 +593,7 @@ CREATE TABLE IF NOT EXISTS thumbs (
|
||||
fn migrate_size_column(conn: &Connection, table: &str) -> Result<(), ThumbError> {
|
||||
let has_size: bool = conn
|
||||
.prepare(&format!("SELECT * FROM {table} LIMIT 0"))
|
||||
.map(|stmt| stmt.column_names().iter().any(|c| *c == "size"))
|
||||
.map(|stmt| stmt.column_names().contains(&"size"))
|
||||
.unwrap_or(true);
|
||||
if has_size {
|
||||
return Ok(());
|
||||
@@ -748,7 +747,6 @@ mod tests {
|
||||
assert!(s.get(1, ThumbSize::Grid).unwrap().is_some());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn the_two_size_classes_coexist() {
|
||||
// Adding the large class must not evict or shadow the grid one: the
|
||||
@@ -800,7 +798,6 @@ mod tests {
|
||||
assert_eq!(ThumbSize::for_cell(400), ThumbSize::Large);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn a_shard_written_before_the_size_class_accepts_new_thumbnails() {
|
||||
// The index is not the only table with a `size` column: every shard
|
||||
@@ -969,8 +966,16 @@ mod tests {
|
||||
let original = s.shard_of(0, ThumbSize::Grid).unwrap();
|
||||
|
||||
s.put(0, ThumbSize::Grid, &thumb(32)).unwrap();
|
||||
assert_eq!(s.shard_of(0, ThumbSize::Grid), Some(original), "must stay put");
|
||||
assert_eq!(s.get(0, ThumbSize::Grid).unwrap().unwrap().bytes.len(), 32, "but update");
|
||||
assert_eq!(
|
||||
s.shard_of(0, ThumbSize::Grid),
|
||||
Some(original),
|
||||
"must stay put"
|
||||
);
|
||||
assert_eq!(
|
||||
s.get(0, ThumbSize::Grid).unwrap().unwrap().bytes.len(),
|
||||
32,
|
||||
"but update"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -997,7 +1002,8 @@ mod tests {
|
||||
mine.put(1, ThumbSize::Grid, &thumb(64)).unwrap();
|
||||
|
||||
// A second store standing in for another device's downloaded shard.
|
||||
let other_dir = std::env::temp_dir().join(format!("dr-thumbs-other-{}", std::process::id()));
|
||||
let other_dir =
|
||||
std::env::temp_dir().join(format!("dr-thumbs-other-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&other_dir);
|
||||
let mut theirs = ThumbStore::open(&other_dir).unwrap();
|
||||
theirs.put(1, ThumbSize::Grid, &thumb(999)).unwrap(); // we already have this one
|
||||
@@ -1017,8 +1023,7 @@ mod tests {
|
||||
#[test]
|
||||
fn merging_is_idempotent() {
|
||||
let (mut mine, _d1) = store();
|
||||
let other_dir =
|
||||
std::env::temp_dir().join(format!("dr-thumbs-idem-{}", std::process::id()));
|
||||
let other_dir = std::env::temp_dir().join(format!("dr-thumbs-idem-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&other_dir);
|
||||
let mut theirs = ThumbStore::open(&other_dir).unwrap();
|
||||
theirs.put(10, ThumbSize::Grid, &thumb(64)).unwrap();
|
||||
|
||||
@@ -19,7 +19,9 @@ pub use settings::{
|
||||
|
||||
/// Identifies a granted library location — a directory on Linux, a persisted
|
||||
/// document tree on Android.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct RootId(pub u64);
|
||||
|
||||
@@ -32,7 +34,9 @@ pub struct ImageId(pub u64);
|
||||
pub struct VersionId(pub u64);
|
||||
|
||||
/// Identifies a user-defined collection.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct CollectionId(pub u64);
|
||||
|
||||
@@ -251,7 +255,9 @@ impl FormatFilter {
|
||||
///
|
||||
/// Surfaced in the UI so a user always knows what they have — the failure
|
||||
/// Lightroom makes by showing an original's filename beside a 2560px proxy.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub enum Availability {
|
||||
/// Catalogued, nothing cached.
|
||||
MetadataOnly,
|
||||
|
||||
@@ -115,7 +115,9 @@ pub enum DateSelector {
|
||||
}
|
||||
|
||||
/// Colour labels, matching the conventional set.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub enum ColourLabel {
|
||||
Red,
|
||||
Yellow,
|
||||
@@ -130,8 +132,17 @@ pub enum ColourLabel {
|
||||
/// anyone has looked at it — an unjudged frame, not a rejected one. Any other
|
||||
/// default would silently assert a judgement the photographer never made.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash,
|
||||
serde::Serialize, serde::Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
pub enum FlagState {
|
||||
/// Not yet judged. What "filter to unjudged" selects (FR-CULL-4).
|
||||
@@ -144,7 +155,9 @@ pub enum FlagState {
|
||||
/// How much of an image a cache rule asks to keep locally.
|
||||
///
|
||||
/// Ordered so the most generous matching rule wins (ARCH §9.3).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub enum Tier {
|
||||
/// Catalog rows and sidecars only. Never evicted — authoritative and tiny.
|
||||
Metadata,
|
||||
|
||||
@@ -363,7 +363,12 @@ pub enum OutputSharpening {
|
||||
}
|
||||
|
||||
impl OutputSharpening {
|
||||
pub const ALL: [Self; 4] = [Self::None, Self::Screen, Self::MattePaper, Self::GlossyPaper];
|
||||
pub const ALL: [Self; 4] = [
|
||||
Self::None,
|
||||
Self::Screen,
|
||||
Self::MattePaper,
|
||||
Self::GlossyPaper,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
@@ -454,9 +459,11 @@ pub mod budget {
|
||||
/// stray keystroke should leave the previous budget in place, where
|
||||
/// clamping to a minimum would silently shrink a cache to nothing.
|
||||
pub fn from_gb(text: &str) -> Option<u64> {
|
||||
let gb: f64 = text.trim().trim_end_matches(|c: char| {
|
||||
c.is_ascii_alphabetic() || c.is_whitespace()
|
||||
}).parse().ok()?;
|
||||
let gb: f64 = text
|
||||
.trim()
|
||||
.trim_end_matches(|c: char| c.is_ascii_alphabetic() || c.is_whitespace())
|
||||
.parse()
|
||||
.ok()?;
|
||||
if !gb.is_finite() || gb <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
@@ -480,10 +487,7 @@ mod tests {
|
||||
fn defaults_round_trip_through_json() {
|
||||
let settings = Settings::default();
|
||||
let json = serde_json::to_string(&settings).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Settings>(&json).unwrap(),
|
||||
settings
|
||||
);
|
||||
assert_eq!(serde_json::from_str::<Settings>(&json).unwrap(), settings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user