mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 18:54:57 +00:00
Merge pull request #6231 from dotCooCoo/fix/missing-link-relative-path
Links: a missing linked file keeps its relative folder when a moved document is saved
This commit is contained in:
@@ -794,11 +794,14 @@ fn new_document_from_state(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let mut doc = Document::clone(chosen.as_ref().unwrap_or(&st.doc));
|
||||
let (imported_from, import_losses, opened_by) = (st.imported_from.clone(), st.import_losses.clone(), st.opened_by.clone());
|
||||
if let Some(path) = st.path.as_deref() {
|
||||
let base = super::links::LinkBase::of(st);
|
||||
if base.doc.is_some() {
|
||||
// Files are looked for within the asking caller's roots, else within those of the caller that opened the source.
|
||||
let roots = crate::file_access::current().or_else(|| opened_by.clone());
|
||||
crate::file_access::confine(roots.as_ref(), || super::links::with_found_paths(&mut doc, path));
|
||||
crate::file_access::confine(roots.as_ref(), || super::links::with_found_paths(&mut doc, base));
|
||||
}
|
||||
// The copy's links keep their relative paths, which name places from the same file.
|
||||
let links_from = base.relative.map(str::to_string);
|
||||
doc.drop_edit_modes();
|
||||
if doc.pattern_edit.is_some() {
|
||||
super::patterncmds::finish_edit(&mut doc, &mut vectorcraft_doc::Selection::default())?;
|
||||
@@ -815,6 +818,7 @@ fn new_document_from_state(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc_mut()?;
|
||||
st.imported_from = imported_from;
|
||||
st.import_losses = import_losses;
|
||||
st.links_from = links_from;
|
||||
st.opened_by = st.opened_by.take().or(opened_by);
|
||||
st.mark_unsaved();
|
||||
Ok(json!({ "index": i, "title": st.title() }))
|
||||
|
||||
@@ -570,7 +570,7 @@ impl ReadFile {
|
||||
pub fn read_for_open(name: &str, bytes: &[u8], path: Option<String>, p: &Value, update_links: bool, low_res_proxy_eps: bool) -> Result<ReadFile> {
|
||||
let opts = LoadOptions::from_params("document.open", p)?;
|
||||
let mut loaded = load_with(name, bytes, &opts)?;
|
||||
let links = crate::cmd::links::resolve(&mut loaded.doc, path.as_deref(), update_links, low_res_proxy_eps);
|
||||
let links = crate::cmd::links::resolve(&mut loaded.doc, crate::cmd::links::LinkBase::file(path.as_deref()), update_links, low_res_proxy_eps);
|
||||
Session::refresh_text_bounds(&mut loaded.doc);
|
||||
Ok(ReadFile { loaded, opts, path, links })
|
||||
}
|
||||
@@ -590,7 +590,8 @@ pub fn open_template(s: &mut Session, name: &str, bytes: &[u8], path: Option<&st
|
||||
"only this Affinity file's embedded preview could be read: use File › Open to see it with its warning, or export SVG or PDF from Affinity first",
|
||||
));
|
||||
}
|
||||
let links = crate::cmd::links::resolve(&mut loaded.doc, path, s.prefs.update_links == "automatically", s.prefs.low_res_proxy_eps);
|
||||
let base = crate::cmd::links::LinkBase::file(path);
|
||||
let links = crate::cmd::links::resolve(&mut loaded.doc, base, s.prefs.update_links == "automatically", s.prefs.low_res_proxy_eps);
|
||||
open_loaded(s, loaded, links, path.map(str::to_string), &LoadOptions::default(), true)
|
||||
}
|
||||
|
||||
@@ -629,6 +630,7 @@ fn open_loaded(s: &mut Session, loaded: Loaded, links: Resolved, path: Option<St
|
||||
// writes .ai that way).
|
||||
let partial = format.id == "pdf" && opts.is_partial();
|
||||
let lossy_ai = format.id == "ai" && !restored;
|
||||
let read_from = path.clone();
|
||||
let path = path.filter(|_| !template && !partial && !lossy_ai && SAVE_FORMATS.contains(&format.id));
|
||||
let saves_back = path.is_some();
|
||||
let converted = saves_back && converted && s.prefs.append_converted;
|
||||
@@ -648,6 +650,7 @@ fn open_loaded(s: &mut Session, loaded: Loaded, links: Resolved, path: Option<St
|
||||
st.converted = converted;
|
||||
st.imported_from = source_path;
|
||||
st.import_losses = losses;
|
||||
st.links_from = read_from;
|
||||
let opened = &s.documents()[index];
|
||||
let (title, file_info_title) = (opened.title(), opened.doc.title.clone());
|
||||
Ok(super::merge(
|
||||
|
||||
@@ -777,7 +777,8 @@ fn job_for_stamped(s: &Session, st: &DocState, plan: SavePlan, stamped: Option<i
|
||||
stamp_doc_dates(Arc::make_mut(&mut snapshot), at);
|
||||
}
|
||||
// A native file records its links' paths relative to where it is written.
|
||||
let relative = plan.path.as_deref().filter(|_| is_native(plan.format)).and_then(|p| crate::cmd::links::with_relative_paths(&own, p));
|
||||
let base = crate::cmd::links::LinkBase::of(st);
|
||||
let relative = plan.path.as_deref().filter(|_| is_native(plan.format)).and_then(|p| crate::cmd::links::with_relative_paths(&own, base, p));
|
||||
let doc = relative.map_or(own, Arc::new);
|
||||
let mut params = plan.options.clone();
|
||||
if is_svg(plan.format) {
|
||||
|
||||
+147
-60
@@ -23,7 +23,7 @@
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::{Component, Path};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
@@ -219,6 +219,30 @@ struct Found {
|
||||
modified: Option<u64>,
|
||||
/// Read when telling whether it changed needed them.
|
||||
bytes: Option<Vec<u8>>,
|
||||
/// `path` relative to the folder of [`LinkBase::relative`], when [`locate`] had that file.
|
||||
relative: Option<Option<String>>,
|
||||
}
|
||||
|
||||
/// The files a document's links are looked for from: `doc`, whose folder a link's path is followed
|
||||
/// from and its file is looked for by name in, and `relative`, the file its links' stored relative
|
||||
/// paths name places from (the file the document was read from).
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct LinkBase<'a> {
|
||||
pub doc: Option<&'a str>,
|
||||
pub relative: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> LinkBase<'a> {
|
||||
/// Both the file at `path`.
|
||||
pub fn file(path: Option<&'a str>) -> Self {
|
||||
Self { doc: path, relative: path }
|
||||
}
|
||||
|
||||
/// The open document `st`'s. One with no file of its own looks beside the file it was read from.
|
||||
pub(crate) fn of(st: &'a crate::DocState) -> Self {
|
||||
let (path, from) = (st.path.as_deref(), st.links_from.as_deref());
|
||||
Self { doc: path.or(from), relative: from.or(path) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Found {
|
||||
@@ -237,7 +261,17 @@ impl Found {
|
||||
|
||||
/// [`Self::link`] with the bytes' hash already taken.
|
||||
fn link_hashed(&self, hash: String, base: &LinkInfo) -> LinkInfo {
|
||||
LinkInfo { path: self.path.clone(), size: Some(self.size), modified: self.modified, hash: Some(hash), ..base.clone() }
|
||||
LinkInfo { size: Some(self.size), modified: self.modified, hash: Some(hash), ..self.moved(base) }
|
||||
}
|
||||
|
||||
/// `base` pointing at this file. A file found away from `base`'s path takes its relative path
|
||||
/// from where it was found.
|
||||
fn moved(&self, base: &LinkInfo) -> LinkInfo {
|
||||
let relative = match &self.relative {
|
||||
Some(found) if self.path != base.path => found.clone(),
|
||||
_ => base.relative.clone(),
|
||||
};
|
||||
LinkInfo { path: self.path.clone(), relative, ..base.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,15 +289,41 @@ pub(crate) fn link_info(path: &str, bytes: &[u8]) -> LinkInfo {
|
||||
/// The file at `path` when there is one.
|
||||
fn found_at(path: String) -> Option<Found> {
|
||||
let (size, modified) = file_stamp(&path)?;
|
||||
Some(Found { path, size, modified, bytes: None })
|
||||
Some(Found { path, size, modified, bytes: None, relative: None })
|
||||
}
|
||||
|
||||
/// Where the file of `link` (stored in the document saved at `doc_path`) is: at its path, else at
|
||||
/// its relative path or by name in the document's folder. Only paths
|
||||
/// [`fileio::document_path`] follows are looked at.
|
||||
fn locate(link: &LinkInfo, doc_path: Option<&str>) -> Option<Found> {
|
||||
let stored = [Some(link.path.as_str()), link.relative.as_deref(), Some(link.name())];
|
||||
stored.into_iter().flatten().filter_map(|p| fileio::document_path(p, doc_path).ok()).find_map(found_at)
|
||||
/// Where the file of `link` (of a document looking from `base`) is: at its path, else at its
|
||||
/// relative path from the folder of [`LinkBase::relative`], then from the document's folder, else
|
||||
/// by name in the document's folder, then in that of [`LinkBase::relative`]. Only paths
|
||||
/// [`fileio::document_path`] follows from the document's own file are looked at.
|
||||
fn locate(link: &LinkInfo, base: LinkBase) -> Option<Found> {
|
||||
let (relative, other) = (link.relative.as_deref(), base.doc != base.relative);
|
||||
let stored = [
|
||||
(Some(link.path.as_str()), base.doc),
|
||||
(relative, base.relative),
|
||||
(relative.filter(|_| other), base.doc),
|
||||
(Some(link.name()), base.doc),
|
||||
(other.then_some(link.name()), base.relative),
|
||||
];
|
||||
let mut f = stored.into_iter().filter_map(|(p, from)| follow_from(p?, from, base.doc, fileio::document_path)).find_map(found_at)?;
|
||||
f.relative = base.relative.map(|from| {
|
||||
let from = absolute_path(from);
|
||||
Path::new(&from).parent().and_then(|dir| relative_path(Path::new(&f.path), dir))
|
||||
});
|
||||
Some(f)
|
||||
}
|
||||
|
||||
/// The place `stored` names from the file `from`, as `resolve` ([`fileio::document_path`]) follows
|
||||
/// it, when it also follows that place from the document's own file `doc`: a network share the
|
||||
/// document is no longer on is refused.
|
||||
pub(crate) fn follow_from(
|
||||
stored: &str,
|
||||
from: Option<&str>,
|
||||
doc: Option<&str>,
|
||||
resolve: impl Fn(&str, Option<&str>) -> std::result::Result<String, craft_docpath::Refused>,
|
||||
) -> Option<String> {
|
||||
let path = resolve(stored, from).ok()?;
|
||||
if from == doc { Some(path) } else { resolve(&path, doc).ok() }
|
||||
}
|
||||
|
||||
/// Why the path of `link` (stored in the document saved at `doc_path`) isn't followed, when it
|
||||
@@ -272,10 +332,10 @@ fn refused(link: &LinkInfo, doc_path: Option<&str>) -> Option<craft_docpath::Ref
|
||||
fileio::document_path(&link.path, doc_path).err().filter(|r| *r != craft_docpath::Refused::Invalid)
|
||||
}
|
||||
|
||||
/// Where the file of group `g` (of the document saved at `doc_path`) is and whether it still has
|
||||
/// Where the file of group `g` (of a document looking from `base`) is and whether it still has
|
||||
/// the content shown. Its bytes are read only when its size or modification time changed.
|
||||
fn probe(g: &Group, doc_path: Option<&str>) -> (Status, Option<Found>) {
|
||||
let Some(mut f) = locate(&g.link, doc_path) else { return (Status::Missing, None) };
|
||||
fn probe(g: &Group, base: LinkBase) -> (Status, Option<Found>) {
|
||||
let Some(mut f) = locate(&g.link, base) else { return (Status::Missing, None) };
|
||||
let l = &g.link;
|
||||
if l.size == Some(f.size) && l.modified.is_some() && l.modified == f.modified {
|
||||
return (Status::Ok, Some(f));
|
||||
@@ -328,9 +388,9 @@ pub(crate) struct LinkedFile {
|
||||
pub bytes: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Every file the linked images of `d` (saved at `doc_path`) show, in the layers, symbols and
|
||||
/// Every file the linked images of `d` (looking from `base`) show, in the layers, symbols and
|
||||
/// patterns, once each.
|
||||
pub(crate) fn linked_files(d: &Document, doc_path: Option<&str>) -> Vec<LinkedFile> {
|
||||
pub(crate) fn linked_files(d: &Document, base: LinkBase) -> Vec<LinkedFile> {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
groups(d, None)
|
||||
.into_iter()
|
||||
@@ -338,7 +398,7 @@ pub(crate) fn linked_files(d: &Document, doc_path: Option<&str>) -> Vec<LinkedFi
|
||||
.map(|g| {
|
||||
let named = fileio::format_for_name(g.link.name()).or_else(|| fileio::readable_format_for_name(g.link.name()));
|
||||
let own = |b: &&ImageBlob| !b.is_proxy() && named.is_some_and(|f| f.mime == b.mime);
|
||||
let found = locate(&g.link, doc_path);
|
||||
let found = locate(&g.link, base);
|
||||
let found_path = found.as_ref().map(|f| f.path.clone());
|
||||
let bytes = found.and_then(|mut f| f.read().ok()).or_else(|| d.images.get(&g.key).filter(own).map(|b| b.bytes.to_vec()));
|
||||
LinkedFile { path: g.link.path.clone(), name: g.link.name().to_string(), found_path, bytes }
|
||||
@@ -348,7 +408,7 @@ pub(crate) fn linked_files(d: &Document, doc_path: Option<&str>) -> Vec<LinkedFi
|
||||
|
||||
/// `d` for output: linked raster images that still show only their saved preview read their files
|
||||
/// again (Preferences › Use Low Resolution Proxy for Linked EPS keeps the preview on screen only).
|
||||
pub(crate) fn full_linked_images<'a>(d: &'a Document, doc_path: Option<&str>) -> (Cow<'a, Document>, Vec<String>) {
|
||||
pub(crate) fn full_linked_images<'a>(d: &'a Document, base: LinkBase) -> (Cow<'a, Document>, Vec<String>) {
|
||||
let mut keys = BTreeSet::new();
|
||||
d.visit_images(|_, im| {
|
||||
if im.link.is_some() && d.images.get(&im.key).is_some_and(ImageBlob::is_proxy) {
|
||||
@@ -363,7 +423,7 @@ pub(crate) fn full_linked_images<'a>(d: &'a Document, doc_path: Option<&str>) ->
|
||||
let all = groups(d, None);
|
||||
for key in keys {
|
||||
let Some(g) = all.iter().find(|g| g.document.is_none() && g.key == key) else { continue };
|
||||
let Some(mut f) = locate(&g.link, doc_path) else {
|
||||
let Some(mut f) = locate(&g.link, base) else {
|
||||
previews.insert(key);
|
||||
continue;
|
||||
};
|
||||
@@ -398,7 +458,7 @@ pub(crate) fn full_linked_images<'a>(d: &'a Document, doc_path: Option<&str>) ->
|
||||
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, st.path.as_deref()).0 == Status::Missing))
|
||||
.filter(|g| !g.ids.is_empty() && (ids.is_some() || probe(g, LinkBase::of(st)).0 == Status::Missing))
|
||||
.map(|g| (g.link, g.ids))
|
||||
.collect()
|
||||
}
|
||||
@@ -847,13 +907,13 @@ impl Resolved {
|
||||
}
|
||||
}
|
||||
|
||||
/// The linked files of document `d`, just read from `doc_path`: those found unchanged are read
|
||||
/// again (their images had the preview), modified ones are read again when `update`, links found
|
||||
/// away from their path take the new path. A link nothing is known of whose image, or placed
|
||||
/// The linked files of document `d`, just read and looking from `base`: those found unchanged are
|
||||
/// read again (their images had the preview), modified ones are read again when `update`, links
|
||||
/// found away from their path take the new path. A link nothing is known of whose image, or placed
|
||||
/// document, shows a preview (a file another app wrote kept only that) was never read: its file is
|
||||
/// read now, into the object's bounds, which isn't a change to report. A placed document whose
|
||||
/// file's bytes are gone isn't showing a preview. Nothing here is an undo step.
|
||||
pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool, low_res_proxy_eps: bool) -> Resolved {
|
||||
pub fn resolve(d: &mut Document, base: LinkBase, update: bool, low_res_proxy_eps: bool) -> Resolved {
|
||||
let mut out = Resolved::default();
|
||||
// The image (with its preview) and hash of each file read for a preview here, by path: images
|
||||
// of other keys linking to the same file share them, rather than each reading, decoding and
|
||||
@@ -863,14 +923,14 @@ pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool, low_res_p
|
||||
// away from its path or read again.
|
||||
let mut moved: Vec<(String, String, bool, LinkInfo)> = vec![];
|
||||
for g in groups(d, None) {
|
||||
let row = rows(std::iter::once(&g), doc_path);
|
||||
let row = rows(std::iter::once(&g), base.doc);
|
||||
let shows_preview = match g.document {
|
||||
None => previewing(d, &g.key),
|
||||
Some(_) => d.images.get(&g.key).is_some_and(ImageBlob::is_proxy),
|
||||
};
|
||||
let unread = g.link.hash.is_none() && g.link.size.is_none() && shows_preview;
|
||||
let eps_preview = keep_eps_preview(low_res_proxy_eps, &g, d);
|
||||
match probe(&g, doc_path) {
|
||||
match probe(&g, base) {
|
||||
(Status::Missing, _) => out.missing.extend(row),
|
||||
(Status::Modified, Some(mut f)) if update || unread => {
|
||||
let force = unread.then_some(Preserve::Bounds);
|
||||
@@ -894,7 +954,7 @@ pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool, low_res_p
|
||||
None => {
|
||||
let link = match f.bytes.take() {
|
||||
Some(b) => f.link(&b, &g.link),
|
||||
None => LinkInfo { path: f.path.clone(), ..g.link.clone() },
|
||||
None => f.moved(&g.link),
|
||||
};
|
||||
if link != g.link {
|
||||
set_links(d, &g.ids, &link);
|
||||
@@ -932,7 +992,7 @@ pub fn resolve(d: &mut Document, doc_path: Option<&str>, update: bool, low_res_p
|
||||
// Found away from its path, or read again: the link follows.
|
||||
let link = match &bytes {
|
||||
Some(b) => f.link(b, &g.link),
|
||||
None => LinkInfo { path: f.path.clone(), ..g.link.clone() },
|
||||
None => f.moved(&g.link),
|
||||
};
|
||||
if link != g.link {
|
||||
set_links(d, &g.ids, &link);
|
||||
@@ -1035,42 +1095,68 @@ fn relative_path(target: &Path, base: &Path) -> Option<String> {
|
||||
Some(ups.chain(rest).collect::<Vec<std::borrow::Cow<str>>>().join("/"))
|
||||
}
|
||||
|
||||
/// `d` as saved to `dest`: the links' paths relative to its folder written in; `None` when they
|
||||
/// are already. Links in the layers, symbols, graph designs, patterns and opacity masks are all set.
|
||||
pub fn with_relative_paths(d: &Document, dest: &str) -> Option<Document> {
|
||||
let dest = absolute_path(dest);
|
||||
let dir = Path::new(&dest).parent()?;
|
||||
let relative = |l: &LinkInfo| relative_path(Path::new(&l.path), dir);
|
||||
let outdated = |l: &LinkInfo| relative(l) != l.relative;
|
||||
let mut changed = false;
|
||||
d.visit_images(|_, im| changed |= im.link.as_ref().is_some_and(outdated));
|
||||
d.visit_placed(|_, p| changed |= outdated(&p.link));
|
||||
if !changed {
|
||||
return None;
|
||||
/// The file `link` (of a document looking from `base`) is written relative to: where [`locate`]
|
||||
/// finds it, else the place its relative path names from the folder of [`LinkBase::relative`],
|
||||
/// else its path.
|
||||
fn link_target(link: &LinkInfo, base: LinkBase) -> PathBuf {
|
||||
if let Some(f) = locate(link, base) {
|
||||
return PathBuf::from(f.path);
|
||||
}
|
||||
match (base.relative, link.relative.as_deref()) {
|
||||
(Some(_), Some(rel)) => fileio::document_path(rel, base.relative).map_or_else(|_| PathBuf::from(&link.path), PathBuf::from),
|
||||
_ => PathBuf::from(&link.path),
|
||||
}
|
||||
let mut d = d.clone();
|
||||
d.update_links_where(outdated, |l| l.relative = relative(l));
|
||||
Some(d)
|
||||
}
|
||||
|
||||
/// Sets each link of `d`, a copy of the document saved at `doc_path` that has no file of its own,
|
||||
/// to the path where that document finds its file, so the copy finds the same files.
|
||||
/// Links in the layers, symbols, graph designs, patterns and opacity masks are all set.
|
||||
pub(crate) fn with_found_paths(d: &mut Document, doc_path: &str) {
|
||||
// The new path of each link by what `locate` reads of it: its path, relative path and name.
|
||||
let mut found: BTreeMap<(String, Option<String>), Option<String>> = BTreeMap::new();
|
||||
/// `d` (looking from `base`) as saved to `dest`: the links' paths relative to its folder written
|
||||
/// in; `None` when they are already. A link whose file is not at its path is written relative to
|
||||
/// where its file is found, else to the place its relative path names. Links in the layers,
|
||||
/// symbols, graph designs, patterns and opacity masks are all set.
|
||||
pub fn with_relative_paths(d: &Document, base: LinkBase, dest: &str) -> Option<Document> {
|
||||
let dest = absolute_path(dest);
|
||||
let dir = Path::new(&dest).parent()?;
|
||||
// The relative path of each link by its path and stored relative path, each file looked at once.
|
||||
let mut wanted: BTreeMap<(String, Option<String>), Option<String>> = BTreeMap::new();
|
||||
let mut look = |l: &LinkInfo| {
|
||||
found.entry((l.path.clone(), l.relative.clone())).or_insert_with(|| locate(l, Some(doc_path)).map(|f| f.path));
|
||||
wanted.entry((l.path.clone(), l.relative.clone())).or_insert_with(|| relative_path(&link_target(l, base), dir));
|
||||
};
|
||||
d.visit_images(|_, im| im.link.iter().for_each(&mut look));
|
||||
d.visit_placed(|_, p| look(&p.link));
|
||||
found.retain(|(path, _), f| f.as_ref().is_some_and(|f| f != path));
|
||||
wanted.retain(|(_, stored), rel| rel != stored);
|
||||
if wanted.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let key = |l: &LinkInfo| (l.path.clone(), l.relative.clone());
|
||||
let mut d = d.clone();
|
||||
d.update_links_where(|l| wanted.contains_key(&key(l)), |l| l.relative = wanted.get(&key(l)).cloned().flatten());
|
||||
Some(d)
|
||||
}
|
||||
|
||||
/// Sets each link of `d`, a copy of a document looking from `base`, to the path where that
|
||||
/// document finds its file, and its relative path to it as that document's links have them, so
|
||||
/// the copy finds the same files. Links in the layers, symbols, graph designs, patterns and opacity
|
||||
/// masks are all set.
|
||||
pub(crate) fn with_found_paths(d: &mut Document, base: LinkBase) {
|
||||
// The new path and relative path of each link by what `locate` reads of it: its path, relative
|
||||
// path and name.
|
||||
type PathAndRelative = (String, Option<String>);
|
||||
let mut found: BTreeMap<PathAndRelative, Option<PathAndRelative>> = BTreeMap::new();
|
||||
let mut look = |l: &LinkInfo| {
|
||||
found.entry((l.path.clone(), l.relative.clone())).or_insert_with(|| {
|
||||
let moved = locate(l, base)?.moved(l);
|
||||
Some((moved.path, moved.relative))
|
||||
});
|
||||
};
|
||||
d.visit_images(|_, im| im.link.iter().for_each(&mut look));
|
||||
d.visit_placed(|_, p| look(&p.link));
|
||||
found.retain(|(path, _), f| f.as_ref().is_some_and(|(f, _)| f != path));
|
||||
if found.is_empty() {
|
||||
return;
|
||||
}
|
||||
d.update_links(|l| {
|
||||
if let Some(Some(f)) = found.get(&(l.path.clone(), l.relative.clone())) {
|
||||
l.path = f.clone();
|
||||
if let Some(Some((path, relative))) = found.get(&(l.path.clone(), l.relative.clone())) {
|
||||
l.path = path.clone();
|
||||
l.relative = relative.clone();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1083,11 +1169,11 @@ fn check(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (mut missing, mut modified) = (0, 0);
|
||||
let mut links = vec![];
|
||||
for g in groups(&st.doc, ids.as_deref()).iter().filter(|g| !g.ids.is_empty()) {
|
||||
let (status, found) = probe(g, st.path.as_deref());
|
||||
let (status, found) = probe(g, LinkBase::of(st));
|
||||
missing += usize::from(status == Status::Missing);
|
||||
modified += usize::from(status == Status::Modified);
|
||||
let mut row = json!({ "name": g.link.name(), "path": g.link.path, "status": status.as_str(), "ids": ids_json(&g.ids) });
|
||||
note_refused(&mut row, &g.link, st.path.as_deref());
|
||||
note_refused(&mut row, &g.link, LinkBase::of(st).doc);
|
||||
if let Some(f) = found.filter(|f| f.path != g.link.path) {
|
||||
row["found"] = json!(f.path);
|
||||
}
|
||||
@@ -1112,7 +1198,7 @@ fn update_where(s: &mut Session, ids: Option<Vec<NodeId>>, path: Option<&str>) -
|
||||
let (mut reads, mut missing) = (vec![], vec![]);
|
||||
let asked = ids.is_some() || path.is_some();
|
||||
for g in groups(&st.doc, ids.as_deref()).into_iter().filter(|g| path.is_none_or(|p| g.document.is_some() && g.link.path == p)) {
|
||||
match probe(&g, st.path.as_deref()) {
|
||||
match probe(&g, LinkBase::of(st)) {
|
||||
(Status::Missing, _) | (_, None) => missing.extend(ids_json(&g.ids)),
|
||||
// Unchanged and showing the file: nothing to read unless asked for.
|
||||
(Status::Ok, Some(_)) if !asked && !stale(&st.doc, &g) => {}
|
||||
@@ -1232,7 +1318,7 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let selected: Vec<NodeId> = st.selection.objects.iter().copied().filter(|id| image(id).is_some()).collect();
|
||||
match folder_param {
|
||||
Some(_) if selected.is_empty() => {
|
||||
groups(&st.doc, None).into_iter().filter(|g| probe(g, st.path.as_deref()).0 == Status::Missing).flat_map(|g| g.ids).collect()
|
||||
groups(&st.doc, None).into_iter().filter(|g| probe(g, LinkBase::of(st)).0 == Status::Missing).flat_map(|g| g.ids).collect()
|
||||
}
|
||||
_ => selected,
|
||||
}
|
||||
@@ -1368,7 +1454,7 @@ fn missing_in_folder(
|
||||
let Some(dir) = files.first().and_then(|(_, p)| Path::new(p).parent()).map(Path::to_path_buf) else { return };
|
||||
for g in groups(&st.doc, None).into_iter().filter(|g| !(images_only && g.document.is_some())) {
|
||||
let rest: Vec<NodeId> = g.ids.iter().copied().filter(|id| !given.contains(id)).collect();
|
||||
if rest.is_empty() || probe(&g, st.path.as_deref()).0 != Status::Missing {
|
||||
if rest.is_empty() || probe(&g, LinkBase::of(st)).0 != Status::Missing {
|
||||
continue;
|
||||
}
|
||||
let found = absolute_path(&dir.join(g.link.name()).to_string_lossy());
|
||||
@@ -1482,7 +1568,7 @@ fn linked_param(st: &crate::DocState, p: &Value, cmd: &str) -> Result<NodeId> {
|
||||
pub(crate) fn statuses(st: &crate::DocState) -> BTreeMap<NodeId, (Status, Option<String>, bool)> {
|
||||
let mut out = BTreeMap::new();
|
||||
for g in groups(&st.doc, None).iter().filter(|g| !g.ids.is_empty()) {
|
||||
let (status, found) = probe(g, st.path.as_deref());
|
||||
let (status, found) = probe(g, LinkBase::of(st));
|
||||
let found = found.map(|f| f.path).filter(|p| *p != g.link.path);
|
||||
let preview = stale(&st.doc, g);
|
||||
out.extend(g.ids.iter().map(|id| (*id, (status, found.clone(), preview))));
|
||||
@@ -1625,7 +1711,7 @@ fn embed(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
done.extend(g.ids);
|
||||
continue;
|
||||
}
|
||||
match probe(&g, st.path.as_deref()) {
|
||||
match probe(&g, LinkBase::of(st)) {
|
||||
(Status::Ok | Status::Modified, Some(mut f)) => match read_image(&mut f, &g.link) {
|
||||
Ok(read) => {
|
||||
done.extend(&g.ids);
|
||||
@@ -1714,8 +1800,9 @@ fn file_fields(st: &crate::DocState, id: NodeId, l: &LinkInfo, out: &mut Value)
|
||||
}
|
||||
|
||||
fn file_fields_here(st: &crate::DocState, id: NodeId, l: &LinkInfo, out: &mut Value) {
|
||||
let status = groups(&st.doc, Some(&[id])).first().map_or(Status::Missing, |g| probe(g, st.path.as_deref()).0);
|
||||
let doc_path = st.path.as_deref();
|
||||
let base = LinkBase::of(st);
|
||||
let status = groups(&st.doc, Some(&[id])).first().map_or(Status::Missing, |g| probe(g, base).0);
|
||||
let doc_path = base.doc;
|
||||
let path = fileio::document_path(&l.path, doc_path).ok();
|
||||
let stamp = path.as_deref().and_then(file_stamp);
|
||||
out["status"] = json!(status.as_str());
|
||||
|
||||
@@ -13,6 +13,7 @@ use vectorcraft_doc::{Document, LinkInfo};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::fileio::{create_dir, write_file};
|
||||
use super::links::LinkBase;
|
||||
use super::{CommandSpec, Result, Session, bad, bool_or, cmd, has_doc, str_param, unique_name};
|
||||
|
||||
const C: &str = "file.package";
|
||||
@@ -187,8 +188,18 @@ impl<'a> Collector<'a> {
|
||||
}
|
||||
|
||||
/// Collects the files `doc` links. `by_name`: `doc` is a placed `.ai` file's editing data,
|
||||
/// whose links are not rewritten, so its copy finds them by name beside it.
|
||||
fn collect(&mut self, doc: &mut Document, source: &str, destination: &str, depth: usize, by_name: bool) -> Result<()> {
|
||||
/// whose links are not rewritten, so its copy finds them by name beside it. `lookup`: the
|
||||
/// document whose links (with the same paths as `doc`'s) are looked for, and where from, when
|
||||
/// not `doc` from `source`.
|
||||
fn collect(
|
||||
&mut self,
|
||||
doc: &mut Document,
|
||||
source: &str,
|
||||
destination: &str,
|
||||
depth: usize,
|
||||
by_name: bool,
|
||||
lookup: Option<(&Document, LinkBase)>,
|
||||
) -> Result<()> {
|
||||
// The caller checks cycles and depth before descending; active documents have
|
||||
// a destination reserved so back-links can point at their single packaged copy.
|
||||
self.visiting.insert(source.to_string(), destination.to_string());
|
||||
@@ -210,6 +221,10 @@ impl<'a> Collector<'a> {
|
||||
// The fonts of the type on the PDF and `.ai` pages placed linked.
|
||||
let mut placed_fonts = BTreeSet::new();
|
||||
let mut placed = BTreeMap::<String, PlacedPages>::new();
|
||||
let linked_files = |doc: &Document| match lookup {
|
||||
Some((d, base)) => super::links::linked_files(d, base),
|
||||
None => super::links::linked_files(doc, LinkBase::file(Some(source))),
|
||||
};
|
||||
doc.visit_placed(|_, p| {
|
||||
let page = (p.link.page.unwrap_or(1), p.bounding, p.page_box);
|
||||
let pages = placed.entry(p.link.path.clone()).or_default();
|
||||
@@ -218,7 +233,7 @@ impl<'a> Collector<'a> {
|
||||
}
|
||||
});
|
||||
if self.opts.copy_fonts && !self.opts.copy_links && !placed.is_empty() {
|
||||
for file in super::links::linked_files(doc, Some(source)) {
|
||||
for file in linked_files(doc) {
|
||||
if let (Some(pages), Some(bytes)) = (placed.get(&file.path), &file.bytes) {
|
||||
placed_fonts.extend(page_fonts(&file.name, bytes, file.found_path.as_deref().unwrap_or(&file.path), pages));
|
||||
}
|
||||
@@ -226,7 +241,7 @@ impl<'a> Collector<'a> {
|
||||
}
|
||||
if self.opts.copy_links {
|
||||
heading(&mut lines, "LINKED FILES");
|
||||
for file in super::links::linked_files(doc, Some(source)) {
|
||||
for file in linked_files(doc) {
|
||||
let found_path = file.found_path.as_deref().unwrap_or(&file.path);
|
||||
if let Some(ancestor) = self.visiting.get(found_path).cloned() {
|
||||
// The ancestor is already included (possibly the root document). A
|
||||
@@ -272,7 +287,7 @@ impl<'a> Collector<'a> {
|
||||
if depth >= vectorcraft_doc::placed_document::MAX_DEPTH {
|
||||
self.warn(&mut lines, format!("{}: nesting limit reached; copied unchanged without collecting its own links", file.path));
|
||||
} else {
|
||||
self.collect(&mut art, found_path, &destination_file, depth + 1, true)?;
|
||||
self.collect(&mut art, found_path, &destination_file, depth + 1, true, None)?;
|
||||
}
|
||||
} else if self.opts.copy_fonts
|
||||
&& let Some(pages) = pages
|
||||
@@ -289,7 +304,7 @@ impl<'a> Collector<'a> {
|
||||
let include_linked =
|
||||
saved.doc.linked_only_images().iter().any(|key| saved.doc.images.get(key).is_some_and(|blob| !blob.is_proxy()));
|
||||
let mut nested = saved.doc;
|
||||
self.collect(&mut nested, found_path, &destination_file, depth + 1, false)?;
|
||||
self.collect(&mut nested, found_path, &destination_file, depth + 1, false, None)?;
|
||||
if self.opts.relink {
|
||||
#[cfg(test)]
|
||||
PARTS_READ.with(|c| c.set(c.get() + 1));
|
||||
@@ -405,6 +420,8 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
pub struct Package {
|
||||
o: Options,
|
||||
doc_path: String,
|
||||
/// The file the document's links' stored relative paths are taken from.
|
||||
links_from: String,
|
||||
stem: String,
|
||||
name: String,
|
||||
root: Option<PathBuf>,
|
||||
@@ -450,7 +467,8 @@ fn prepare(s: &mut Session, p: &Value) -> Result<Package> {
|
||||
let root = folder.map(|f| Path::new(f).join(&name));
|
||||
let info = if o.report { Some(super::docinfo::report(s, false)?) } else { None };
|
||||
let doc = s.doc()?.doc.clone();
|
||||
Ok(Package { o, doc_path, stem, name, root, info, doc })
|
||||
let links_from = s.doc()?.links_from.clone().unwrap_or_else(|| doc_path.clone());
|
||||
Ok(Package { o, doc_path, links_from, stem, name, root, info, doc })
|
||||
}
|
||||
|
||||
impl Package {
|
||||
@@ -464,18 +482,27 @@ impl Package {
|
||||
|
||||
/// [`Package::write`].
|
||||
fn collect_and_write(package: Package, write: &mut dyn FnMut(&str, &[u8]) -> Result<()>) -> Result<Value> {
|
||||
let Package { o, doc_path, stem, name, root, info, doc } = package;
|
||||
let mut doc = (*doc).clone();
|
||||
let Package { o, doc_path, links_from, stem, name, root, info, doc: original } = package;
|
||||
let mut doc = (*original).clone();
|
||||
let doc_name = format!("{stem}.{}", vectorcraft_format::EXTENSION);
|
||||
let base = LinkBase { doc: Some(&doc_path), relative: Some(&links_from) };
|
||||
// After a save in another folder, links found away from their paths take the paths where they
|
||||
// were found, as a copy of the document has them.
|
||||
let found = (links_from != doc_path).then(|| {
|
||||
super::links::with_found_paths(&mut doc, base);
|
||||
doc.clone()
|
||||
});
|
||||
let looked = found.as_ref().unwrap_or(&original);
|
||||
// Prepare original links for the destination *before* rebasing copied links.
|
||||
// Recalculating afterward would overwrite the portable relative paths.
|
||||
if let Some(root) = &root
|
||||
&& let Some(d) = super::links::with_relative_paths(&doc, &root.join(&doc_name).to_string_lossy())
|
||||
&& let Some(d) = super::links::with_relative_paths(&doc, base, &root.join(&doc_name).to_string_lossy())
|
||||
{
|
||||
doc = d;
|
||||
}
|
||||
let mut collector = Collector::new(&o, root.clone(), &doc_name);
|
||||
collector.collect(&mut doc, &doc_path, &doc_name, 0, false)?;
|
||||
// The files are looked for where the links name them before the rebasing above.
|
||||
collector.collect(&mut doc, &doc_path, &doc_name, 0, false, Some((looked, base)))?;
|
||||
let mut entries = std::mem::take(&mut collector.entries);
|
||||
entries.insert(0, (doc_name.clone(), vectorcraft_format::save_file(&doc)));
|
||||
let mut taken = std::mem::take(&mut collector.taken);
|
||||
|
||||
@@ -254,7 +254,7 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>, link_pdf: bool) -> Result<Loa
|
||||
));
|
||||
}
|
||||
// Linked images show their files, found from its folder.
|
||||
super::links::resolve(&mut l.doc, src.path, false, false);
|
||||
super::links::resolve(&mut l.doc, super::links::LinkBase::file(src.path), false, false);
|
||||
(l.doc, l.warnings)
|
||||
};
|
||||
doc.drop_edit_modes();
|
||||
@@ -312,7 +312,7 @@ pub(crate) fn pdf_art(
|
||||
// file again: one already being read further up, or past the most nested reads, keeps its links
|
||||
// as stored, as a VectorCraft document's do.
|
||||
if alone && let Some(_reading) = document::Reading::enter(bytes) {
|
||||
super::links::resolve(&mut doc, path, false, false);
|
||||
super::links::resolve(&mut doc, super::links::LinkBase::file(path), false, false);
|
||||
}
|
||||
doc.drop_edit_modes();
|
||||
let layers = std::mem::take(&mut doc.layers);
|
||||
|
||||
@@ -133,7 +133,7 @@ pub(crate) fn document_art(
|
||||
// A document already being read further up, or past [`MAX_NESTED_READS`] of them, has its links
|
||||
// left as stored.
|
||||
if let Some(_reading) = Reading::enter(bytes) {
|
||||
super::super::links::resolve(&mut l.doc, path, false, false);
|
||||
super::super::links::resolve(&mut l.doc, super::super::links::LinkBase::file(path), false, false);
|
||||
}
|
||||
l.doc.drop_edit_modes();
|
||||
let n = l.doc.artboards.len();
|
||||
@@ -332,7 +332,7 @@ pub(crate) fn node(d: &mut Document, s: &Source, link: LinkInfo, bounding: bool)
|
||||
/// `d` for output: linked images and placed documents that keep only their previews get their
|
||||
/// files back, else output as their previews, with a warning.
|
||||
pub(crate) fn full_documents(d: &Document) -> (Cow<'_, Document>, Vec<String>) {
|
||||
let (d, mut warnings) = super::super::links::full_linked_images(d, None);
|
||||
let (d, mut warnings) = super::super::links::full_linked_images(d, Default::default());
|
||||
let doc = match &d {
|
||||
Cow::Borrowed(doc) => doc,
|
||||
Cow::Owned(doc) => doc,
|
||||
|
||||
@@ -575,6 +575,10 @@ struct Meta {
|
||||
/// (see [`crate::DocState::import_losses`]): a restored copy still refuses to replace it.
|
||||
imported_from: Option<String>,
|
||||
import_losses: Vec<String>,
|
||||
/// The file the links' relative paths name places from, for a copy without `path` (see
|
||||
/// [`crate::DocState::links_from`]).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
links_from: Option<String>,
|
||||
/// The document's images by key, in entries of the area (none in copies written before
|
||||
/// images were kept apart: the document entry holds them).
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
@@ -844,6 +848,7 @@ pub fn jobs(s: &mut Session, store: &Arc<dyn RecoveryStore>) -> Result<(Vec<Reco
|
||||
saved,
|
||||
imported_from: st.imported_from.clone(),
|
||||
import_losses: st.import_losses.clone(),
|
||||
links_from: st.links_from.clone().filter(|_| st.path.is_none()),
|
||||
// Filled in as the copy is written.
|
||||
images: BTreeMap::new(),
|
||||
};
|
||||
@@ -1061,11 +1066,14 @@ fn restore_one(s: &mut Session, store: &Arc<dyn RecoveryStore>, file: &str) -> R
|
||||
if doc.title.is_empty() {
|
||||
doc.title = if meta.title.is_empty() { stem.to_string() } else { meta.title.clone() };
|
||||
}
|
||||
// Linked files are looked for from the document's own folder.
|
||||
let links = super::links::resolve(&mut doc, meta.path.as_deref(), s.prefs.update_links == "automatically", s.prefs.low_res_proxy_eps);
|
||||
// Linked files are looked for from the document's own folder, else from the file it was read from.
|
||||
let links_from = meta.path.clone().or_else(|| meta.links_from.clone());
|
||||
let base = super::links::LinkBase::file(links_from.as_deref());
|
||||
let links = super::links::resolve(&mut doc, base, s.prefs.update_links == "automatically", s.prefs.low_res_proxy_eps);
|
||||
let index = s.add_document(doc, meta.path.clone());
|
||||
let st = s.doc_mut()?;
|
||||
st.recovered = true;
|
||||
st.links_from = links_from;
|
||||
st.imported_from = meta.imported_from.clone();
|
||||
st.import_losses = meta.import_losses.clone();
|
||||
st.format = SAVE_FORMATS.iter().copied().find(|f| *f == meta.format).unwrap_or("vectorcraft");
|
||||
|
||||
@@ -95,6 +95,11 @@ pub struct DocState {
|
||||
/// so an export or save to it asks first ([`cmd::fileio::check_not_lossy_overwrite`]). Not saved.
|
||||
pub imported_from: Option<String>,
|
||||
pub import_losses: Vec<String>,
|
||||
/// The file its links' stored relative paths name places from: the file the document was read
|
||||
/// from, also when that file is not its own (such as a `.ai` file another app wrote). A save
|
||||
/// writes the saved file's relative paths and leaves the open document's links as they are.
|
||||
/// `None`: the document's own file. Not saved ([`crate::cmd::links::LinkBase`]).
|
||||
pub links_from: Option<String>,
|
||||
/// Transform Again after a perspective move or scale (Perspective Selection tool): the
|
||||
/// `perspective.transform` params it repeats. `None` once an ordinary transform follows.
|
||||
pub last_perspective: Option<Value>,
|
||||
@@ -296,6 +301,7 @@ impl DocState {
|
||||
layers_open,
|
||||
imported_from: None,
|
||||
import_losses: vec![],
|
||||
links_from: None,
|
||||
last_perspective: None,
|
||||
opened_by: file_access::current(),
|
||||
empty_artboards: Default::default(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! History panel › New Document from State (`edit.newDocumentFromState`).
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
@@ -262,7 +263,72 @@ fn a_file_the_source_finds_in_its_folder_is_found_from_the_new_document() {
|
||||
s.execute(CMD, &json!({})).unwrap();
|
||||
assert_eq!(s.active_index(), Some(1), "the new document");
|
||||
let c = check(&mut s);
|
||||
assert_eq!((&c["links"][0]["status"], &c["links"][0]["path"]), (&json!("ok"), &json!(moved)), "{c}");
|
||||
assert_eq!((&c["links"][0]["status"], c["links"][0]["path"].as_str().map(Path::new)), (&json!("ok"), Some(Path::new(&moved))), "{c}");
|
||||
}
|
||||
|
||||
/// A linked file missing from the moved source keeps the place its relative path names from the
|
||||
/// source's folder: the new document, saved beside the source, writes the same relative path.
|
||||
#[test]
|
||||
fn a_missing_file_keeps_its_relative_path_in_the_new_document() {
|
||||
let a = Folder::new("history-document-missing-a");
|
||||
let b = Folder::new("history-document-missing-b");
|
||||
let away = Folder::new("history-document-missing-away");
|
||||
write(&a.file("art/photo.png"), &png(60, 30, RED));
|
||||
let mut s = session();
|
||||
place(&mut s, &a.file("art/photo.png"));
|
||||
s.execute("document.save", &json!({"path": a.file("poster.vectorcraft")})).unwrap();
|
||||
std::fs::remove_dir_all(&b.0).unwrap();
|
||||
std::fs::rename(&a.0, &b.0).unwrap();
|
||||
std::fs::rename(b.0.join("art"), away.0.join("art")).unwrap();
|
||||
let r = s.execute("document.open", &json!({"path": b.file("poster.vectorcraft")})).unwrap();
|
||||
assert_eq!(r["missingLinks"][0]["name"], "photo.png", "{r}");
|
||||
s.execute(CMD, &json!({})).unwrap();
|
||||
let copy = b.file("copy.vectorcraft");
|
||||
s.execute("document.save", &json!({"path": copy})).unwrap();
|
||||
let saved: Value = serde_json::from_slice(&std::fs::read(©).unwrap()).unwrap();
|
||||
let link = &saved["document"]["layers"][0]["kind"]["children"][0]["kind"]["link"];
|
||||
assert_eq!(link["relative"].as_str(), Some("art/photo.png"), "{link}");
|
||||
}
|
||||
|
||||
/// A linked file that comes back beside the document the source was read from, after the source
|
||||
/// was saved in another folder, is found from the new document.
|
||||
#[test]
|
||||
fn a_file_back_beside_the_source_it_was_read_from_is_found_from_the_new_document() {
|
||||
let a = Folder::new("history-document-back-a");
|
||||
let b = Folder::new("history-document-back-b");
|
||||
let away = Folder::new("history-document-back-away");
|
||||
write(&a.file("art/photo.png"), &png(60, 30, RED));
|
||||
let mut s = session();
|
||||
place(&mut s, &a.file("art/photo.png"));
|
||||
s.execute("document.save", &json!({"path": a.file("poster.vectorcraft")})).unwrap();
|
||||
std::fs::remove_dir_all(&b.0).unwrap();
|
||||
std::fs::rename(&a.0, &b.0).unwrap();
|
||||
std::fs::rename(b.0.join("art"), away.0.join("art")).unwrap();
|
||||
s.execute("document.open", &json!({"path": b.file("poster.vectorcraft")})).unwrap();
|
||||
std::fs::create_dir_all(b.0.join("saved")).unwrap();
|
||||
s.execute("document.save", &json!({"path": b.file("saved/poster.vectorcraft")})).unwrap();
|
||||
std::fs::rename(away.0.join("art"), b.0.join("art")).unwrap();
|
||||
let source = s.active_index().unwrap();
|
||||
s.execute(CMD, &json!({})).unwrap();
|
||||
let c = s.execute("links.check", &json!({})).unwrap();
|
||||
let moved = b.file("art/photo.png");
|
||||
assert_eq!((&c["links"][0]["status"], c["links"][0]["path"].as_str().map(Path::new)), (&json!("ok"), Some(Path::new(&moved))), "{c}");
|
||||
// A file beside the folder the source was last saved in is found too.
|
||||
std::fs::create_dir_all(b.0.join("saved/art")).unwrap();
|
||||
std::fs::rename(&moved, b.file("saved/art/photo.png")).unwrap();
|
||||
s.execute("document.activate", &json!({"index": source})).unwrap();
|
||||
s.execute(CMD, &json!({})).unwrap();
|
||||
let c = s.execute("links.check", &json!({})).unwrap();
|
||||
let saved = b.file("saved/art/photo.png");
|
||||
assert_eq!((&c["links"][0]["status"], c["links"][0]["path"].as_str().map(Path::new)), (&json!("ok"), Some(Path::new(&saved))), "{c}");
|
||||
// Missing again when the new document is saved, the file keeps the place it was found.
|
||||
std::fs::rename(&saved, away.file("photo.png")).unwrap();
|
||||
std::fs::create_dir_all(b.0.join("new")).unwrap();
|
||||
let copy = b.file("new/copy.vectorcraft");
|
||||
s.execute("document.save", &json!({"path": copy})).unwrap();
|
||||
let file: Value = serde_json::from_slice(&std::fs::read(©).unwrap()).unwrap();
|
||||
let link = &file["document"]["layers"][0]["kind"]["children"][0]["kind"]["link"];
|
||||
assert_eq!(link["relative"].as_str(), Some("../saved/art/photo.png"), "{link}");
|
||||
}
|
||||
|
||||
/// The same holds for a linked image only a symbol shows.
|
||||
|
||||
@@ -85,6 +85,21 @@ fn linked_files_on_other_computers_are_never_read() {
|
||||
assert!(s.link_stamps.keys().all(|(_, p)| !far.contains(p)), "{:?}", s.link_stamps.keys());
|
||||
}
|
||||
|
||||
/// A place a link names from the file the document was read from is followed only when the
|
||||
/// document's own file follows it too: after a document read from a network share is saved on
|
||||
/// this computer, its links are not looked for on that share.
|
||||
#[test]
|
||||
fn places_named_from_a_share_are_not_followed_after_a_save_elsewhere() {
|
||||
use crate::cmd::links::follow_from;
|
||||
let resolve =
|
||||
|stored: &str, doc: Option<&str>| craft_docpath::resolve(stored, doc.map(std::path::Path::new)).map(|p| p.to_string_lossy().into_owned());
|
||||
let (link, on_share) = (r"\\server\share\a\art\photo.png", Some(r"\\server\share\a\poster.vectorcraft"));
|
||||
assert_eq!(follow_from(link, on_share, on_share, resolve).as_deref(), Some(link));
|
||||
assert_eq!(follow_from(link, on_share, Some(r"\\server\share\b\poster.vectorcraft"), resolve).as_deref(), Some(link));
|
||||
assert_eq!(follow_from(link, on_share, Some("/local/b/poster.vectorcraft"), resolve), None);
|
||||
assert_eq!(follow_from(link, on_share, Some(r"C:\b\poster.vectorcraft"), resolve), None);
|
||||
}
|
||||
|
||||
/// The final path Windows gives a folder of the drive `Z:` mapped to `\\server\share`. The
|
||||
/// share's root folder has no trailing separator (`\\?\UNC\server\share`).
|
||||
fn on_share(folder: &str) -> Option<std::path::PathBuf> {
|
||||
|
||||
@@ -244,6 +244,141 @@ fn the_relative_path_finds_the_file_after_the_folder_moves() {
|
||||
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
|
||||
}
|
||||
|
||||
/// A document moved to another folder while its linked file is missing keeps the file's relative
|
||||
/// path when it is saved again, there or in a subfolder, so the file is found once it is back
|
||||
/// beside the document.
|
||||
#[test]
|
||||
fn a_missing_link_keeps_its_relative_path_when_the_moved_document_is_saved() {
|
||||
let a = Folder::new("missing-save-a");
|
||||
let b = Folder::new("missing-save-b");
|
||||
let away = Folder::new("missing-save-away");
|
||||
let pic = a.file("art/photo.png");
|
||||
write(&pic, &png(600, 300, RED));
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &pic);
|
||||
save(&mut s, &a.file("poster.vectorcraft"));
|
||||
// Move the document and its links, then take the links away.
|
||||
std::fs::remove_dir_all(&b.0).unwrap();
|
||||
std::fs::rename(&a.0, &b.0).unwrap();
|
||||
std::fs::rename(b.0.join("art"), away.0.join("art")).unwrap();
|
||||
let r = open(&mut s, &b.file("poster.vectorcraft"));
|
||||
assert_eq!(r["missingLinks"][0]["name"].as_str(), Some("photo.png"), "{r}");
|
||||
save(&mut s, &b.file("poster.vectorcraft"));
|
||||
std::fs::create_dir_all(b.0.join("copies")).unwrap();
|
||||
save(&mut s, &b.file("copies/poster.vectorcraft"));
|
||||
let relative = |path: &str| {
|
||||
let mut saved: Value = serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
|
||||
saved_image(&mut saved)["link"]["relative"].as_str().map(str::to_string)
|
||||
};
|
||||
assert_eq!(relative(&b.file("poster.vectorcraft")).as_deref(), Some("art/photo.png"));
|
||||
assert_eq!(relative(&b.file("copies/poster.vectorcraft")).as_deref(), Some("../art/photo.png"));
|
||||
// Saved again where the last save put it, the file is still named from the folder it was read from.
|
||||
s.execute("document.save", &json!({})).unwrap();
|
||||
assert_eq!(relative(&b.file("copies/poster.vectorcraft")).as_deref(), Some("../art/photo.png"));
|
||||
// The links come back: both saved documents find the file.
|
||||
std::fs::rename(away.0.join("art"), b.0.join("art")).unwrap();
|
||||
let moved = b.file("art/photo.png");
|
||||
for doc in [b.file("poster.vectorcraft"), b.file("copies/poster.vectorcraft")] {
|
||||
let r = open(&mut s, &doc);
|
||||
assert_eq!(&r["missingLinks"], &json!([]), "{doc}: {r}");
|
||||
assert_eq!(Path::new(&image(&s, id).link.unwrap().path), Path::new(&moved), "{doc}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A file found by name in the document's folder, then missing before the next save, keeps the
|
||||
/// place it was last found: the saved relative path names that place, not the folder its old
|
||||
/// relative path named.
|
||||
#[test]
|
||||
fn a_file_found_elsewhere_then_missing_keeps_the_place_it_was_found() {
|
||||
let dir = Folder::new("found-then-missing");
|
||||
let pic = dir.file("art/photo.png");
|
||||
write(&pic, &png(600, 300, RED));
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &pic);
|
||||
let doc = dir.file("poster.vectorcraft");
|
||||
save(&mut s, &doc);
|
||||
let beside = dir.file("photo.png");
|
||||
std::fs::rename(&pic, &beside).unwrap();
|
||||
let r = open(&mut s, &doc);
|
||||
assert_eq!(&r["missingLinks"], &json!([]), "{r}");
|
||||
assert_eq!(Path::new(&image(&s, id).link.unwrap().path), Path::new(&beside));
|
||||
let away = dir.file("away.png");
|
||||
std::fs::rename(&beside, &away).unwrap();
|
||||
save(&mut s, &doc);
|
||||
let mut saved: Value = serde_json::from_slice(&std::fs::read(&doc).unwrap()).unwrap();
|
||||
assert_eq!(saved_image(&mut saved)["link"]["relative"].as_str(), Some("photo.png"));
|
||||
}
|
||||
|
||||
/// After a save in another folder, a file Update Links finds by name beside the saved document,
|
||||
/// then missing before the next save, keeps the place it was found.
|
||||
#[test]
|
||||
fn a_file_update_links_finds_after_a_save_elsewhere_keeps_its_place() {
|
||||
let a = Folder::new("update-after-save-as-a");
|
||||
let b = Folder::new("update-after-save-as-b");
|
||||
let pic = a.file("art/photo.png");
|
||||
write(&pic, &png(600, 300, RED));
|
||||
let mut s = session();
|
||||
let id = place(&mut s, &pic);
|
||||
save(&mut s, &a.file("poster.vectorcraft"));
|
||||
open(&mut s, &a.file("poster.vectorcraft"));
|
||||
let doc = b.file("poster.vectorcraft");
|
||||
save(&mut s, &doc);
|
||||
let beside = b.file("photo.png");
|
||||
std::fs::rename(&pic, &beside).unwrap();
|
||||
let r = s.execute("links.update", &json!({"ids": [id.0]})).unwrap();
|
||||
assert_eq!(&r["missing"], &json!([]), "{r}");
|
||||
assert_eq!(Path::new(&image(&s, id).link.unwrap().path), Path::new(&beside));
|
||||
std::fs::remove_file(&beside).unwrap();
|
||||
s.execute("document.save", &json!({})).unwrap();
|
||||
let mut saved: Value = serde_json::from_slice(&std::fs::read(&doc).unwrap()).unwrap();
|
||||
assert_eq!(saved_image(&mut saved)["link"]["relative"].as_str(), Some("photo.png"));
|
||||
}
|
||||
|
||||
/// A file found at its relative path from the folder of the last save, away from its path and from
|
||||
/// the folder the document was read from, is written relative to where it was found.
|
||||
#[test]
|
||||
fn a_file_found_beside_the_last_save_is_written_where_it_was_found() {
|
||||
let a = Folder::new("found-beside-save-a");
|
||||
let b = Folder::new("found-beside-save-b");
|
||||
let c = Folder::new("found-beside-save-c");
|
||||
write(&a.file("art/photo.png"), &png(600, 300, RED));
|
||||
let mut s = session();
|
||||
place(&mut s, &a.file("art/photo.png"));
|
||||
save(&mut s, &a.file("poster.vectorcraft"));
|
||||
open(&mut s, &a.file("poster.vectorcraft"));
|
||||
save(&mut s, &b.file("poster.vectorcraft"));
|
||||
std::fs::rename(a.0.join("art"), b.0.join("art")).unwrap();
|
||||
save(&mut s, &c.file("poster.vectorcraft"));
|
||||
let r = open(&mut s, &c.file("poster.vectorcraft"));
|
||||
assert_eq!(&r["missingLinks"], &json!([]), "{r}");
|
||||
}
|
||||
|
||||
/// After a moved document with a missing link is saved in another folder, `links.check` finds the
|
||||
/// file once it is back at the place its relative path names from the folder the document was
|
||||
/// read from, which is where the saved file looks for it too.
|
||||
#[test]
|
||||
fn a_missing_link_comes_back_where_the_document_saved_elsewhere_looks_for_it() {
|
||||
let a = Folder::new("check-after-save-as-a");
|
||||
let b = Folder::new("check-after-save-as-b");
|
||||
let c = Folder::new("check-after-save-as-c");
|
||||
let away = Folder::new("check-after-save-as-away");
|
||||
write(&a.file("art/photo.png"), &png(600, 300, RED));
|
||||
let mut s = session();
|
||||
place(&mut s, &a.file("art/photo.png"));
|
||||
save(&mut s, &a.file("poster.vectorcraft"));
|
||||
std::fs::remove_dir_all(&b.0).unwrap();
|
||||
std::fs::rename(&a.0, &b.0).unwrap();
|
||||
std::fs::rename(b.0.join("art"), away.0.join("art")).unwrap();
|
||||
open(&mut s, &b.file("poster.vectorcraft"));
|
||||
save(&mut s, &c.file("poster.vectorcraft"));
|
||||
std::fs::rename(away.0.join("art"), b.0.join("art")).unwrap();
|
||||
let r = s.execute("links.check", &json!({})).unwrap();
|
||||
let found = r["links"][0]["found"].as_str().map(Path::new);
|
||||
assert_eq!((r["missing"].as_u64(), found), (Some(0), Some(Path::new(&b.file("art/photo.png")))), "{r}");
|
||||
let r = open(&mut s, &c.file("poster.vectorcraft"));
|
||||
assert_eq!(&r["missingLinks"], &json!([]), "{r}");
|
||||
}
|
||||
|
||||
/// Links in symbols, graph designs, patterns and opacity masks are saved with their relative
|
||||
/// paths, as links in the layers are: after the folder moves, each file is found and its link takes
|
||||
/// the new path, which the next save writes relative to the document again.
|
||||
@@ -781,7 +916,7 @@ fn low_res_proxy_eps_export_uses_full_resolution_from_the_linked_file() {
|
||||
assert_eq!((r["modifiedLinks"].clone(), r["updatedLinks"].clone()), (json!([]), json!([])), "{r}");
|
||||
let key = image(&s, id).key;
|
||||
assert!(s.doc().unwrap().doc.images[&key].is_proxy());
|
||||
let (full, warnings) = cmd::links::full_linked_images(&s.doc().unwrap().doc, None);
|
||||
let (full, warnings) = cmd::links::full_linked_images(&s.doc().unwrap().doc, Default::default());
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert!(!full.images[&key].is_proxy());
|
||||
let dim =
|
||||
@@ -812,7 +947,7 @@ fn export_reads_a_found_svg_link_into_its_own_images_only() {
|
||||
|
||||
let bytes = png(20, 20, RED);
|
||||
write(&dir.file("a.png"), &bytes);
|
||||
let (full, warnings) = cmd::links::full_linked_images(&s.doc().unwrap().doc, None);
|
||||
let (full, warnings) = cmd::links::full_linked_images(&s.doc().unwrap().doc, Default::default());
|
||||
assert_eq!(warnings.len(), 1, "b.png is still missing: {warnings:?}");
|
||||
assert_eq!(*full.images[&a.key].bytes, bytes, "a.png's pixels");
|
||||
assert_eq!(full.images[&b.key].bytes, placeholder, "b.png keeps its placeholder");
|
||||
@@ -845,7 +980,7 @@ fn export_reads_a_changed_svg_link_into_its_own_images_only() {
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
assert!(doc.images[&a.key].is_proxy() && doc.images[&b.key].is_proxy(), "previews");
|
||||
let preview = doc.images[&a.key].bytes.clone();
|
||||
let (full, warnings) = cmd::links::full_linked_images(doc, None);
|
||||
let (full, warnings) = cmd::links::full_linked_images(doc, Default::default());
|
||||
assert_eq!(warnings.len(), 1, "a.png is missing: {warnings:?}");
|
||||
assert_eq!(*full.images[&b.key].bytes, blue, "b.png's new pixels");
|
||||
assert_eq!(full.images[&a.key].bytes, preview, "a.png keeps its preview");
|
||||
@@ -1002,7 +1137,7 @@ fn relative_paths_are_written_against_the_saved_file() {
|
||||
};
|
||||
d.insert(Some(lid), 0, vectorcraft_doc::Node::new(nid, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let rel = |dest: String| {
|
||||
let d = cmd::links::with_relative_paths(&d, &dest).unwrap_or_else(|| d.clone());
|
||||
let d = cmd::links::with_relative_paths(&d, Default::default(), &dest).unwrap_or_else(|| d.clone());
|
||||
match &d.node(nid).unwrap().kind {
|
||||
NodeKind::Image(im) => im.link.as_ref().unwrap().relative.clone(),
|
||||
_ => None,
|
||||
@@ -1083,15 +1218,11 @@ fn links_of_an_imported_file_are_read_where_they_are_found() {
|
||||
assert!((im.width, im.height) == (600, 300) && !s.doc().unwrap().doc.images[&im.key].is_proxy(), "{im:?}");
|
||||
}
|
||||
|
||||
/// A placed file of a `.ai` file whose folder moved with the document is found by the folder the
|
||||
/// file keeps relative to the document's (`src/`), and its link takes the size, modification time
|
||||
/// and hash of the file found there. Not found, the link keeps no such details.
|
||||
#[test]
|
||||
fn links_of_an_imported_file_are_found_by_their_relative_folder() {
|
||||
/// `poster.ai` in `dir`, a `.ai` file another app wrote, placing `src/photo.png` (relative to it)
|
||||
/// under a path that no longer exists → its path.
|
||||
fn imported_with_placed_file(dir: &Folder) -> String {
|
||||
use vectorcraft_testkit::ai;
|
||||
let dir = Folder::new("imported-relative");
|
||||
std::fs::create_dir_all(dir.0.join("src")).unwrap();
|
||||
let pic = dir.file("src/photo.png").replace('\\', "/");
|
||||
let place = "%AI5_BeginPlace\n%%FileType: 1\n[0.1 0 0 -0.1 20 80] 0 0 600 300 4 4 0\n(/no/such/folder/src/photo.png)`\n\
|
||||
%%IncludeFile:/no/such/folder/src/photo.png\n%AI17_Begin_Content_if_version_gt:24 12\n%AI28_BeginPlacedRelativePath\n(src/)\n\
|
||||
%AI28_EndPlacedRelativePath\n%AI17_Alternate_Content\n%AI17_End_Versioned_Content\n%AI26_BeginPlacedObjectPreview\n\
|
||||
@@ -1099,6 +1230,17 @@ fn links_of_an_imported_file_are_found_by_their_relative_folder() {
|
||||
let doc_path = dir.file("poster.ai");
|
||||
let data = ai::editing_data(200.0, 100.0, &ai::layer("Art", place));
|
||||
write(&doc_path, &ai::ai(data.as_bytes(), "0.56 0.56 0.87 rg 20 50 60 30 re f"));
|
||||
doc_path
|
||||
}
|
||||
|
||||
/// A placed file of a `.ai` file whose folder moved with the document is found by the folder the
|
||||
/// file keeps relative to the document's (`src/`), and its link takes the size, modification time
|
||||
/// and hash of the file found there. Not found, the link keeps no such details.
|
||||
#[test]
|
||||
fn links_of_an_imported_file_are_found_by_their_relative_folder() {
|
||||
let dir = Folder::new("imported-relative");
|
||||
let pic = dir.file("src/photo.png").replace('\\', "/");
|
||||
let doc_path = imported_with_placed_file(&dir);
|
||||
let link = |s: &Session| {
|
||||
let mut found = vec![];
|
||||
s.doc().unwrap().doc.visit_images(|_, im| found.push(im.clone()));
|
||||
@@ -1112,9 +1254,16 @@ fn links_of_an_imported_file_are_found_by_their_relative_folder() {
|
||||
let (l, proxy) = link(&s);
|
||||
assert_eq!((l.path.as_str(), l.relative.as_deref()), ("/no/such/folder/src/photo.png", Some("src/photo.png")));
|
||||
assert!(proxy && l.size.is_none() && l.modified.is_none() && l.hash.is_none(), "{l:?}");
|
||||
// Saved beside it while the file is missing, the document keeps the folder the file is in.
|
||||
let native = dir.file("poster.vectorcraft");
|
||||
save(&mut s, &native);
|
||||
let mut saved: Value = serde_json::from_slice(&std::fs::read(&native).unwrap()).unwrap();
|
||||
assert_eq!(saved_image(&mut saved)["link"]["relative"].as_str(), Some("src/photo.png"));
|
||||
|
||||
let bytes = png(600, 300, RED);
|
||||
write(&pic, &bytes);
|
||||
let r = open(&mut s, &native);
|
||||
assert_eq!(&r["missingLinks"], &json!([]), "the native copy finds the file: {r}");
|
||||
let r = open(&mut s, &doc_path);
|
||||
assert_eq!(&r["missingLinks"], &json!([]), "{r}");
|
||||
let (l, proxy) = link(&s);
|
||||
@@ -1123,3 +1272,27 @@ fn links_of_an_imported_file_are_found_by_their_relative_folder() {
|
||||
assert_eq!((l.size, l.hash.clone()), (Some(bytes.len() as u64), Some(vectorcraft_doc::links::hash_bytes(&bytes))));
|
||||
assert!(l.modified.is_some(), "{l:?}");
|
||||
}
|
||||
|
||||
/// A Data Recovery copy of such a document keeps the file its links' relative paths name places
|
||||
/// from: restored and saved beside the `.ai` file, it keeps the folder the placed file is in.
|
||||
#[test]
|
||||
fn a_recovered_copy_of_an_imported_file_keeps_its_links_folder() {
|
||||
let dir = Folder::new("imported-recovered");
|
||||
let doc_path = imported_with_placed_file(&dir);
|
||||
let store = std::sync::Arc::new(crate::cmd::recovery::MemoryStore::default());
|
||||
let mut s = session();
|
||||
s.recovery.set_store(store.clone());
|
||||
open(&mut s, &doc_path);
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 20})).unwrap();
|
||||
let r = s.execute("file.recovery.save", &json!({})).unwrap();
|
||||
assert_eq!(r["saved"].as_array().map(Vec::len), Some(1), "{r}");
|
||||
drop(s);
|
||||
let mut s = Session::new();
|
||||
s.recovery.set_store(store);
|
||||
let r = s.execute("file.recovery.restore", &json!({})).unwrap();
|
||||
assert_eq!(r["restored"].as_array().map(Vec::len), Some(1), "{r}");
|
||||
let native = dir.file("poster.vectorcraft");
|
||||
save(&mut s, &native);
|
||||
let mut saved: Value = serde_json::from_slice(&std::fs::read(&native).unwrap()).unwrap();
|
||||
assert_eq!(saved_image(&mut saved)["link"]["relative"].as_str(), Some("src/photo.png"));
|
||||
}
|
||||
|
||||
@@ -253,6 +253,103 @@ fn a_missing_link_is_reported_not_copied() {
|
||||
assert!(report.contains("blue.png: not found"), "{report}");
|
||||
}
|
||||
|
||||
/// A file missing from a document that moved keeps the place its relative path names from the
|
||||
/// document's folder: the packaged document writes its relative path from there.
|
||||
#[test]
|
||||
fn a_missing_link_of_a_moved_document_keeps_its_relative_place() {
|
||||
let a = Folder::new("package-moved-a");
|
||||
let b = Folder::new("package-moved-b");
|
||||
let mut s = poster(&a);
|
||||
std::fs::remove_dir_all(&b.0).unwrap();
|
||||
std::fs::rename(&a.0, &b.0).unwrap();
|
||||
std::fs::remove_file(b.file("art/blue.png")).unwrap();
|
||||
open(&mut s, &b.file("poster.vectorcraft"));
|
||||
// Saved in another folder first: the relative path still names the folder the file was read from.
|
||||
std::fs::create_dir_all(b.0.join("saved")).unwrap();
|
||||
save(&mut s, &b.file("saved/poster.vectorcraft"));
|
||||
let out = b.file("out");
|
||||
let r = run(&mut s, "file.package", json!({"folder": out}));
|
||||
assert_eq!(r["missingLinks"], json!(["blue.png"]), "{r}");
|
||||
let packaged = std::path::Path::new(&out).join("poster Folder/poster.vectorcraft");
|
||||
let doc = vectorcraft_format::load(&std::fs::read(packaged).unwrap()).unwrap();
|
||||
let mut rel = vec![];
|
||||
doc.visit_images(|_, im| rel.push(im.link.clone().unwrap().relative));
|
||||
rel.sort();
|
||||
assert_eq!(rel, [Some("../../art/blue.png".to_string()), Some("Links/red.png".to_string())]);
|
||||
}
|
||||
|
||||
/// A missing link stays missing when another file sits where its relative path, written for the
|
||||
/// package folder, would name from the folder the document was last saved in.
|
||||
#[test]
|
||||
fn a_missing_link_is_not_collected_from_another_place() {
|
||||
let a = Folder::new("package-other-a");
|
||||
let b = Folder::new("package-other-b");
|
||||
let mut s = poster(&a);
|
||||
std::fs::remove_dir_all(&b.0).unwrap();
|
||||
std::fs::rename(&a.0, &b.0).unwrap();
|
||||
std::fs::remove_file(b.file("art/blue.png")).unwrap();
|
||||
open(&mut s, &b.file("poster.vectorcraft"));
|
||||
std::fs::create_dir_all(b.0.join("s1/s2/s3")).unwrap();
|
||||
save(&mut s, &b.file("s1/s2/s3/poster.vectorcraft"));
|
||||
// `../../art/blue.png` from the package folder, read from `s1/s2/s3`.
|
||||
write(&b.file("s1/art/blue.png"), &png(10, 10, RED));
|
||||
let r = run(&mut s, "file.package", json!({"folder": b.file("out")}));
|
||||
assert_eq!((r["links"].as_u64(), r["missingLinks"].clone()), (Some(1), json!(["blue.png"])), "{r}");
|
||||
}
|
||||
|
||||
/// Without copying, a packaged document links a file found by name beside the document it was read
|
||||
/// from, after a save in another folder, where it was found.
|
||||
#[test]
|
||||
fn a_link_found_by_name_is_kept_where_it_was_found_without_copying() {
|
||||
let a = Folder::new("package-name-a");
|
||||
let b = Folder::new("package-name-b");
|
||||
let mut s = poster(&a);
|
||||
std::fs::remove_dir_all(&b.0).unwrap();
|
||||
std::fs::rename(&a.0, &b.0).unwrap();
|
||||
let away = b.file("away.png");
|
||||
std::fs::rename(b.file("art/blue.png"), &away).unwrap();
|
||||
open(&mut s, &b.file("poster.vectorcraft"));
|
||||
std::fs::create_dir_all(b.0.join("saved")).unwrap();
|
||||
save(&mut s, &b.file("saved/poster.vectorcraft"));
|
||||
std::fs::rename(&away, b.file("blue.png")).unwrap();
|
||||
let out = b.file("out");
|
||||
run(&mut s, "file.package", json!({"folder": out, "copyLinks": false}));
|
||||
let packaged = std::path::Path::new(&out).join("poster Folder/poster.vectorcraft");
|
||||
let r = open(&mut s, &packaged.to_string_lossy());
|
||||
assert_eq!(&r["missingLinks"], &json!([]), "{r}");
|
||||
}
|
||||
|
||||
/// A file that comes back beside the document it was read from, after the document was saved in
|
||||
/// another folder, is collected from there.
|
||||
#[test]
|
||||
fn a_link_that_comes_back_after_a_save_elsewhere_is_collected() {
|
||||
let a = Folder::new("package-back-a");
|
||||
let b = Folder::new("package-back-b");
|
||||
let away = Folder::new("package-back-away");
|
||||
let mut s = poster(&a);
|
||||
std::fs::remove_dir_all(&b.0).unwrap();
|
||||
std::fs::rename(&a.0, &b.0).unwrap();
|
||||
std::fs::rename(b.file("art/blue.png"), away.file("blue.png")).unwrap();
|
||||
open(&mut s, &b.file("poster.vectorcraft"));
|
||||
std::fs::create_dir_all(b.0.join("saved")).unwrap();
|
||||
save(&mut s, &b.file("saved/poster.vectorcraft"));
|
||||
std::fs::rename(away.file("blue.png"), b.file("art/blue.png")).unwrap();
|
||||
let out = b.file("out");
|
||||
let r = run(&mut s, "file.package", json!({"folder": out}));
|
||||
assert_eq!((r["links"].as_u64(), r["missingLinks"].clone()), (Some(2), json!([])), "{r}");
|
||||
let packaged = std::path::Path::new(&out).join("poster Folder/poster.vectorcraft");
|
||||
let doc = vectorcraft_format::load(&std::fs::read(packaged).unwrap()).unwrap();
|
||||
let mut rel = vec![];
|
||||
doc.visit_images(|_, im| rel.push(im.link.clone().unwrap().relative));
|
||||
rel.sort();
|
||||
assert_eq!(rel, [Some("Links/blue.png".to_string()), Some("Links/red.png".to_string())]);
|
||||
// A file beside the folder the document was last saved in is collected too.
|
||||
std::fs::create_dir_all(b.0.join("saved/art")).unwrap();
|
||||
std::fs::rename(b.file("art/blue.png"), b.file("saved/art/blue.png")).unwrap();
|
||||
let r = run(&mut s, "file.package", json!({"folder": b.file("out2")}));
|
||||
assert_eq!((r["links"].as_u64(), r["missingLinks"].clone()), (Some(2), json!([])), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_font_named_by_another_of_its_names_is_copied() {
|
||||
let dir = Folder::new("package-alias");
|
||||
|
||||
@@ -489,7 +489,7 @@ fn a_placed_document_without_its_bytes_or_a_hash_is_reported_not_read() {
|
||||
let Some(NodeKind::PlacedDocument(p)) = d.node_mut(id).map(|n| &mut n.kind) else { panic!("a placed document") };
|
||||
p.link.hash = None;
|
||||
p.link.size = None;
|
||||
let r = crate::cmd::links::resolve(&mut d, None, false, false).to_json();
|
||||
let r = crate::cmd::links::resolve(&mut d, crate::cmd::links::LinkBase::file(None), false, false).to_json();
|
||||
assert_eq!(r["modifiedLinks"].as_array().map(Vec::len), Some(1), "{r}");
|
||||
assert!(!d.images.contains_key(&key), "not read");
|
||||
}
|
||||
|
||||
@@ -4595,6 +4595,14 @@ its file was found. Only files on this computer, or on the network share the doc
|
||||
on, are looked at: a path to another computer, a device or a URL counts as missing. On Windows, a document opened from
|
||||
a mapped drive (`Z:\…`) is on the share the drive is mapped to, and links on that share (`\\server\share\…`) are
|
||||
looked at. A link on a drive mapped to another share (`Y:\…`) counts as missing, from a document on this computer too.
|
||||
The links of an open document keep their relative paths from the file it was opened from (for a `.ai` file another
|
||||
app wrote, that file), also after a save to another folder. When a link's file is not at its path, a save and Package
|
||||
write the path relative to the new file of the place the file is found, or for a missing file of the place that
|
||||
relative path names, and a document made with New Document from State keeps the same places, so a document moved while
|
||||
its linked files were missing finds them once they are back in that folder. In the open document, a link
|
||||
is looked for at its path, then at its relative path from the folder of the file the document was opened from and from
|
||||
the document's own folder, then by name in those two folders. A link found away from its path takes the relative path
|
||||
of the place it was found.
|
||||
The result lists `missingLinks`
|
||||
(their images show the preview), `modifiedLinks` (left as they were; read again only with the preference
|
||||
`updateLinks: "automatically"`, then they are in `updatedLinks`), each as `{name, path, ids}`, with `refused` (why the
|
||||
|
||||
@@ -1391,6 +1391,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Missing linked files can be looked for in a folder and its subfolders: Find in Folder… in the Linked File Not Found dialog and in the Links panel menu lists the files with each one's name, those with the size and contents saved with the link first, and relinks the files chosen, copying an image's file into the document's folder when asked, without replacing a file there (`links.findFiles`, `links.relink {copy}`). In the Links panel it looks for the selected linked files when any are selected.
|
||||
- The Links panel's Relink… opens a Relink dialog whose Replace menu picks which links take the new file: Selected Instances (the selected images, as before), All Instances (every image linked to the same file), Missing Links from This Folder (then every other missing link whose file name is in the new file's folder) and Each Link Separately (a file for each selected link in turn). Each relinks as one undo step (`links.relink {scope}`, `ui.relinkDialog`). Relink to Folder… stays as it is.
|
||||
- Linked files inside symbols, graph designs, patterns and opacity masks are saved with their paths relative to the document, as linked files in the layers are, so they are found after the document and its files move to another folder together; on open each of those links takes the path where its file was found. Update Links, and opening with Update Links: Automatically, read a modified file again for that art too, and saving a document refreshes its placed copies inside symbols and patterns of the other open documents. Evidence: `links_outside_the_layers_find_their_files_after_the_folder_moves`, `update_links_reads_files_again_inside_symbols_patterns_and_masks`, `saving_a_document_refreshes_its_placed_copies_inside_symbols_and_patterns`.
|
||||
- A linked file that is missing when a moved document is saved keeps the folder its relative path names, instead of a relative path worked out from the out-of-date absolute path, also for a `.ai` file another app wrote (such as `src/`), so the file is found once it is back in that folder. Evidence: `a_missing_link_keeps_its_relative_path_when_the_moved_document_is_saved`, `links_of_an_imported_file_are_found_by_their_relative_folder`, `a_missing_link_of_a_moved_document_keeps_its_relative_place`.
|
||||
- On Windows, a document opened from a mapped network drive finds its links stored with the share's own path (`\\server\share\…`), as the same document opened from the share does; links on other computers stay missing.
|
||||
- On Windows, a link on a drive mapped to a network share other than the document's (`Y:\…`) is missing, with the reason that the path is on another computer, as a link stored with that share's own path is; links on this computer's drives and on the document's share are followed.
|
||||
- In the Links panel, Shift-click selects a range of links and Ctrl/Cmd-click adds or removes one.
|
||||
|
||||
Reference in New Issue
Block a user