use std::fs::File;
use std::io::{Read, Write};
use std::num::NonZeroU64;
use std::path::PathBuf;
use std::time::Duration;
use std::{fs, io};
use reqwest::blocking::Client;
use crate::hashed::HashedVec;
use crate::utils::{hex_str, sha256};
use crate::{
utils, DownloadRequest, Downloadable, DownloaderBuilder, Error, HashMismatch, InnerDownloadable,
};
#[derive(Debug)]
pub(crate) struct DowloadContext {
pub(crate) path: PathBuf,
}
#[derive(Debug)]
pub(crate) struct InnerDownloader {
pub(super) storage_dir: PathBuf,
pub(super) download_attempts: NonZeroU64,
pub(super) failed_download_wait_time: Duration,
pub(super) client: Client,
}
impl InnerDownloader {
pub(crate) fn compute_path(&self, r: &InnerDownloadable) -> io::Result<PathBuf> {
let mut target_path = self.storage_dir.clone();
target_path.push(hex_str(r.sha256()));
Ok(target_path)
}
pub(crate) fn get_path_with_optional_data(
&self,
mut ctx: DowloadContext,
r: &InnerDownloadable,
) -> Result<(PathBuf, Option<HashedVec>), Error> {
if ctx.path.exists() {
Ok((ctx.path, None))
} else {
let dat = self.get(&mut ctx, r)?;
Ok((ctx.path, Some(dat)))
}
}
pub(crate) fn make_context(&self, r: &InnerDownloadable) -> Result<DowloadContext, Error> {
let path = self.compute_path(r)?;
Ok(DowloadContext { path })
}
fn write_to_file_prechecked(
&self,
ctx: &mut DowloadContext,
r: &InnerDownloadable,
contents: &HashedVec,
) -> Result<(), io::Error> {
assert_eq!(contents.sha256(), r.sha256());
self.write_to_file_unchecked(ctx, r, contents.as_slice())
}
fn write_to_file_unchecked(
&self,
ctx: &DowloadContext,
r: &InnerDownloadable,
contents: &[u8],
) -> Result<(), io::Error> {
let (mut tmp_file, tmp_file_path) = utils::create_file_at(
ctx.path.parent().expect("target path is a file in a dir"),
&format!("download_{}", hex_str(r.sha256())),
)?;
tmp_file.write_all(contents)?;
fs::rename(tmp_file_path, &ctx.path)?;
Ok(())
}
fn write_to_file(
&self,
ctx: &DowloadContext,
r: &InnerDownloadable,
contents: &[u8],
) -> Result<(), Error> {
let real = sha256(contents);
if r.sha256() != real {
return Err(Error::ManualHashMismatch(HashMismatch {
expected: hex_str(r.sha256()),
was: hex_str(&real),
}));
}
Ok(self.write_to_file_unchecked(ctx, r, contents)?)
}
fn procure_and_write(
&self,
ctx: &mut DowloadContext,
r: &InnerDownloadable,
) -> Result<HashedVec, Error> {
let contents = r.procure(self, ctx)?;
self.write_to_file_prechecked(ctx, r, &contents)?;
debug_assert_eq!(r.sha256(), contents.sha256());
Ok(contents)
}
pub(crate) fn get(
&self,
ctx: &mut DowloadContext,
r: &InnerDownloadable,
) -> Result<HashedVec, Error> {
if ctx.path.exists() {
match self.get_cached(ctx, r) {
Err(Error::OnDiskHashMismatch { .. }) => self.procure_and_write(ctx, r),
e => e,
}
} else {
self.procure_and_write(ctx, r)
}
}
pub(crate) fn get_cached(
&self,
ctx: &DowloadContext,
r: &InnerDownloadable,
) -> Result<HashedVec, Error> {
let mut res = vec![];
let mut file = File::open(&ctx.path)?;
file.read_to_end(&mut res)?;
match HashedVec::try_new(res, r.sha256()) {
Ok(vec) => {
debug_assert_eq!(r.sha256(), vec.sha256());
Ok(vec)
}
Err(err) => Err(Error::OnDiskHashMismatch(HashMismatch {
expected: hex_str(r.sha256()),
was: hex_str(&err.got),
})),
}
}
}
#[derive(Debug)]
pub struct Downloader {
pub(crate) inner: InnerDownloader,
}
impl Downloader {
pub fn new() -> Result<Self, Error> {
Self::builder().build()
}
pub fn builder() -> DownloaderBuilder {
DownloaderBuilder::new()
}
pub fn get<'a>(&self, r: impl Into<Downloadable<'a>>) -> Result<Vec<u8>, Error> {
let r = r.into().0;
let mut ctx = self.inner.make_context(&r)?;
Ok(self.inner.get(&mut ctx, &r)?.into_vec())
}
pub fn get_cached<'a>(&self, r: impl Into<Downloadable<'a>>) -> Result<Vec<u8>, Error> {
let r = &r.into().0;
let ctx = self.inner.make_context(r)?;
Ok(self.inner.get_cached(&ctx, r)?.into_vec())
}
pub fn get_cached_by_hash(&self, hash: &[u8]) -> Result<Vec<u8>, Error> {
let dr = DownloadRequest {
url: "", sha256_hash: hash,
};
self.get_cached(&dr)
}
pub fn set<'a>(&self, r: impl Into<Downloadable<'a>>, data: &[u8]) -> Result<(), Error> {
let r = &r.into().0;
let ctx = self.inner.make_context(r)?;
self.inner.write_to_file(&ctx, r, data)
}
pub fn set_by_hash(&self, hash: &[u8], data: &[u8]) -> Result<(), Error> {
let dr = DownloadRequest {
url: "", sha256_hash: hash,
};
self.set(&dr, data)
}
pub fn insert(&self, data: &[u8]) -> Result<[u8; 32], Error> {
let hash = utils::sha256(data);
self.set_by_hash(&hash, data)?;
Ok(hash)
}
pub fn get_path<'a>(&self, r: impl Into<Downloadable<'a>>) -> Result<PathBuf, Error> {
let r = &r.into().0;
let ctx = self.inner.make_context(r)?;
let (path, _) = self.inner.get_path_with_optional_data(ctx, r)?;
Ok(path)
}
}