1use crate::{Bundle, CachableBundle, FileIndex, FileInfo};
11use std::{
12 fs::{self, File},
13 io::{self, BufReader, Read, Write},
14 path::{Path, PathBuf},
15 process,
16 str::FromStr,
17};
18use tectonic_errors::{anyhow::Context, prelude::*};
19use tectonic_io_base::{
20 app_dirs,
21 digest::{self, DigestData},
22 InputHandle, InputOrigin, IoProvider, OpenResult,
23};
24use tectonic_status_base::StatusBackend;
25
26fn file_create_write<P, F, E>(path: P, write_fn: F) -> Result<()>
28where
29 P: AsRef<Path>,
30 F: FnOnce(&mut File) -> std::result::Result<(), E>,
31 E: std::error::Error + 'static + Sync + Send,
32{
33 let path = path.as_ref();
34 let mut f = atry!(
35 File::create(path);
36 ["couldn't open {} for writing", path.display()]
37 );
38 atry!(
39 write_fn(&mut f);
40 ["couldn't write to {}", path.display()]
41 );
42 Ok(())
43}
44
45macro_rules! ensure_dir {
48 (inline, $path:expr) => {
49 {
50 atry!(
51 fs::create_dir_all(&$path);
52 ["failed to create directory `{}` or one of its parents", $path.display()]
53 );
54 $path
55 }
56 };
57
58 ($path:expr) => {
59 atry!(
60 fs::create_dir_all(&$path);
61 ["failed to create directory `{}` or one of its parents", $path.display()]
62 );
63 };
64}
65
66pub struct BundleCache<'this, T> {
75 only_cached: bool,
84
85 bundle: Box<dyn CachableBundle<'this, T>>,
88
89 cache_root: PathBuf,
92
93 bundle_hash: DigestData,
95}
96
97impl<'this, T: FileIndex<'this>> BundleCache<'this, T> {
98 pub fn new(
104 mut bundle: Box<dyn CachableBundle<'this, T>>,
105 only_cached: bool,
106 cache_root: Option<PathBuf>,
107 ) -> Result<Self> {
108 let cache_root = match cache_root {
110 None => app_dirs::get_user_cache_dir("bundles").context("while making cache root")?,
111 Some(p) => ensure_dir!(inline, p),
112 };
113
114 let hash_dir = ensure_dir!(inline, &cache_root.join("hashes"));
115 let hash_file = hash_dir.join(app_dirs::app_dirs2::sanitized(&bundle.get_location()));
116
117 let saved_hash = {
118 if !hash_file.exists() {
119 None
120 } else {
121 match File::open(&hash_file) {
122 Err(e) => return Err(e.into()),
123 Ok(f) => {
124 let mut digest_text = String::with_capacity(digest::DIGEST_LEN);
125 f.take(digest::DIGEST_LEN as u64)
126 .read_to_string(&mut digest_text)
127 .with_context(|| {
128 format!("while reading hash from {hash_file:?} in cache")
129 })?;
130 Some(
131 DigestData::from_str(&digest_text)
132 .with_context(|| format!("while parsing hash `{digest_text}`"))?,
133 )
134 }
135 }
136 }
137 };
138
139 let bundle_hash: DigestData = match saved_hash {
154 Some(cached) => cached,
155 None if only_cached => bail!(
156 "bundle is not cached and offline mode forbids network access"
157 ),
158 None => {
159 let live = bundle
160 .get_digest()
161 .context("while fetching the bundle digest from the network")?;
162 file_create_write(&hash_file, |f| writeln!(f, "{}", &live.to_string()))
163 .with_context(|| {
164 format!("while writing bundle hash to {hash_file:?} in cache")
165 })?;
166 live
167 }
168 };
169 let bundle = BundleCache {
172 only_cached,
173 bundle,
174 cache_root,
175 bundle_hash,
176 };
177
178 ensure_dir!(&bundle
187 .cache_root
188 .join(format!("data/{}", bundle.bundle_hash)));
189
190 Ok(bundle)
191 }
192
193 fn get_file_path(&self, info: &T::InfoType) -> PathBuf {
195 let mut out = self.cache_root.clone();
196 out.push(format!("data/{}", self.bundle_hash));
197 out.push(info.path());
198 out
199 }
200
201 fn get_file_path_tmp(&self, info: &T::InfoType) -> PathBuf {
205 let mut out = self.cache_root.clone();
206 out.push(format!("data/{}", self.bundle_hash));
207 out.push(format!("{}-tmp-pid{}", info.path(), process::id()));
208 out
209 }
210
211 fn ensure_index(&mut self) -> Result<()> {
212 let target = self
213 .cache_root
214 .join(format!("data/{}.index", self.bundle_hash));
215
216 if target.exists() {
224 if self.bundle.index().is_initialized() {
225 return Ok(());
226 }
227
228 let mut file = File::open(&target)
230 .with_context(|| format!("while opening index {target:?} in cache"))?;
231 self.bundle
232 .initialize_index(&mut file)
233 .with_context(|| format!("while inititalizing index using cached {target:?}"))?;
234 } else {
235 let tmp_target = self.cache_root.join(format!(
240 "data/{}.index-tmp-pid{}",
241 self.bundle_hash,
242 process::id()
243 ));
244
245 let mut reader = self
246 .bundle
247 .get_index_reader()
248 .context("while getting index reader")?;
249 let mut file = File::create(&tmp_target)
250 .with_context(|| format!("while creating index {tmp_target:?} in cache"))?;
251 io::copy(&mut reader, &mut file)
252 .with_context(|| format!("while writing index {tmp_target:?} in cache"))?;
253 drop(file);
254
255 fs::rename(&tmp_target, &target).with_context(|| {
256 format!("while renaming index {tmp_target:?} to {target:?} in cache")
257 })?;
258
259 if self.bundle.index().is_initialized() {
260 return Ok(());
261 }
262
263 let mut file = File::open(&target)
264 .with_context(|| format!("while opening index from {target:?} in cache"))?;
265 self.bundle
266 .initialize_index(&mut file)
267 .with_context(|| format!("while initializing index {target:?} in cache"))?;
268 }
269
270 Ok(())
271 }
272
273 fn get_fileinfo(&mut self, name: &str) -> OpenResult<(bool, T::InfoType)> {
278 if let Err(e) = self.ensure_index() {
279 return OpenResult::Err(e);
280 };
281
282 let info = match self.bundle.search(name) {
283 Some(i) => i,
284 None => return OpenResult::NotAvailable,
285 };
286
287 let target = self.get_file_path(&info);
288 OpenResult::Ok((target.exists(), info))
289 }
290
291 fn fetch_file(
294 &mut self,
295 info: T::InfoType,
296 status: &mut dyn StatusBackend,
297 ) -> OpenResult<PathBuf> {
298 let target = self.get_file_path(&info);
299 match fs::create_dir_all(target.parent().unwrap()) {
300 Ok(()) => {}
301 Err(e) => return OpenResult::Err(e.into()),
302 };
303
304 if target.exists() {
306 return OpenResult::Ok(target);
307 }
308
309 if self.only_cached {
311 return OpenResult::NotAvailable;
312 }
313
314 let mut handle = match self.bundle.open_fileinfo(&info, status) {
316 OpenResult::Ok(c) => c,
317 OpenResult::Err(e) => return OpenResult::Err(e),
318 OpenResult::NotAvailable => return OpenResult::NotAvailable,
319 };
320
321 let tmp_path = self.get_file_path_tmp(&info);
324 if let Err(e) = file_create_write(&tmp_path, |f| io::copy(&mut handle, f).map(|_| ())) {
325 return OpenResult::Err(e);
326 }
327 if let Err(e) = fs::rename(&tmp_path, &target) {
328 return OpenResult::Err(e.into());
329 };
330
331 OpenResult::Ok(target)
332 }
333}
334
335impl<'this, T: FileIndex<'this>> IoProvider for BundleCache<'this, T> {
336 fn input_open_name(
337 &mut self,
338 name: &str,
339 status: &mut dyn StatusBackend,
340 ) -> OpenResult<InputHandle> {
341 let path = match self.get_fileinfo(name) {
342 OpenResult::NotAvailable => return OpenResult::NotAvailable,
343 OpenResult::Err(e) => return OpenResult::Err(e),
344 OpenResult::Ok((true, f)) => self.get_file_path(&f),
345 OpenResult::Ok((false, f)) => match self.fetch_file(f, status) {
346 OpenResult::Ok(p) => p,
347 OpenResult::NotAvailable => return OpenResult::NotAvailable,
348 OpenResult::Err(e) => return OpenResult::Err(e),
349 },
350 };
351
352 let f = match File::open(path) {
353 Ok(f) => f,
354 Err(e) => return OpenResult::Err(e.into()),
355 };
356
357 OpenResult::Ok(InputHandle::new_read_only(
358 name,
359 BufReader::new(f),
360 InputOrigin::Other,
361 ))
362 }
363}
364
365impl<'this, T: FileIndex<'this>> Bundle for BundleCache<'this, T> {
366 fn get_digest(&mut self) -> Result<DigestData> {
367 Ok(self.bundle_hash)
368 }
369
370 fn all_files(&self) -> Vec<String> {
371 self.bundle.all_files()
372 }
373}