mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Edit: Edit Original opens only regular files with a footage media extension
A folder or bundle, a link to another kind of file, or a file whose extension the footage importer doesn't take is refused with a message instead of being handed to the system's default app.
This commit is contained in:
@@ -808,8 +808,28 @@ fn original_path(s: &Session) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether Edit Original may hand `path` to the system's default app: a regular file (after
|
||||
/// following links) whose name, and the name it links to, have a media extension the footage
|
||||
/// importer takes. Anything else (a folder or app bundle, a script, a shortcut) is refused.
|
||||
pub(crate) fn openable_original(path: &str) -> std::result::Result<(), String> {
|
||||
let media = |p: &std::path::Path| {
|
||||
let ext = p.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default();
|
||||
crate::media_browser::kind_of(&p.to_string_lossy()).is_some_and(|k| k != "project") || matches!(ext.as_str(), "pdf" | "ai" | "eps")
|
||||
};
|
||||
let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| path.to_string());
|
||||
let real = std::fs::canonicalize(path).map_err(|e| format!("cannot open “{name}”: {e}"))?;
|
||||
if !real.is_file() {
|
||||
return Err(format!("“{name}” is not a file"));
|
||||
}
|
||||
if !media(std::path::Path::new(path)) || !media(&real) {
|
||||
return Err(format!("Edit Original opens media files only; “{name}” is not one"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn edit_original(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let path = original_path(s).ok_or_else(|| super::bad("edit.editOriginal", "select a footage item or layer"))?;
|
||||
openable_original(&path).map_err(|e| super::bad("edit.editOriginal", e))?;
|
||||
let url = super::layer_menu::file_url(&path);
|
||||
s.events.push(crate::Event::OpenUrl(url.clone()));
|
||||
Ok(json!({"url": url}))
|
||||
@@ -952,20 +972,67 @@ fn always_ok(_: &Session) -> std::result::Result<(), String> {
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
fn tmp(name: &str) -> std::path::PathBuf {
|
||||
let d = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/test-out").join(name);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// Edit Original opens the footage as a well-formed `file://` URL, whatever its name contains.
|
||||
#[test]
|
||||
fn edit_original_opens_an_encoded_file_url() {
|
||||
let clip = tmp("edit-original-url").join("take #1.mov");
|
||||
std::fs::write(&clip, b"").unwrap();
|
||||
let clip = clip.to_string_lossy().into_owned();
|
||||
let url = super::super::layer_menu::file_url(&clip);
|
||||
assert!(url.ends_with("/take%20%231.mov"), "{url}");
|
||||
let mut s = crate::Session::default();
|
||||
let id = s
|
||||
.edit("add", None, |p, _| {
|
||||
let f = effectcraft_project::Footage { path: "/clips/take #1.mov".into(), ..Default::default() };
|
||||
let f = effectcraft_project::Footage { path: clip.clone(), ..Default::default() };
|
||||
Ok(p.add_item("take", effectcraft_color::Label::Lavender, None, effectcraft_project::ItemKind::Footage(f)))
|
||||
})
|
||||
.unwrap();
|
||||
s.state.project_selection = vec![id];
|
||||
s.drain_events();
|
||||
let r = s.execute("edit.editOriginal", json!({})).unwrap();
|
||||
assert_eq!(r["url"], "file:///clips/take%20%231.mov");
|
||||
assert!(s.drain_events().iter().any(|e| matches!(e, crate::Event::OpenUrl(u) if u == "file:///clips/take%20%231.mov")));
|
||||
assert_eq!(r["url"], url.as_str());
|
||||
assert!(s.drain_events().iter().any(|e| matches!(e, crate::Event::OpenUrl(u) if *u == url)));
|
||||
}
|
||||
|
||||
/// Only regular media files reach the system opener.
|
||||
#[test]
|
||||
fn edit_original_opens_media_files_only() {
|
||||
let d = tmp("edit-original-kinds");
|
||||
for f in ["clip.mov", "Still.PNG", "run.bat", "app.desktop", "clip.mov.lnk"] {
|
||||
std::fs::write(d.join(f), b"").unwrap();
|
||||
}
|
||||
for f in ["Tool.app", "folder.mov"] {
|
||||
std::fs::create_dir_all(d.join(f)).unwrap();
|
||||
}
|
||||
let ok = |f: &str| super::openable_original(&d.join(f).to_string_lossy()).is_ok();
|
||||
assert!(ok("clip.mov") && ok("Still.PNG"));
|
||||
for f in ["run.bat", "app.desktop", "clip.mov.lnk", "Tool.app", "folder.mov", "missing.mov", ""] {
|
||||
assert!(!ok(f), "{f}");
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::symlink(d.join("app.desktop"), d.join("link.png")).unwrap();
|
||||
assert!(!ok("link.png"), "a media name linking to another kind of file");
|
||||
}
|
||||
// The command refuses without opening anything.
|
||||
let mut s = crate::Session::default();
|
||||
let path = d.join("run.bat").to_string_lossy().into_owned();
|
||||
let id = s
|
||||
.edit("add", None, |p, _| {
|
||||
let f = effectcraft_project::Footage { path, ..Default::default() };
|
||||
Ok(p.add_item("bat", effectcraft_color::Label::Lavender, None, effectcraft_project::ItemKind::Footage(f)))
|
||||
})
|
||||
.unwrap();
|
||||
s.state.project_selection = vec![id];
|
||||
s.drain_events();
|
||||
assert!(s.execute("edit.editOriginal", json!({})).is_err());
|
||||
assert!(!s.drain_events().iter().any(|e| matches!(e, crate::Event::OpenUrl(_))));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user