use crate::{
ttb::{TTBFileIndex, TTBFileInfo, TTBv1Header},
Bundle, CachableBundle, FileIndex, FileInfo, NET_RETRY_ATTEMPTS, NET_RETRY_SLEEP_MS,
};
use flate2::read::GzDecoder;
use std::{
convert::TryFrom,
io::{Cursor, Read},
thread,
time::Duration,
};
use tectonic_errors::prelude::*;
use tectonic_geturl::{DefaultBackend, DefaultRangeReader, GetUrlBackend, RangeReader};
use tectonic_io_base::{InputHandle, InputOrigin, IoProvider, OpenResult};
use tectonic_status_base::{tt_note, tt_warning, StatusBackend};
fn read_fileinfo(fileinfo: &TTBFileInfo, reader: &mut DefaultRangeReader) -> Result<Box<dyn Read>> {
let stream = reader.read_range(fileinfo.start, fileinfo.gzip_len as usize)?;
Ok(Box::new(GzDecoder::new(stream)))
}
pub struct TTBNetBundle<T>
where
for<'a> T: FileIndex<'a>,
{
url: String,
index: T,
reader: Option<DefaultRangeReader>,
}
impl TTBNetBundle<TTBFileIndex> {
pub fn new(url: String) -> Result<Self> {
Ok(TTBNetBundle {
reader: None,
index: TTBFileIndex::default(),
url,
})
}
fn connect_reader(&mut self) -> Result<()> {
if self.reader.is_some() {
return Ok(());
}
let geturl_backend = DefaultBackend::default();
self.reader = Some(geturl_backend.open_range_reader(&self.url));
Ok(())
}
fn get_header(&mut self) -> Result<TTBv1Header> {
self.connect_reader()?;
let mut header: [u8; 70] = [0u8; 70];
self.reader
.as_mut()
.unwrap()
.read_range(0, 70)?
.read_exact(&mut header)?;
let header = TTBv1Header::try_from(header)?;
Ok(header)
}
fn ensure_index(&mut self) -> Result<()> {
if self.index.is_initialized() {
return Ok(());
}
let mut reader = self.get_index_reader()?;
self.index.initialize(&mut reader)?;
Ok(())
}
}
impl IoProvider for TTBNetBundle<TTBFileIndex> {
fn input_open_name(
&mut self,
name: &str,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
if let Err(e) = self.ensure_index() {
return OpenResult::Err(e);
};
let info = match self.search(name) {
None => return OpenResult::NotAvailable,
Some(s) => s,
};
self.open_fileinfo(&info, status)
}
}
impl Bundle for TTBNetBundle<TTBFileIndex> {
fn all_files(&self) -> Vec<String> {
self.index.iter().map(|x| x.path().to_owned()).collect()
}
fn get_digest(&mut self) -> Result<tectonic_io_base::digest::DigestData> {
let header = self.get_header()?;
Ok(header.digest)
}
}
impl CachableBundle<'_, TTBFileIndex> for TTBNetBundle<TTBFileIndex> {
fn get_location(&mut self) -> String {
self.url.clone()
}
fn initialize_index(&mut self, source: &mut dyn Read) -> Result<()> {
self.index.initialize(source)?;
Ok(())
}
fn index(&mut self) -> &mut TTBFileIndex {
&mut self.index
}
fn search(&mut self, name: &str) -> Option<TTBFileInfo> {
self.index.search(name)
}
fn get_index_reader(&mut self) -> Result<Box<dyn Read>> {
self.connect_reader()?;
let header = self.get_header()?;
read_fileinfo(
&TTBFileInfo {
start: header.index_start,
gzip_len: header.index_gzip_len,
real_len: header.index_real_len,
path: "".to_owned(),
name: "".to_owned(),
hash: None,
},
self.reader.as_mut().unwrap(),
)
}
fn open_fileinfo(
&mut self,
info: &TTBFileInfo,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
let mut v: Vec<u8> = Vec::with_capacity(info.real_len as usize);
tt_note!(status, "downloading {}", info.name);
if info.gzip_len == 0 {
return OpenResult::Ok(InputHandle::new_read_only(
info.name.to_owned(),
Cursor::new(v),
InputOrigin::Other,
));
}
for i in 0..NET_RETRY_ATTEMPTS {
let mut reader = match read_fileinfo(info, self.reader.as_mut().unwrap()) {
Ok(r) => r,
Err(e) => {
tt_warning!(status,
"failure fetching \"{}\" from network ({}/{NET_RETRY_ATTEMPTS})",
info.name, i+1; e
);
thread::sleep(Duration::from_millis(NET_RETRY_SLEEP_MS));
continue;
}
};
match reader.read_to_end(&mut v) {
Ok(_) => {}
Err(e) => {
tt_warning!(status,
"failure downloading \"{}\" from network ({}/{NET_RETRY_ATTEMPTS})",
info.name, i+1; e.into()
);
thread::sleep(Duration::from_millis(NET_RETRY_SLEEP_MS));
continue;
}
};
return OpenResult::Ok(InputHandle::new_read_only(
info.name.to_owned(),
Cursor::new(v),
InputOrigin::Other,
));
}
OpenResult::Err(anyhow!(
"failed to download \"{}\"; please check your network connection.",
info.name
))
}
}