mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Links: Find in Folder… looks for missing linked files in a folder and its subfolders
Find in Folder… in Linked File Not Found and in the Links panel menu
looks in a folder the user picks, and in its subfolders, for the files
with the names of missing linked files, in any letter case. In the
Links panel menu it looks for the selected linked files when there are
any. It uses the folder search of the Missing Fonts dialog. Find
Linked Files lists each file it looks for with the files found for it.
A file with the size and contents saved with the link comes first. A
file of the saved size comes next when the link saved no hash or the
file is larger than 1 GB, and any other file of the name comes last.
Within each group, a file named in the same letter case comes first.
Then each file's folder names are compared with the saved path's, from
the innermost outward, up to the first one that differs, and a file
with more matching names comes before one with fewer. A file is chosen
when it is the only file found, or the only one with the saved
contents, and the user can choose another file or Don't Relink.
Relink relinks each file chosen, and each relink is one undo step.
With Copy into the document's folder, which shows for a saved
document, each image's file is copied next to the document first. The
file's name, then Name 2.png, Name 3.png and so on, are tried in turn
until one is free, which gets the copy, or is a file with the same
contents, which is used. A placed document is relinked where its file
is. The status line names the first file that fails to relink, and the
other files still relink. When Find Linked Files opens from Linked
File Not Found, the missing files left without a choice, and any that
fail to relink, go back to that dialog. On the web, Find in Folder… is
disabled in the Links panel menu, and links.findFiles can't start a
search.
- links.findFiles searches on separate threads and returns at once;
links.findFiles {} reports the search, and {stop: true} stops it. It
looks for at most 1,000 linked files at once: the active document's
missing ones, or those of the objects it is given. It hashes a file
only when a link of its name saved the file's size and a hash, and
only when the file is 1 GB or smaller. It ends once every linked
file has its same file, and it searches only the folders
text.findFontFiles may search, with the same maxSeconds, depth and
count limits.
- links.relink {copy} copies the file it is given into the saved
document's folder and links images to the copy; a call that includes
a placed document fails. Each name it writes is checked against the
automation write root. text.addFontFiles and links.relink {copy}
make their copies with fileio::copy_new, which never replaces a
file. The text.addFontFiles params text and docs/mcp.md say which
names are tried for a copy.
- vectorcraft-doc adds links::hash_reader, which hashes what a reader
reads as hash_bytes hashes bytes.
- ui.findLinksInFolder is the Links panel's Find in Folder… for agents;
docs/control-protocol.md lists the dialog's fields.
Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md.
This commit is contained in:
@@ -66,7 +66,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Relink",
|
||||
[],
|
||||
None,
|
||||
"{ids?: [image or placed document ids] (default: the selected ones; with folder and nothing selected, every missing link), path | folder} link images (or placed documents, each showing the same artboard of the new file) to the file at path, or each to the file of its link's name in folder; embedded images become linked; each keeps its bounds. One undo step → {relinked: [ids], notFound: [file names]}",
|
||||
"{ids?: [image or placed document ids] (default: the selected ones; with folder and nothing selected, every missing link), path | folder, copy?: false (with path, for images of a saved document: copy the file into the document's folder and link to the copy; the names Name.png, Name 2.png … Name 99.png are tried in turn until one is free, which gets the copy, or is a file with the same contents, which is used)} link images (or placed documents, each showing the same artboard of the new file) to the file at path, or each to the file of its link's name in folder; embedded images become linked; each keeps its bounds. One undo step → {relinked: [ids], notFound: [file names], copied?: [{from, to}], kept?: [{from, to}] (with copy)}",
|
||||
has_doc,
|
||||
relink
|
||||
),
|
||||
@@ -322,6 +322,17 @@ pub(crate) fn linked_files(d: &Document, doc_path: Option<&str>) -> Vec<LinkedFi
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The linked files of the open document `st` that `links.findFiles` looks for, each with the
|
||||
/// objects in the layers that show it: the files of objects `ids` whatever their status, else the
|
||||
/// files that can't be found.
|
||||
pub(crate) fn files_to_find(st: &crate::DocState, ids: Option<&[NodeId]>) -> Vec<(LinkInfo, Vec<NodeId>)> {
|
||||
groups(&st.doc, ids)
|
||||
.into_iter()
|
||||
.filter(|g| !g.ids.is_empty() && (ids.is_some() || probe(g, folder(st.path.as_deref())).0 == Status::Missing))
|
||||
.map(|g| (g.link, g.ids))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ---------- reading files into the document ----------
|
||||
|
||||
/// Keep `full` (a file's pixels) as image `key` of `d`, unless pixels are there already; a
|
||||
@@ -731,6 +742,15 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "links.relink";
|
||||
let st = s.doc()?;
|
||||
let folder_param = str_param(p, "folder");
|
||||
// With copy: the saved document's folder, which the file is copied into.
|
||||
let copy_to = match (bool_or(p, "copy", false), str_param(p, "path")) {
|
||||
(false, _) => None,
|
||||
(true, None) => return Err(bad(C, "copy needs path: the file to copy into the document's folder")),
|
||||
(true, Some(_)) => {
|
||||
let doc = st.path.as_deref().map(absolute_path).ok_or_else(|| bad(C, "save the document first: copy puts the file in its folder"))?;
|
||||
Some(Path::new(&doc).parent().map(Path::to_path_buf).ok_or_else(|| bad(C, format!("{doc} has no folder")))?)
|
||||
}
|
||||
};
|
||||
// An image or a placed document, with its link.
|
||||
let image = |id: &NodeId| {
|
||||
st.doc.node(*id).and_then(|n| match &n.kind {
|
||||
@@ -759,6 +779,9 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if ids.is_empty() {
|
||||
return Err(bad(C, "select the images to relink, or pass ids"));
|
||||
}
|
||||
if copy_to.is_some() && ids.iter().any(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(NodeKind::PlacedDocument(_)))) {
|
||||
return Err(bad(C, "copy takes images only: relink placed documents where their files are"));
|
||||
}
|
||||
// (ids, the file) per file to read.
|
||||
let mut files: Vec<(Vec<NodeId>, String)> = vec![];
|
||||
let mut not_found = vec![];
|
||||
@@ -778,9 +801,9 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(None, None) => return Err(bad(C, "give path (a file) or folder")),
|
||||
}
|
||||
let mut reads = vec![];
|
||||
let (mut copied, mut kept) = (vec![], vec![]);
|
||||
for (ids, path) in files {
|
||||
let bytes = read_file(&path)?;
|
||||
let link = link_info(&path, &bytes);
|
||||
// Images show the file; placed documents each show their artboard of it.
|
||||
let mut images = vec![];
|
||||
let mut documents: Vec<((u32, bool), Vec<NodeId>)> = vec![];
|
||||
@@ -796,12 +819,30 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ => images.push(id),
|
||||
}
|
||||
}
|
||||
if !images.is_empty() {
|
||||
let img = fileio::raster_image(&bytes).map_err(|e| bad(C, format!("{path}: {e}")))?;
|
||||
// Read before it is copied: a file that can't be shown isn't copied.
|
||||
let img = if images.is_empty() { None } else { Some(fileio::raster_image(&bytes).map_err(|e| bad(C, format!("{path}: {e}")))?) };
|
||||
let mut sources = vec![];
|
||||
for ((page, bounding), ids) in documents {
|
||||
sources.push((page, ids, super::place::document::source(&path, &bytes, Some(&path), page, bounding, C)?));
|
||||
}
|
||||
let to = match ©_to {
|
||||
Some(dir) => {
|
||||
let (to, new) = copy_into_folder(&path, &bytes, dir)?;
|
||||
let pair = json!({ "from": path, "to": to });
|
||||
if new {
|
||||
copied.push(pair)
|
||||
} else {
|
||||
kept.push(pair)
|
||||
}
|
||||
to
|
||||
}
|
||||
None => path,
|
||||
};
|
||||
let link = link_info(&to, &bytes);
|
||||
if let Some(img) = img {
|
||||
reads.push((images, Read::Image((link.clone(), img))));
|
||||
}
|
||||
for ((page, bounding), ids) in documents {
|
||||
let src = super::place::document::source(&path, &bytes, Some(&path), page, bounding, C)?;
|
||||
for (page, ids, src) in sources {
|
||||
reads.push((ids, Read::Document(LinkInfo { page: Some(page), ..link.clone() }, src)));
|
||||
}
|
||||
}
|
||||
@@ -811,7 +852,43 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
not_found.sort();
|
||||
not_found.dedup();
|
||||
Ok(json!({ "relinked": relinked, "notFound": not_found }))
|
||||
let mut out = json!({ "relinked": relinked, "notFound": not_found });
|
||||
if copy_to.is_some() {
|
||||
out["copied"] = json!(copied);
|
||||
out["kept"] = json!(kept);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `links.relink {copy}`: put the file at `from` (`bytes`, as just read) into the folder `dir`,
|
||||
/// never replacing a file there ([`fileio::copy_new`]) → where it is now, and whether it was
|
||||
/// copied (`false`: a file there with the same contents, of its name or a numbered one, is used).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn copy_into_folder(from: &str, bytes: &[u8], dir: &Path) -> Result<(String, bool)> {
|
||||
use std::io::Write as _;
|
||||
let source = Path::new(from);
|
||||
let name = source.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
// Each name it takes is checked against the automation roots in force.
|
||||
let create = |to: &Path| {
|
||||
crate::file_access::check_write(&to.to_string_lossy()).map_err(|e| std::io::Error::new(std::io::ErrorKind::PermissionDenied, e))?;
|
||||
vectorcraft_format::write_new_with(to, |f| f.write_all(bytes))
|
||||
};
|
||||
// A file there that automation may not read isn't taken for the same file.
|
||||
let same = |to: &Path, m: &std::fs::Metadata| {
|
||||
m.len() == bytes.len() as u64 && crate::file_access::check_read(&to.to_string_lossy()).is_ok() && std::fs::read(to).is_ok_and(|b| b == bytes)
|
||||
};
|
||||
match fileio::copy_new(source, dir, create, same) {
|
||||
Ok(Some(fileio::Placed::Copied(to))) => Ok((to.to_string_lossy().into_owned(), true)),
|
||||
Ok(Some(fileio::Placed::Kept(to))) => Ok((to.to_string_lossy().into_owned(), false)),
|
||||
Ok(None) => Err(EngineError::Other(format!("every numbered name for {name} is taken in the document's folder"))),
|
||||
Err(e) => Err(EngineError::Other(format!("{}: {e}", dir.display()))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The web has no file system: nothing is copied.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn copy_into_folder(_: &str, _: &[u8], _: &Path) -> Result<(String, bool)> {
|
||||
Err(bad("links.relink", "copying a file isn't available on the web"))
|
||||
}
|
||||
|
||||
// ---------- the Links panel ----------
|
||||
|
||||
Reference in New Issue
Block a user