mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
DES-10: linked graphics from IDML, packages and file checks agree with T6 linking
IDML imports link the files they read through links::link_to_file, the path File > Place takes, so a file read just after it was written keeps an unverified fingerprint and its status comes from the hash instead of reading as missing. File > Package and Links > Copy Links To point a linked graphic's record (paths, unverified fingerprint) at its copy. Regular-file metadata goes through io::file_metadata, which the file guard accepts.
This commit is contained in:
1 parent
e8701e3f4c
commit
9c66115fce
8 files changed
+69
-67
No files matched your search
@@ -139,13 +139,6 @@ impl AssetData {
|
||||
AssetData { key, repr: Repr::Linked(Arc::new(Linked { source, trusted: Mutex::new(None) })) }
|
||||
}
|
||||
|
||||
/// [`AssetData::linked_under`] with `bytes` (the file's, just read) already in the store, so
|
||||
/// the first draw doesn't read the file again.
|
||||
pub fn linked_resident(key: SourceKey, source: LinkedSource, bytes: Arc<Vec<u8>>) -> Self {
|
||||
store::global().keep(key, bytes);
|
||||
AssetData::linked_under(key, source)
|
||||
}
|
||||
|
||||
/// This value pointing at `source` (the file moved, the document was saved elsewhere, the
|
||||
/// fingerprint was brought up to date). It keeps the key when both name the same bytes
|
||||
/// (same size and content hash), so nothing decoded is decoded again.
|
||||
|
||||
@@ -100,10 +100,7 @@ pub fn read_linked(source: &LinkedSource, trusted: Option<Fingerprint>) -> Resul
|
||||
}
|
||||
|
||||
fn read_candidate(path: &Path, source: &LinkedSource, trusted: Option<Fingerprint>) -> Result<Loaded, Miss> {
|
||||
let meta = crate::io::metadata(path).map_err(|_| Miss::Absent)?;
|
||||
if !meta.is_file() {
|
||||
return Err(Miss::Absent);
|
||||
}
|
||||
let meta = crate::io::file_metadata(path).ok_or(Miss::Absent)?;
|
||||
let found = fingerprint_from(&meta);
|
||||
if found.size > MAX_LINKED_BYTES {
|
||||
return Err(Miss::TooLarge);
|
||||
|
||||
@@ -214,34 +214,17 @@ fn import_report_with_previews(
|
||||
Ok((document, warnings, story_ids))
|
||||
}
|
||||
|
||||
/// `asset`, whose embedded bytes were just read from the file its link names, as a linked asset:
|
||||
/// the file's record (hash, relative path, fingerprint) and a proxy (the document's preview when
|
||||
/// the importer kept one, else made from the bytes). The bytes stay resident for the first draw.
|
||||
/// `asset`, whose embedded bytes were just read from the file its link names, as a linked asset
|
||||
/// ([`super::links::link_to_file`]), drawn with the document's preview while the file can't be
|
||||
/// read when the importer kept one.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn link_read_file(asset: &mut designcraft_doc::Asset, package: Option<&std::path::Path>) {
|
||||
let Some(bytes) = asset.data.embedded_bytes().cloned() else { return };
|
||||
let Some(path) = asset.link.clone() else { return };
|
||||
asset.link_hash = Some(crate::asset_io::content_hash(&bytes));
|
||||
asset.link_relative = package.and_then(|d| super::native_links::relative_to(d, std::path::Path::new(&path)));
|
||||
if asset.proxy.is_none() {
|
||||
asset.proxy = designcraft_render::proxy::make_proxy(&bytes, asset.page).map(|p| designcraft_doc::Proxy {
|
||||
mime: p.mime.into(),
|
||||
pixels: p.pixels,
|
||||
data: designcraft_doc::AssetData::embedded(p.data),
|
||||
});
|
||||
}
|
||||
asset.storage = designcraft_doc::Storage::Linked;
|
||||
// A file just written has no settled fingerprint ([`super::links::loaded_fingerprint`]): the
|
||||
// hash tells its bytes.
|
||||
let source = designcraft_doc::LinkedSource {
|
||||
path,
|
||||
relative: asset.link_relative.clone(),
|
||||
document: package.map(std::path::Path::to_path_buf),
|
||||
len: bytes.len() as u64,
|
||||
fingerprint: asset.link_fingerprint,
|
||||
hash: asset.link_hash.clone(),
|
||||
};
|
||||
asset.data = designcraft_doc::AssetData::linked_resident(asset.data.key(), source, bytes);
|
||||
let preview = asset.proxy.take();
|
||||
// The fingerprint kept by the read (none for a file just written: its hash tells its bytes).
|
||||
let file = super::links::FileLink::of(std::path::Path::new(&path), asset.link_fingerprint);
|
||||
super::links::link_to_file(asset, bytes, file, package, preview);
|
||||
}
|
||||
|
||||
/// Placed PDFs read from IDML name their crop (`PDFCrop`): find where each box sits on its page,
|
||||
|
||||
@@ -200,15 +200,6 @@ impl FileLink {
|
||||
}
|
||||
}
|
||||
|
||||
/// `target` relative to the folder of the document file `document`, as link records keep it.
|
||||
fn relative_to(document: &Path, target: &Path) -> Option<String> {
|
||||
let document = std::path::absolute(document).ok()?;
|
||||
let relative = super::datamerge::relative_between(document.parent()?, target);
|
||||
#[cfg(windows)]
|
||||
let relative = relative.replace('\\', "/");
|
||||
Some(relative)
|
||||
}
|
||||
|
||||
/// The proxy a linked graphic is drawn with while its file can't be read: none for video and
|
||||
/// sound, or a file that doesn't decode.
|
||||
fn proxy_of(a: &Asset, bytes: &[u8]) -> Option<designcraft_doc::Proxy> {
|
||||
@@ -220,9 +211,9 @@ fn proxy_of(a: &Asset, bytes: &[u8]) -> Option<designcraft_doc::Proxy> {
|
||||
}
|
||||
|
||||
/// Give `a` the bytes of a graphic being placed, relinked or updated (its `mime` and `page` set).
|
||||
/// Bytes read from a file (`file`) link to it, unless `embed` or on the web, where they are
|
||||
/// embedded with the link record kept; the bytes just read stay in memory for the first draw.
|
||||
/// Other bytes (pasted, dropped, base64) are embedded without a link.
|
||||
/// Bytes read from a file (`file`) link to it ([`link_to_file`]), unless `embed` or on the web,
|
||||
/// where they are embedded with the link record kept. Other bytes (pasted, dropped, base64) are
|
||||
/// embedded without a link.
|
||||
pub(crate) fn give_bytes(a: &mut Asset, bytes: impl Into<Arc<Vec<u8>>>, file: Option<FileLink>, embed: bool, document: Option<&Path>) {
|
||||
let bytes: Arc<Vec<u8>> = bytes.into();
|
||||
a.proxy = None;
|
||||
@@ -234,28 +225,43 @@ pub(crate) fn give_bytes(a: &mut Asset, bytes: impl Into<Arc<Vec<u8>>>, file: Op
|
||||
a.data = AssetData::embedded(bytes);
|
||||
return;
|
||||
};
|
||||
let loaded = loaded_fingerprint(file.before, &bytes);
|
||||
a.link_relative = document.and_then(|d| relative_to(d, Path::new(&file.path)));
|
||||
a.link = Some(file.path);
|
||||
a.link_hash = Some(crate::asset_io::content_hash(&bytes));
|
||||
if embed || cfg!(target_arch = "wasm32") {
|
||||
a.link_fingerprint = loaded;
|
||||
record_file(a, &bytes, file, document);
|
||||
a.storage = Storage::Embedded;
|
||||
a.data = AssetData::embedded(bytes);
|
||||
return;
|
||||
}
|
||||
a.link_fingerprint = Some(loaded.unwrap_or(Fingerprint { size: bytes.len() as u64, mtime: UNVERIFIED_MTIME }));
|
||||
link_to_file(a, bytes, file, document, None);
|
||||
}
|
||||
|
||||
/// Link `a` to `file`, whose bytes (`bytes`) were just read: the file's record (paths, size and
|
||||
/// time, content hash) and a proxy to draw while the file can't be read (`preview` when there is
|
||||
/// one, else made from the bytes). The bytes stay in memory for the first draw.
|
||||
pub(crate) fn link_to_file(a: &mut Asset, bytes: Arc<Vec<u8>>, file: FileLink, document: Option<&Path>, preview: Option<designcraft_doc::Proxy>) {
|
||||
record_file(a, &bytes, file, document);
|
||||
a.link_fingerprint = Some(a.link_fingerprint.unwrap_or(Fingerprint { size: bytes.len() as u64, mtime: UNVERIFIED_MTIME }));
|
||||
let Some(source) = LinkedSource::of(a, document) else {
|
||||
a.storage = Storage::Embedded;
|
||||
a.proxy = None;
|
||||
a.data = AssetData::embedded(bytes);
|
||||
return;
|
||||
};
|
||||
a.storage = Storage::Linked;
|
||||
a.proxy = proxy_of(a, &bytes);
|
||||
a.proxy = preview.or_else(|| proxy_of(a, &bytes));
|
||||
a.data = AssetData::linked(source);
|
||||
a.data.keep_resident(bytes);
|
||||
}
|
||||
|
||||
/// Keep with `a` the record of `file`, which `bytes` were read from: its absolute path, its path
|
||||
/// relative to the `document` file, the size and time that vouch for the bytes
|
||||
/// ([`loaded_fingerprint`]) and their content hash.
|
||||
fn record_file(a: &mut Asset, bytes: &[u8], file: FileLink, document: Option<&Path>) {
|
||||
a.link_fingerprint = loaded_fingerprint(file.before, bytes);
|
||||
a.link_relative = document.and_then(|d| super::native_links::relative_to(d, Path::new(&file.path)));
|
||||
a.link = Some(file.path);
|
||||
a.link_hash = Some(crate::asset_io::content_hash(bytes));
|
||||
}
|
||||
|
||||
/// Does `bytes` (a file's) hold `a`'s bytes: its recorded hash, or, without one, its bytes?
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn holds(a: &Asset, bytes: &[u8]) -> bool {
|
||||
@@ -274,7 +280,7 @@ fn linked_status(a: &Asset, document: Option<&Path>, checks: &mut ImageChecks) -
|
||||
{
|
||||
let mut status = "missing";
|
||||
for path in crate::asset_io::candidates(&source) {
|
||||
let Some(meta) = crate::io::metadata(&path).ok().filter(std::fs::Metadata::is_file) else { continue };
|
||||
let Some(meta) = crate::io::file_metadata(&path) else { continue };
|
||||
let now = crate::asset_io::fingerprint_from(&meta);
|
||||
if Some(now) == source.fingerprint && settled(&now) {
|
||||
return ("ok", None);
|
||||
@@ -1014,7 +1020,7 @@ fn write_into(folder: &Path, name: &str, mime: &str, bytes: &[u8]) -> Result<std
|
||||
/// Does the regular file at `path` hold exactly `bytes`?
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn file_holds(path: &Path, bytes: &[u8]) -> bool {
|
||||
crate::io::metadata(path).is_ok_and(|m| m.is_file() && m.len() == bytes.len() as u64)
|
||||
crate::io::file_metadata(path).is_some_and(|m| m.len() == bytes.len() as u64)
|
||||
&& crate::io::read_capped(path, bytes.len() as u64).is_ok_and(|b| b == bytes)
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ fn locate(a: &mut Asset, source: &LinkedSource, document: Option<&Path>, work: &
|
||||
let found: Vec<(PathBuf, Fingerprint)> = crate::asset_io::candidates(source)
|
||||
.into_iter()
|
||||
.filter_map(|p| {
|
||||
let meta = crate::io::metadata(&p).ok().filter(std::fs::Metadata::is_file)?;
|
||||
let meta = crate::io::file_metadata(&p)?;
|
||||
Some((p, crate::asset_io::fingerprint_from(&meta)))
|
||||
})
|
||||
.collect();
|
||||
@@ -287,8 +287,7 @@ fn migration(a: &Asset, document: &Path) -> Option<Work> {
|
||||
fingerprint: None,
|
||||
hash: None,
|
||||
};
|
||||
let path =
|
||||
crate::asset_io::candidates(&source).into_iter().find(|p| crate::io::metadata(p).is_ok_and(|m| m.is_file() && m.len() == source.len))?;
|
||||
let path = crate::asset_io::candidates(&source).into_iter().find(|p| crate::io::file_metadata(p).is_some_and(|m| m.len() == source.len))?;
|
||||
Some(Work::Migrate { asset: a.id, key: a.data.key(), bytes, path, page: a.page })
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -387,7 +386,7 @@ fn check(work: Work, cancel: &AtomicBool) -> Option<Outcome> {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn fingerprint(path: &Path) -> Option<Fingerprint> {
|
||||
crate::io::metadata(path).ok().filter(std::fs::Metadata::is_file).map(|m| crate::asset_io::fingerprint_from(&m))
|
||||
crate::io::file_metadata(path).map(|m| crate::asset_io::fingerprint_from(&m))
|
||||
}
|
||||
|
||||
/// Read the file at `path`, which must be `len` bytes, handing each chunk to `keep` (with its
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::io::Write;
|
||||
use super::{CommandSpec, bad, cmd, has_doc, str_param};
|
||||
use crate::{EngineError, Result, Session};
|
||||
use designcraft_compose::ComposedStory;
|
||||
use designcraft_doc::{AssetId, Document};
|
||||
use designcraft_doc::{AssetId, Document, Fingerprint, LinkedSource, Storage};
|
||||
use designcraft_fonts::{FaceRef, FontFace, FontSource};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -354,7 +354,21 @@ fn write_links(d: &Document, root: &OutputRoot, dir: &Path, only: Option<&[Asset
|
||||
// The copy is the linked file now, and the original's size and time say nothing of it.
|
||||
// Its own are too recent to vouch for it yet: the first check compares the bytes and
|
||||
// keeps them (`remember_link_files`).
|
||||
na.set_link(Some(link), None);
|
||||
if na.storage == Storage::Linked {
|
||||
// A linked graphic is told by its hash until then; its paths name the copy.
|
||||
na.link_relative = relative_to.map(|_| link.clone());
|
||||
na.set_link(Some(link), Some(Fingerprint { size: bytes.len() as u64, mtime: super::links::UNVERIFIED_MTIME }));
|
||||
// Without a folder the link is the copy's full path, which the open document reads
|
||||
// from now on; a package's document reads its copies once opened beside them.
|
||||
if relative_to.is_none()
|
||||
&& na.data.is_linked()
|
||||
&& let Some(source) = LinkedSource::of(&na, None)
|
||||
{
|
||||
na.data = na.data.with_source(source);
|
||||
}
|
||||
} else {
|
||||
na.set_link(Some(link), None);
|
||||
}
|
||||
out.assets.insert(id, Arc::new(na));
|
||||
written.push(path);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@ fn relocated_packages_rebind_embedded_image_links_on_open() {
|
||||
let mut opened = Session::new();
|
||||
opened.execute(command, &json!({"path": moved.join(format!("OwnedPackage.{extension}")).to_string_lossy()})).unwrap();
|
||||
let asset = opened.doc().unwrap().doc.assets.values().next().unwrap();
|
||||
assert_eq!(**asset.data.embedded_bytes().unwrap(), png, "{extension}: the packaged pixels");
|
||||
// The IDML copy links Links/owned.png, so its graphic opens as a link to that file.
|
||||
assert_eq!(asset.data.load().ok().as_deref(), Some(&png), "{extension}: the packaged pixels");
|
||||
assert_eq!(asset.link.as_deref(), expected.to_str(), "{extension}: use the relocated Links file");
|
||||
let links = opened.execute("links.list", &json!({})).unwrap();
|
||||
assert_eq!(links[0]["status"], "ok", "{extension}");
|
||||
@@ -267,6 +268,8 @@ fn package_leaves_missing_links_missing() {
|
||||
for (command, extension) in [("file.open", "designcraft"), ("file.openIdml", "idml")] {
|
||||
let mut opened = Session::new();
|
||||
opened.execute(command, &json!({"path": out.join(format!("TwoLinks.{extension}")).to_string_lossy()})).unwrap();
|
||||
// The open checks linked files in the background.
|
||||
super::super::native_links::wait(&mut opened);
|
||||
assert_eq!(status(&mut opened, "gone.svg"), Some(json!("missing")), "{extension}");
|
||||
assert_eq!(status(&mut opened, "kept.svg"), Some(json!("ok")), "{extension}");
|
||||
}
|
||||
@@ -329,15 +332,17 @@ fn package_writes_document_links_and_report() {
|
||||
let link = packed.assets.values().next().unwrap().link.clone().unwrap();
|
||||
assert!(link.contains("Links"), "{link}");
|
||||
// The link is the copy now: the original file's size and time don't go with it (the copy's
|
||||
// own are too recent to vouch for it; the first check compares the bytes and keeps them).
|
||||
// own are too recent to vouch for it; the first check compares the bytes by their hash and
|
||||
// keeps them).
|
||||
let unverified = Some(super::super::links::UNVERIFIED_MTIME);
|
||||
assert!(fingerprint(&out.join("Links").join("logo.svg")).is_some());
|
||||
assert_eq!(packed.assets.values().next().unwrap().link_fingerprint, None);
|
||||
assert_eq!(packed.assets.values().next().unwrap().link_fingerprint.map(|f| f.mtime), unverified);
|
||||
// Copy Links To relinks the open document.
|
||||
let r = s.execute("links.copyTo", &json!({"dir": dir.join("Copies").to_string_lossy()})).unwrap();
|
||||
assert_eq!(r["copied"], 1);
|
||||
let relinked = s.doc().unwrap().doc.assets.values().next().unwrap().clone();
|
||||
assert!(relinked.link.as_deref().unwrap().contains("Copies"));
|
||||
assert_eq!(relinked.link_fingerprint, None);
|
||||
assert_eq!(relinked.link_fingerprint.map(|f| f.mtime), unverified);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -214,9 +214,14 @@ pub fn metadata(path: impl AsRef<Path>) -> std::io::Result<Metadata> {
|
||||
std::fs::metadata(path)
|
||||
}
|
||||
|
||||
/// [`metadata`] of the regular file at `path`: `None` when there is none automation may read.
|
||||
pub fn file_metadata(path: impl AsRef<Path>) -> Option<Metadata> {
|
||||
metadata(path).ok().filter(Metadata::is_file)
|
||||
}
|
||||
|
||||
/// Is there a file at `path` that automation may read?
|
||||
pub fn is_file(path: impl AsRef<Path>) -> bool {
|
||||
metadata(path).is_ok_and(|m| m.is_file())
|
||||
file_metadata(path).is_some()
|
||||
}
|
||||
|
||||
/// Is there a folder at `path` that automation may read?
|
||||
|
||||
Reference in new issue
Block a user