Merge pull request #1639 from storytold/fix/relink-windows-paths

Footage: relinking a moved project keeps Windows separators and UNC shares
This commit is contained in:
BFlat
2026-10-16 17:13:59 +07:00
committed by GitHub
3 changed files with 20 additions and 15 deletions
+16 -11
View File
@@ -126,17 +126,18 @@ pub(crate) fn find_moved(services: &dyn Services, project_dir: &str, f: &Footage
})
}
/// The folder a project file is in.
/// The folder a project file is in, with the path's own separator and its leading `/` or UNC `\\`.
pub(crate) fn project_dir(project_path: &str) -> Option<String> {
let p = parts(project_path);
if p.len() < 2 {
return None;
}
let mut dir = join(if project_path.starts_with('/') { "/" } else { "" }, p.get(..p.len() - 1)?);
if !project_path.starts_with('/') {
dir = dir.trim_start_matches(['/', '\\']).to_string();
}
Some(dir)
let dir = p.get(..p.len().checked_sub(1)?).filter(|d| !d.is_empty())?;
let root = if project_path.starts_with("\\\\") {
"\\\\"
} else if project_path.starts_with('/') {
"/"
} else {
""
};
Some(format!("{root}{}", dir.join(&sep_of(project_path).to_string())))
}
/// Relink `found` in one undo step and say how many.
@@ -262,6 +263,10 @@ mod tests {
);
assert_eq!(project_dir("/x/y/p.ecproj").as_deref(), Some("/x/y"));
assert_eq!(project_dir("p.ecproj"), None);
// Windows paths keep their separator, and a UNC share its `\\`.
assert_eq!(project_dir("C:\\Work\\Job\\p.ecproj").as_deref(), Some("C:\\Work\\Job"));
assert_eq!(project_dir("\\\\server\\share\\Job\\p.ecproj").as_deref(), Some("\\\\server\\share\\Job"));
assert_eq!(moved_candidates("\\\\server\\share\\Job", "D:\\Old\\Job\\shots\\a.png")[2], "\\\\server\\share\\Job\\shots\\a.png");
// Hostile input doesn't panic.
for (a, b) in [("", ""), ("/", "/"), ("x", ""), ("\\\\", "//"), ("/a", "/a/b/c")] {
let _ = moved_root(a, b);
@@ -302,11 +307,11 @@ mod tests {
let b = add(&mut s, "/gone/proj/shots/b.mov");
let m = add(&mut s, "/gone/proj/audio/m.wav");
let other = add(&mut s, "/elsewhere/c.mov");
let target = new.join("shots/a.mov").to_string_lossy().to_string();
let target = new.join("shots").join("a.mov").to_string_lossy().to_string();
let r = s.execute("footage.relink", json!({"item": a.0, "path": target})).unwrap();
assert_eq!(r["relinked"], json!([a.0, b.0, m.0]), "{r}");
assert_eq!(r["stillMissing"], 1);
assert_eq!(path_of(&s, m), new.join("audio/m.wav").to_string_lossy());
assert_eq!(path_of(&s, m), new.join("audio").join("m.wav").to_string_lossy());
assert_eq!(path_of(&s, other), "/elsewhere/c.mov");
// One undo step brings every path back.
s.execute("edit.undo", json!({})).unwrap();
+2 -2
View File
@@ -254,7 +254,7 @@ mod tests {
std::fs::write(d.join("Job/shots/a.png"), b"x").unwrap();
// Saved on another machine: the footage sat next to the project in /Users/x/Job.
let mut s = session_with(&["/Users/x/Job/shots/a.png", "/Users/x/elsewhere/b.png"]);
let path = d.join("Job/p.ecproj");
let path = d.join("Job").join("p.ecproj");
s.execute("file.saveAs", json!({"path": path.to_string_lossy()})).unwrap();
let mut o = Session::default();
o.execute("file.open", json!({"path": path.to_string_lossy()})).unwrap();
@@ -269,7 +269,7 @@ mod tests {
_ => None,
})
.collect();
assert!(paths.contains(&d.join("Job/shots/a.png").to_string_lossy().to_string()), "{paths:?}");
assert!(paths.contains(&d.join("Job").join("shots").join("a.png").to_string_lossy().to_string()), "{paths:?}");
assert!(!o.is_dirty(), "finding the files is not an edit");
let _ = std::fs::remove_dir_all(d);
}
+2 -2
View File
@@ -38,14 +38,14 @@ fn replacing_a_missing_file_relinks_the_others_that_moved_with_it() {
let before = footage_paths(&s);
assert_eq!(before.len(), 2, "{before:?}");
// The job folder moves; both files go missing.
let new = root.join("Archive/Job");
let new = root.join("Archive").join("Job");
std::fs::create_dir_all(root.join("Archive")).unwrap();
std::fs::rename(&old, &new).unwrap();
let r = s.execute("footage.check", json!({"wait": true})).unwrap();
assert_eq!(r["missing"], 2, "{r}");
// Locating one finds the other.
let a = before.iter().find(|(_, p, _)| p.ends_with("a.png")).unwrap().0;
let r = s.execute("file.replaceFootage", json!({"item": a, "path": new.join("stills/a.png").to_string_lossy()})).unwrap();
let r = s.execute("file.replaceFootage", json!({"item": a, "path": new.join("stills").join("a.png").to_string_lossy()})).unwrap();
assert_eq!(r["relinked"].as_array().map(Vec::len), Some(1), "{r}");
let after = footage_paths(&s);
assert!(after.iter().all(|(_, p, missing)| p.starts_with(&*new.to_string_lossy()) && !missing), "{after:?}");