From 2b30f53e7a7b9b78c96c1f02340ff64286fce59b Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 12 Nov 2026 06:29:31 -0500 Subject: [PATCH] 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 --- crates/export/src/lib.rs | 2 +- crates/export/src/pipeline.rs | 14 +++++++ crates/export/tests/it/render_settings.rs | 48 +++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/crates/export/src/lib.rs b/crates/export/src/lib.rs index 2687eadad..d9af7b2aa 100644 --- a/crates/export/src/lib.rs +++ b/crates/export/src/lib.rs @@ -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 = (i..end).filter(|k| !(cx.settings.skip_existing && out::exists(cx.sink, &sequence_path(cx.path, f0 + *k as i64)))).collect(); + let ks: Vec = (i..end).filter(|k| !(cx.settings.skip_existing && cx.frame_exists(&sequence_path(cx.path, f0 + *k as i64)))).collect(); let written: Vec> = cx .frames(comp, ks, |k, img| { let path = sequence_path(cx.path, f0 + k as i64); diff --git a/crates/export/src/pipeline.rs b/crates/export/src/pipeline.rs index d447e0980..a0e4980ec 100644 --- a/crates/export/src/pipeline.rs +++ b/crates/export/src/pipeline.rs @@ -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 { diff --git a/crates/export/tests/it/render_settings.rs b/crates/export/tests/it/render_settings.rs index 99e3d9c19..ddad4336e 100644 --- a/crates/export/tests/it/render_settings.rs +++ b/crates/export/tests/it/render_settings.rs @@ -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("a), + 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)]