mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 18:07:07 -06:00
fix(format): keep stale previews until the manifest commits; errno 45 only where it is ENOTSUP
A directory save removed a thumbnail or composite the new document no longer has before writing the new manifest, so a failed manifest flush left the previous manifest naming a preview that was gone. They are now removed after the manifest commits, best-effort like unreferenced objects; the failed-flush test checks the thumbnail survives. errno 45 counted as "can't flush directories" on every Unix, but it is ENOTSUP only on Apple platforms and the BSDs (illumos: EDEADLK). Other Unix targets now accept only EINVAL and ErrorKind::Unsupported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
747f7a148b
commit
f77ffa3e3d
@@ -251,14 +251,17 @@ fn dir_sync_unsupported(e: &io::Error) -> bool {
|
||||
return true;
|
||||
}
|
||||
// errno values: EINVAL is 22 everywhere; ENOTSUP and EOPNOTSUPP are 95 on Linux and
|
||||
// Android, 45 and 102 on Apple platforms, 45 on the BSDs; ENOTTY is 25.
|
||||
// Android, 45 and 102 on Apple platforms, 45 on the BSDs; ENOTTY is 25. Elsewhere 45 means
|
||||
// something else (illumos: EDEADLK), so only EINVAL and `Unsupported` count there.
|
||||
const EINVAL: i32 = 22;
|
||||
let unsupported: &[i32] = if cfg!(any(target_os = "linux", target_os = "android")) {
|
||||
&[EINVAL, 95]
|
||||
} else if cfg!(target_vendor = "apple") {
|
||||
&[EINVAL, 45, 102, 25]
|
||||
} else if cfg!(unix) {
|
||||
} else if cfg!(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly")) {
|
||||
&[EINVAL, 45]
|
||||
} else if cfg!(unix) {
|
||||
&[EINVAL]
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
|
||||
+15
-13
@@ -909,20 +909,16 @@ impl PcraftWriter {
|
||||
// Pin all GC directories before removing stale previews or objects.
|
||||
// Unix cleanup then stays within these directories even if a pathname is swapped.
|
||||
let gc = BundleGc::new(dir)?;
|
||||
write_bundle_file(dir, &dir.join(MANIFEST), &p.manifest)?;
|
||||
// The manifest is committed: the save has succeeded. Removing previews and objects it no
|
||||
// longer references is best-effort housekeeping (a vanished or locked file must not turn a
|
||||
// complete save into a reported failure); a leftover is retried on the next save. Previews
|
||||
// go only now too: if the manifest can't be committed, the previous one still names them.
|
||||
for stale in [THUMB, COMPOSITE] {
|
||||
if !p.previews.iter().any(|(n, _)| *n == stale) {
|
||||
let path = dir.join(stale);
|
||||
validate_bundle_file(dir, &path)?;
|
||||
match remove_bundle_file(dir, &path, &gc) {
|
||||
Err(FormatError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
other => other?,
|
||||
}
|
||||
let _ = remove_bundle_file(dir, &dir.join(stale), &gc);
|
||||
}
|
||||
}
|
||||
write_bundle_file(dir, &dir.join(MANIFEST), &p.manifest)?;
|
||||
// The manifest is committed: the save has succeeded. Removing objects it no longer
|
||||
// references is best-effort housekeeping (a vanished or locked file must not turn a
|
||||
// complete save into a reported failure); a leftover is retried on the next save.
|
||||
for path in existing {
|
||||
if !p.objects.contains_key(&path) {
|
||||
if remove_bundle_file(dir, &dir.join(&path), &gc).is_ok() {
|
||||
@@ -1003,8 +999,9 @@ fn gc_pinned_directory_handles_resist_subdirectory_swap() {
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
/// #4968: a manifest whose folder can't be flushed fails the save, and the objects the previous
|
||||
/// manifest references stay in place; the next save that flushes removes what is unreferenced.
|
||||
/// #4968: a manifest whose folder can't be flushed fails the save, and the objects and previews the
|
||||
/// previous manifest references stay in place; the next save that flushes removes what is
|
||||
/// unreferenced.
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn a_failed_manifest_flush_keeps_the_previous_objects() {
|
||||
@@ -1021,7 +1018,10 @@ fn a_failed_manifest_flush_keeps_the_previous_objects() {
|
||||
};
|
||||
paint(&mut doc, 1.0);
|
||||
let mut w = PcraftWriter::new();
|
||||
w.save_dir(&doc, &dir, &SaveOptions::default()).unwrap();
|
||||
// The first save has a thumbnail; the next ones drop it.
|
||||
let thumb = SaveOptions { thumbnail: Some(Rgba8Image::new(4, 3)), composite: None };
|
||||
w.save_dir(&doc, &dir, &thumb).unwrap();
|
||||
assert!(dir.join(THUMB).exists());
|
||||
let old = list_objects(&dir).unwrap();
|
||||
paint(&mut doc, 0.0);
|
||||
FAIL_DIR_FLUSH.with(|f| f.set(Some(MANIFEST)));
|
||||
@@ -1030,8 +1030,10 @@ fn a_failed_manifest_flush_keeps_the_previous_objects() {
|
||||
let now = list_objects(&dir).unwrap();
|
||||
assert!(old.is_subset(&now), "the previous manifest's objects are kept");
|
||||
assert!(now.len() > old.len(), "the new objects were written");
|
||||
assert!(dir.join(THUMB).exists(), "the previous manifest's thumbnail is kept");
|
||||
let stats = w.save_dir(&doc, &dir, &SaveOptions::default()).unwrap();
|
||||
assert!(stats.objects_removed > 0, "the next save removes the unreferenced objects");
|
||||
assert!(!dir.join(THUMB).exists(), "and the thumbnail it no longer names");
|
||||
assert_eq!(crate::load_path(&dir).unwrap(), doc);
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user