Export: Skip Existing keeps a frame already in the overflow folder

* Export: Skip Existing keeps a frame already in the overflow folder

* Export: Skip Existing checks every overflow folder before a frame is placed
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-12 06:29:31 -05:00
1 parent bdc56ca75e
commit 2b30f53e7a
3 files changed
+63 -1

No files matched your search

+1 -1
View File
@@ -321,7 +321,7 @@ async fn sequence(cx: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut State<'_>)
let mut i = 0;
while i < st.total {
let end = (i + batch).min(st.total);
let ks: Vec<u64> = (i..end).filter(|k| !(cx.settings.skip_existing && out::exists(cx.sink, &sequence_path(cx.path, f0 + *k as i64)))).collect();
let ks: Vec<u64> = (i..end).filter(|k| !(cx.settings.skip_existing && cx.frame_exists(&sequence_path(cx.path, f0 + *k as i64)))).collect();
let written: Vec<Result<u64>> = cx
.frames(comp, ks, |k, img| {
let path = sequence_path(cx.path, f0 + k as i64);
+14
View File
@@ -894,6 +894,20 @@ impl<'a> Cx<'a> {
}
}
/// Whether Skip Existing finds `path` already written: at its own place or, with Use Storage
/// Overflow, in any overflow folder (checked before a frame is placed, so a skipped frame
/// reserves no quota).
pub fn frame_exists(&self, path: &str) -> bool {
if out::exists(self.sink, path) {
return true;
}
if !self.job.settings.storage_overflow {
return false;
}
let Some(name) = std::path::Path::new(path).file_name() else { return false };
self.job.options.overflow.iter().any(|dir| out::exists(self.sink, &std::path::Path::new(dir).join(name).to_string_lossy()))
}
/// Where a file goes: `path`, or (Use Storage Overflow) the first overflow folder with room
/// for `bytes` when the quota hook says the output volume is full.
pub fn place(&self, path: &str, bytes: u64) -> String {
+48
View File
@@ -444,6 +444,54 @@ fn cancelled_export_removes_only_files_it_opened() {
std::fs::remove_dir_all(dir).unwrap();
}
/// Skip Existing keeps a frame that is already in the overflow folder the frame is placed in
/// (it checked only the primary destination, then truncated the overflow file).
#[test]
fn skip_existing_keeps_a_frame_in_the_overflow_folder() {
use std::path::PathBuf;
// Room only in the second overflow folder; the frame already sits in the first (which is full).
struct Overflow(PathBuf);
impl StorageQuota for Overflow {
fn has_room(&self, path: &str, _: u64) -> bool {
std::path::Path::new(path).starts_with(&self.0)
}
}
let guard = tempfile::tempdir().unwrap();
let (dir, full, over) = (guard.path().to_path_buf(), guard.path().join("full"), guard.path().join("over"));
std::fs::create_dir_all(&full).unwrap();
std::fs::create_dir_all(&over).unwrap();
std::fs::write(full.join("clip_00000.png"), b"keep").unwrap();
let quota = Overflow(over.clone());
let (p, cid) = project(FrameRate::new(25, 1), |_, _| {});
let settings = RenderSettings { skip_existing: true, storage_overflow: true, ..Default::default() };
let om = OutputModule::for_format(OutputFormat::PngSequence);
let path = dir.join("clip_[#####].png");
let result = export(
&Job {
project: &p,
footage: &NoFootage,
expr: None,
accel: None,
comp: cid,
settings: &settings,
output: &om,
path: path.to_str().unwrap(),
sink: None,
nested_switches: true,
options: JobOptions {
storage: Some(&quota),
overflow: vec![full.to_string_lossy().into_owned(), over.to_string_lossy().into_owned()],
..Default::default()
},
},
&mut |_| true,
);
assert!(result.is_ok(), "{result:?}");
assert_eq!(std::fs::read(full.join("clip_00000.png")).unwrap(), b"keep");
assert!(!over.join("clip_00000.png").exists(), "no duplicate in another overflow folder");
assert!(over.join("clip_00001.png").exists(), "the other frames are still written");
}
/// A GPU compositor stand-in that renders every frame solid green (the CPU draws the bar and the
/// block on black) and keeps Auto's timing history.
#[derive(Default)]