use fs2::FileExt;
use std::{
collections::HashMap,
env,
fs::{self, File},
io::{BufRead, BufReader, Error as IoError, ErrorKind as IoErrorKind, Read, Write},
path::{Path, PathBuf},
str::FromStr,
};
use tectonic_errors::prelude::*;
use tectonic_io_base::{
app_dirs,
digest::{self, Digest, DigestData},
try_open_file, InputHandle, InputOrigin, IoProvider, OpenResult,
};
use tectonic_status_base::{tt_warning, StatusBackend};
use crate::Bundle;
#[derive(Debug)]
pub struct Cache {
root: PathBuf,
}
impl Cache {
pub fn get_user_default() -> Result<Self> {
let env_cache_path = env::var_os("TECTONIC_CACHE_DIR");
let cache_path = match env_cache_path {
Some(env_cache_path) => {
let env_cache_path = env_cache_path.into();
fs::create_dir_all(&env_cache_path)?;
env_cache_path
}
None => app_dirs::ensure_user_cache_dir("")?,
};
Ok(Cache { root: cache_path })
}
pub fn get_for_custom_directory<P: Into<PathBuf>>(root: P) -> Self {
Cache { root: root.into() }
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn open<CB: CacheBackend>(
&mut self,
url: &str,
only_cached: bool,
status: &mut dyn StatusBackend,
) -> Result<CachingBundle<CB>> {
CachingBundle::new(url, only_cached, status, &self.root)
}
}
#[derive(Clone, Debug)]
pub struct BackendPullData {
pub resolved_url: String,
pub digest: DigestData,
pub index: String,
}
pub trait CacheBackend: Sized {
type FileInfo: Clone;
fn open_with_pull(
start_url: &str,
status: &mut dyn StatusBackend,
) -> Result<(Self, BackendPullData)>;
fn open_with_quick_check(
resolved_url: &str,
digest_file_info: &Self::FileInfo,
status: &mut dyn StatusBackend,
) -> Result<Option<(Self, DigestData)>>;
fn parse_index_line(line: &str) -> Result<(String, Self::FileInfo)>;
fn get_file(
&mut self,
name: &str,
info: &Self::FileInfo,
status: &mut dyn StatusBackend,
) -> Result<Vec<u8>>;
}
#[derive(Clone, Copy, Debug)]
struct CachedFileInfo {
_length: u64,
digest: DigestData,
}
#[derive(Debug)]
pub struct CachingBundle<CB: CacheBackend> {
start_url: String,
resolved_url: String,
cached_digest: DigestData,
contents: HashMap<String, CachedFileInfo>,
index: HashMap<String, CB::FileInfo>,
only_cached: bool,
backend: Option<CB>,
digest_path: PathBuf,
resolved_base: PathBuf,
manifest_path: PathBuf,
data_base: PathBuf,
}
#[derive(Clone, Debug)]
struct CachedPullData<FI> {
pub digest: DigestData,
pub resolved_url: String,
pub index: HashMap<String, FI>,
}
impl<CB: CacheBackend> CachingBundle<CB> {
fn new(
start_url: &str,
only_cached: bool,
status: &mut dyn StatusBackend,
cache_root: &Path,
) -> Result<Self> {
let digest_path =
ensure_cache_dir(cache_root, "urls")?.join(app_dirs::app_dirs2::sanitized(start_url));
let resolved_base = ensure_cache_dir(cache_root, "redirects")?;
let index_base = ensure_cache_dir(cache_root, "indexes")?;
let manifest_base = ensure_cache_dir(cache_root, "manifests")?;
let data_base = ensure_cache_dir(cache_root, "files")?;
let mut backend = None;
let cached_pull_data =
match load_cached_pull_data::<CB>(&digest_path, &resolved_base, &index_base)? {
Some(c) => c,
None => {
let (new_backend, pull_data) = CB::open_with_pull(start_url, status)?;
backend = Some(new_backend);
let digest_text = pull_data.digest.to_string();
file_create_write(&digest_path, |f| writeln!(f, "{}", &digest_text))?;
file_create_write(make_txt_path(&resolved_base, &digest_text), |f| {
f.write_all(pull_data.resolved_url.as_bytes())
})?;
file_create_write(make_txt_path(&index_base, &digest_text), |f| {
f.write_all(pull_data.index.as_bytes())
})?;
atry!(
load_cached_pull_data::<CB>(&digest_path, &resolved_base, &index_base)?;
["cache files missing even after they were created"]
)
}
};
let cached_digest = cached_pull_data.digest;
let manifest_path = make_txt_path(&manifest_base, &cached_digest.to_string());
let mut contents = HashMap::new();
match try_open_file(&manifest_path) {
OpenResult::NotAvailable => {}
OpenResult::Err(e) => {
return Err(e);
}
OpenResult::Ok(mfile) => {
if let Err(e) = mfile.lock_shared() {
tt_warning!(status, "failed to lock manifest file \"{}\" for reading; this might be fine",
manifest_path.display(); e.into());
}
let f = BufReader::new(mfile);
for res in f.lines() {
let line = res?;
let mut bits = line.rsplitn(3, ' ');
let (original_name, length, digest) =
match (bits.next(), bits.next(), bits.next(), bits.next()) {
(Some(s), Some(t), Some(r), None) => (r, t, s),
_ => continue,
};
let name = original_name.to_owned();
let length = match length.parse::<u64>() {
Ok(l) => l,
Err(_) => continue,
};
let digest = if digest == "-" {
continue;
} else {
match DigestData::from_str(digest) {
Ok(d) => d,
Err(e) => {
tt_warning!(status, "ignoring bad digest data \"{}\" for \"{}\" in \"{}\"",
&digest, original_name, manifest_path.display() ; e);
continue;
}
}
};
contents.insert(
name,
CachedFileInfo {
_length: length,
digest,
},
);
}
}
}
Ok(CachingBundle {
start_url: start_url.to_owned(),
resolved_url: cached_pull_data.resolved_url,
digest_path,
cached_digest,
manifest_path,
data_base,
resolved_base,
contents,
only_cached,
backend,
index: cached_pull_data.index,
})
}
fn save_to_manifest(&mut self, name: &str, length: u64, digest: DigestData) -> Result<()> {
let digest_text = digest.to_string();
let mut man = fs::OpenOptions::new()
.append(true)
.create(true)
.read(true)
.open(&self.manifest_path)?;
atry!(
man.lock_exclusive();
["failed to lock manifest file \"{}\" for writing", self.manifest_path.display()]
);
if !name.contains(|c| c == '\n' || c == '\r') {
writeln!(man, "{} {} {}", name, length, digest_text)?;
}
self.contents.insert(
name.to_owned(),
CachedFileInfo {
_length: length,
digest,
},
);
Ok(())
}
fn ensure_backend_validity(&mut self, status: &mut dyn StatusBackend) -> Result<()> {
if self.backend.is_some() {
return Ok(());
}
if let Some(info) = self.index.get(digest::DIGEST_NAME) {
if let Ok(Some((backend, digest))) =
CB::open_with_quick_check(&self.resolved_url, info, status)
{
if self.cached_digest == digest {
self.backend = Some(backend);
return Ok(());
}
}
}
let (new_backend, pull_data) = CB::open_with_pull(&self.start_url, status)?;
if self.cached_digest != pull_data.digest {
file_create_write(&self.digest_path, |f| {
writeln!(f, "{}", pull_data.digest.to_string())
})?;
bail!("backend digest changed; rerun tectonic to use updated information");
}
if self.resolved_url != pull_data.resolved_url {
let resolved_path = make_txt_path(&self.resolved_base, &pull_data.digest.to_string());
file_create_write(&resolved_path, |f| {
f.write_all(pull_data.resolved_url.as_bytes())
})?;
self.resolved_url = pull_data.resolved_url;
}
self.backend = Some(new_backend);
Ok(())
}
fn ensure_file_availability(
&mut self,
name: &str,
status: &mut dyn StatusBackend,
) -> OpenResult<PathBuf> {
if let Some(info) = self.contents.get(name) {
return match info.digest.create_two_part_path(&self.data_base) {
Ok(p) => OpenResult::Ok(p),
Err(e) => OpenResult::Err(e),
};
}
if self.only_cached {
return OpenResult::NotAvailable;
}
let info = match self.index.get(name).cloned() {
Some(info) => info,
None => return OpenResult::NotAvailable,
};
if let Err(e) = self.ensure_backend_validity(status) {
return OpenResult::Err(e);
}
let content = match self.backend.as_mut().unwrap().get_file(name, &info, status) {
Ok(c) => c,
Err(e) => return OpenResult::Err(e),
};
let length = content.len();
let mut digest_builder = digest::create();
digest_builder.update(&content);
let digest = DigestData::from(digest_builder);
let final_path = match digest.create_two_part_path(&self.data_base) {
Ok(p) => p,
Err(e) => return OpenResult::Err(e),
};
if !final_path.exists() {
if let Err(e) = file_create_write(&final_path, |f| f.write_all(&content)) {
return OpenResult::Err(e);
}
let mut perms = match fs::metadata(&final_path) {
Ok(p) => p,
Err(e) => {
return OpenResult::Err(e.into());
}
}
.permissions();
perms.set_readonly(true);
if let Err(e) = fs::set_permissions(&final_path, perms) {
return OpenResult::Err(e.into());
}
}
if let Err(e) = self.save_to_manifest(name, length as u64, digest) {
return OpenResult::Err(e);
}
OpenResult::Ok(final_path)
}
}
impl<CB: CacheBackend> IoProvider for CachingBundle<CB> {
fn input_open_name(
&mut self,
name: &str,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
let path = match self.ensure_file_availability(name, status) {
OpenResult::Ok(p) => p,
OpenResult::NotAvailable => return OpenResult::NotAvailable,
OpenResult::Err(e) => return OpenResult::Err(e),
};
let f = match File::open(&path) {
Ok(f) => f,
Err(e) => return OpenResult::Err(e.into()),
};
OpenResult::Ok(InputHandle::new_read_only(
name,
BufReader::new(f),
InputOrigin::Other,
))
}
}
impl<CB: CacheBackend> Bundle for CachingBundle<CB> {
fn get_digest(&mut self, _status: &mut dyn StatusBackend) -> Result<DigestData> {
Ok(self.cached_digest)
}
fn all_files(&mut self, status: &mut dyn StatusBackend) -> Result<Vec<String>> {
if !self.only_cached {
self.ensure_backend_validity(status)?;
}
Ok(self.index.keys().cloned().collect())
}
}
fn load_cached_pull_data<CB: CacheBackend>(
digest_path: &Path,
resolved_base: &Path,
index_base: &Path,
) -> Result<Option<CachedPullData<CB::FileInfo>>> {
return match inner::<CB>(digest_path, resolved_base, index_base) {
Ok(r) => Ok(Some(r)),
Err(e) => {
if let Some(ioe) = e.downcast_ref::<IoError>() {
if ioe.kind() == IoErrorKind::NotFound {
return Ok(None);
}
}
Err(e)
}
};
fn inner<CB: CacheBackend>(
digest_path: &Path,
resolved_base: &Path,
index_base: &Path,
) -> Result<CachedPullData<CB::FileInfo>> {
let digest_text = {
let f = File::open(digest_path)?;
let mut digest_text = String::with_capacity(digest::DIGEST_LEN);
f.take(digest::DIGEST_LEN as u64)
.read_to_string(&mut digest_text)?;
digest_text
};
let resolved_path = make_txt_path(resolved_base, &digest_text);
let resolved_url = fs::read_to_string(resolved_path)?;
let index_path = make_txt_path(index_base, &digest_text);
let index = {
let f = File::open(index_path)?;
let mut index = HashMap::new();
for line in BufReader::new(f).lines() {
if let Ok((name, info)) = CB::parse_index_line(&line?) {
index.insert(name, info);
}
}
index
};
Ok(CachedPullData {
digest: DigestData::from_str(&digest_text)?,
resolved_url,
index,
})
}
}
fn file_create_write<P, F, E>(path: P, write_fn: F) -> Result<()>
where
P: AsRef<Path>,
F: FnOnce(&mut File) -> std::result::Result<(), E>,
E: std::error::Error + 'static + Sync + Send,
{
let path = path.as_ref();
let mut f = atry!(
File::create(path);
["couldn't open {} for writing", path.display()]
);
atry!(
write_fn(&mut f);
["couldn't write to {}", path.display()]
);
Ok(())
}
fn ensure_cache_dir(root: &Path, path: &str) -> Result<PathBuf> {
let full_path = root.join(path);
atry!(
fs::create_dir_all(&full_path);
["failed to create directory `{}` or one of its parents", full_path.display()]
);
Ok(full_path)
}
fn make_txt_path(base: &Path, name: &str) -> PathBuf {
base.join(&name).with_extension("txt")
}