mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 02:11:41 -06:00
#1743: validate background and bridged exports in their owning session
This commit is contained in:
@@ -16,6 +16,8 @@ use super::{CommandSpec, always, b_p, bad, comp_id, f_p, has_comp, str_p};
|
||||
use crate::jobs::TaskCtl;
|
||||
use crate::{EngineError, Exporter, Result, Session, cmd};
|
||||
|
||||
mod staging;
|
||||
|
||||
fn format_p(p: &Value, cmd: &str) -> Result<Option<TimelineFormat>> {
|
||||
match str_p(p, "format") {
|
||||
None | Some("") | Some("auto") => Ok(None),
|
||||
@@ -178,7 +180,6 @@ fn export_timeline(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let relative_to = Some(dir.to_string_lossy().to_string()).filter(|d| !d.is_empty());
|
||||
let opts = ExportOptions { format, prerender: prerender_mode, precomps, relative_to };
|
||||
let project = s.project.clone();
|
||||
let services = s.services.clone();
|
||||
let cx = PrerenderCx {
|
||||
protected: crate::output_guard::Protected::of(s),
|
||||
exporter: s.exporter.clone(),
|
||||
@@ -191,17 +192,21 @@ fn export_timeline(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// in Window ▸ Progress instead of with the window waiting (#2269).
|
||||
let wait = !b_p(p, "background").unwrap_or(false);
|
||||
s.spawn_task("exportTimeline", format!("Export {}", format.label()), wait, move |ctl| {
|
||||
let (bytes, mut result) = export_timeline_work(&cx, &project, cid, &opts, &dir, &stem, ctl)?;
|
||||
let (bytes, mut result, staged) = export_timeline_work(&cx, &project, cid, &opts, &dir, &stem, ctl)?;
|
||||
if let Some(source) = cx.protected.find(std::path::Path::new(&path)) {
|
||||
return Err(source.refusal(&path, crate::output_guard::Use::Export));
|
||||
}
|
||||
services.write_file(&path, &bytes).map_err(|e| format!("cannot write {path}: {e}"))?;
|
||||
result["path"] = json!(path);
|
||||
result["toast"] = json!(match format {
|
||||
TimelineFormat::Fcp7Xml => format!("Exported {path} (Final Cut Pro XML; import it in Premiere Pro with File ▸ Import)"),
|
||||
_ => format!("Exported {path} ({})", format.label()),
|
||||
});
|
||||
Ok(Box::new(move |_: &mut Session| Ok(result)))
|
||||
Ok(Box::new(move |s: &mut Session| {
|
||||
s.refuse_export(&path)?;
|
||||
staging::commit(staged, s)?;
|
||||
s.write_output(&path, &bytes)?;
|
||||
Ok(result)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,11 +220,15 @@ fn export_timeline_work(
|
||||
dir: &std::path::Path,
|
||||
stem: &str,
|
||||
ctl: &TaskCtl,
|
||||
) -> std::result::Result<(Vec<u8>, Value), String> {
|
||||
) -> std::result::Result<(Vec<u8>, Value, Vec<staging::Movie>), String> {
|
||||
let mut pre: HashMap<(ItemId, LayerId), Prerendered> = HashMap::new();
|
||||
let mut rendered = vec![];
|
||||
let mut errors = vec![];
|
||||
let mut staged = vec![];
|
||||
let plan = effectcraft_interchange::plan_prerender(project, cid, opts);
|
||||
if plan.len() > staging::MAX_MOVIES {
|
||||
return Err(format!("too many pre-render movies (limit {})", staging::MAX_MOVIES));
|
||||
}
|
||||
let n = plan.len() as u64;
|
||||
for (i, (c, l)) in (0u64..).zip(plan) {
|
||||
let comp_name = project.item(c).map(|it| it.name.clone()).unwrap_or_default();
|
||||
@@ -233,10 +242,13 @@ fn export_timeline_work(
|
||||
}
|
||||
// Progress over all the layers, each one's frames a share of it.
|
||||
let mut progress = |done: u64, total: u64| ctl.progress(i * 1000 + done.saturating_mul(1000) / total.max(1), n * 1000);
|
||||
match prerender(cx, project, c, l, &file, &mut progress) {
|
||||
Ok(r) => {
|
||||
let movie = staging::Movie::new(&file)?;
|
||||
match prerender(cx, project, c, l, &movie.path, &mut progress) {
|
||||
Ok(mut r) => {
|
||||
r.path = file;
|
||||
rendered.push(r.path.clone());
|
||||
pre.insert((c, l), r);
|
||||
staged.push(movie);
|
||||
}
|
||||
Err(_) if ctl.cancelled() => return Err(crate::render_queue::CANCELLED.into()),
|
||||
Err(e) => errors.push(format!("{comp_name} ▸ {layer_name}: {e}")),
|
||||
@@ -255,7 +267,7 @@ fn export_timeline_work(
|
||||
"prerendered": rendered,
|
||||
"warnings": warnings,
|
||||
});
|
||||
Ok((out.bytes, result))
|
||||
Ok((out.bytes, result, staged))
|
||||
}
|
||||
|
||||
/// The formats, for agents and the export dialog.
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Native pre-render files stay private until the app can check the current project.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::EngineError;
|
||||
use crate::Session;
|
||||
|
||||
pub(super) const MAX_MOVIES: usize = 4096;
|
||||
|
||||
pub(super) struct Movie {
|
||||
pub path: String,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
directory: tempfile::TempDir,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
destination: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl Movie {
|
||||
pub fn new(path: &str) -> Result<Self, String> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let path = std::path::Path::new(path);
|
||||
let parent = path.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(std::path::Path::new("."));
|
||||
std::fs::create_dir_all(parent).map_err(|e| format!("cannot create pre-render folder {}: {e}", parent.display()))?;
|
||||
let directory =
|
||||
tempfile::Builder::new().prefix(".effectcraft-prerender-").tempdir_in(parent).map_err(|e| format!("cannot stage pre-render: {e}"))?;
|
||||
let name = path.file_name().ok_or_else(|| format!("pre-render has no file name: {}", path.display()))?;
|
||||
let staged = directory.path().join(name).to_string_lossy().into_owned();
|
||||
Ok(Self { path: staged, directory, destination: parent.to_path_buf() })
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
// spawn_task is synchronous on WASM, so session mutations cannot interleave.
|
||||
Ok(Self { path: path.to_string() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn commit(movies: Vec<Movie>, session: &Session) -> crate::Result<()> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
const FILES_PER_MOVIE: usize = 8;
|
||||
if movies.len() > MAX_MOVIES {
|
||||
return Err(EngineError::Other("too many staged pre-render movies".into()));
|
||||
}
|
||||
let mut files = Vec::new();
|
||||
let mut path_bytes = 0usize;
|
||||
for movie in &movies {
|
||||
let entries = std::fs::read_dir(movie.directory.path()).map_err(|e| EngineError::Other(format!("cannot read staged pre-render: {e}")))?;
|
||||
for (index, entry) in entries.take(FILES_PER_MOVIE + 1).enumerate() {
|
||||
if index == FILES_PER_MOVIE {
|
||||
return Err(EngineError::Other("too many files in a staged pre-render".into()));
|
||||
}
|
||||
let entry = entry.map_err(|e| EngineError::Other(format!("cannot read staged pre-render entry: {e}")))?;
|
||||
let kind = entry.file_type().map_err(|e| EngineError::Other(format!("cannot inspect staged pre-render: {e}")))?;
|
||||
if !kind.is_file() {
|
||||
return Err(EngineError::Other("a staged pre-render contains an unexpected directory or link".into()));
|
||||
}
|
||||
let source = entry.path();
|
||||
let destination = movie.destination.join(entry.file_name());
|
||||
path_bytes = path_bytes.saturating_add(source.as_os_str().len()).saturating_add(destination.as_os_str().len());
|
||||
if path_bytes > 16 * 1024 * 1024 {
|
||||
return Err(EngineError::Other("staged pre-render paths exceed the 16 MiB limit".into()));
|
||||
}
|
||||
files.push((source, destination));
|
||||
}
|
||||
}
|
||||
// If a user imported a private staging file, leave the staging folders intact.
|
||||
if let Some((source, _)) = files.iter().find(|(source, _)| session.refuse_export(&source.to_string_lossy()).is_err()) {
|
||||
let source = source.display().to_string();
|
||||
for movie in movies {
|
||||
let _kept = movie.directory.keep();
|
||||
}
|
||||
return Err(EngineError::Other(format!("staged pre-render {source} is now used by the project; its temporary folder was kept")));
|
||||
}
|
||||
// Preflight every destination before replacing any output, including render logs.
|
||||
for (_, destination) in &files {
|
||||
session.refuse_export(&destination.to_string_lossy())?;
|
||||
}
|
||||
for (source, destination) in files {
|
||||
std::fs::rename(&source, &destination).map_err(|e| EngineError::Other(format!("cannot publish pre-render {}: {e}", destination.display())))?;
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = (movies, session);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user