1use super::*;
2use onestore::FileDataReference;
3use onestore::{
4 RevisionIndex, Store,
5 document::{Document, Kind},
6};
7use rusqlite::OptionalExtension;
8use sha2::{Digest, Sha256};
9
10impl Replica {
11 /// Reads a previously downloaded external payload without network access.
12 /// Absence is distinct from an empty payload; every returned buffer passes its stored checksum.
13 pub fn cached_asset(&self, filename: &str, limit: usize) -> Result<Option<Vec<u8>>> {
14 cached(&*self.lock()?, &key(filename)?, limit)
15 }
16
17 /// Fetches a declared external payload and durably retains it without changing the edit queue.
18 /// Different bytes for an already cached identity return `AssetChanged`, preserving the cache.
19 /// Network I/O does not hold the cache mutex; a stale reference fails before local publication.
20 pub fn fetch_asset(
21 &self,
22 source: &mut impl crate::discover::Source,
23 section: &str,
24 filename: &str,
25 limit: usize,
26 ) -> Result<Vec<u8>> {
27 let key = key(filename)?;
28 if !self.references(&key)? {
29 return Err(io::Error::new(
30 io::ErrorKind::InvalidInput,
31 "The retained document images do not reference this external payload",
32 )
33 .into());
34 }
35 let bytes = crate::discover::read_external_asset(source, section, filename, limit)?;
36 if !self.references(&key)? {
37 return Err(io::Error::new(
38 io::ErrorKind::ResourceBusy,
39 "The external payload reference changed during download",
40 )
41 .into());
42 }
43 let mut connection = self.lock()?;
44 let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?;
45 let previous = match cached(&transaction, &key, bytes.len()) {
46 Err(Error::Io(error)) if error.kind() == io::ErrorKind::FileTooLarge => {
47 return Err(Error::AssetChanged);
48 }
49 other => other?,
50 };
51 if let Some(previous) = previous {
52 if previous != bytes {
53 return Err(Error::AssetChanged);
54 }
55 } else {
56 transaction.execute(
57 "INSERT INTO assets(name,data,sha256) VALUES (?1,?2,?3)",
58 params![key, &bytes, &Sha256::digest(&bytes)[..]],
59 )?;
60 }
61 transaction.commit()?;
62 Ok(bytes)
63 }
64}
65
66pub(crate) fn key(filename: &str) -> Result<String> {
67 format!("<file>{filename}").parse::<FileDataReference>()?;
68 Ok(filename.to_ascii_lowercase())
69}
70
71impl Replica {
72 /// Whether the local pages or the remote image declare the external payload `key`.
73 fn references(&self, key: &str) -> Result<bool> {
74 for image in [self.snapshot()?, self.remote_snapshot()?] {
75 let store = Store::parse(&image)?;
76 let index = RevisionIndex::parse(&store)?;
77 let document = Document::parse(&index)?;
78 if document.spaces.values().flat_map(|space| space.revisions.values())
79 .flat_map(|revision| revision.nodes.values()).any(|node| {
80 matches!(&node.kind, Kind::File { reference: FileDataReference::External(name), .. } if name.eq_ignore_ascii_case(key))
81 }) {
82 return Ok(true);
83 }
84 }
85 Ok(false)
86 }
87}
88
89pub(crate) fn cached(connection: &Connection, key: &str, limit: usize) -> Result<Option<Vec<u8>>> {
90 let length: Option<i64> = connection
91 .query_row(
92 "SELECT length(data) FROM assets WHERE name=?1",
93 [key],
94 |row| row.get(0),
95 )
96 .optional()?;
97 let Some(length) = length else {
98 return Ok(None);
99 };
100 let length =
101 usize::try_from(length).map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?;
102 if length > limit {
103 return Err(io::Error::from(io::ErrorKind::FileTooLarge).into());
104 }
105 let (bytes, expected): (Vec<u8>, Vec<u8>) = connection.query_row(
106 "SELECT data,sha256 FROM assets WHERE name=?1",
107 [key],
108 |row| Ok((row.get(0)?, row.get(1)?)),
109 )?;
110 if bytes.len() != length || Sha256::digest(&bytes)[..] != expected {
111 return Err(io::Error::new(
112 io::ErrorKind::InvalidData,
113 "Cached external payload checksum mismatch",
114 )
115 .into());
116 }
117 Ok(Some(bytes))
118}