use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::errors::{Api2ConvertError, Result};
use crate::models::{Job, OutputFile};
use crate::transport::Transport;
const CHUNK: usize = 64 * 1024;
pub struct FileDownload {
transport: Arc<Transport>,
output: OutputFile,
download_password: Option<String>,
}
impl FileDownload {
pub(crate) fn new(
transport: Arc<Transport>,
output: OutputFile,
download_password: Option<String>,
) -> Self {
FileDownload {
transport,
output,
download_password,
}
}
pub fn url(&self) -> &str {
&self.output.uri
}
pub fn output(&self) -> &OutputFile {
&self.output
}
pub fn save(
&self,
path_or_dir: impl AsRef<Path>,
download_password: Option<&str>,
) -> Result<PathBuf> {
let pw = pick_password(download_password, &self.download_password);
let mut resp = self.transport.open_download(&self.output.uri, pw)?;
let target = resolve_target(path_or_dir.as_ref(), self.output.filename.as_deref());
stream_to_file(&mut resp.body, &target, self.transport.max_download_bytes())?;
Ok(target)
}
pub fn contents(&self, download_password: Option<&str>) -> Result<Vec<u8>> {
let pw = pick_password(download_password, &self.download_password);
let mut resp = self.transport.open_download(&self.output.uri, pw)?;
read_capped(&mut resp.body, self.transport.max_download_bytes())
}
}
pub struct ConversionResult {
transport: Arc<Transport>,
job: Job,
index: usize,
download_password: Option<String>,
}
impl ConversionResult {
pub(crate) fn new(
transport: Arc<Transport>,
job: Job,
index: usize,
download_password: Option<String>,
) -> Self {
ConversionResult {
transport,
job,
index,
download_password,
}
}
pub fn job(&self) -> &Job {
&self.job
}
pub fn outputs(&self) -> &[OutputFile] {
&self.job.output
}
pub fn output(&self) -> Result<&OutputFile> {
self.job.output.get(self.index).ok_or_else(|| {
Api2ConvertError::Config(format!(
"no output file at index {} (job produced {})",
self.index,
self.job.output.len()
))
})
}
pub fn url(&self) -> Result<String> {
Ok(self.output()?.uri.clone())
}
pub fn download(&self, output: Option<OutputFile>) -> Result<FileDownload> {
let out = match output {
Some(o) => o,
None => self.output()?.clone(),
};
Ok(FileDownload::new(
Arc::clone(&self.transport),
out,
self.download_password.clone(),
))
}
pub fn save(
&self,
path_or_dir: impl AsRef<Path>,
download_password: Option<&str>,
) -> Result<PathBuf> {
self.download(None)?.save(path_or_dir, download_password)
}
pub fn contents(&self, download_password: Option<&str>) -> Result<Vec<u8>> {
self.download(None)?.contents(download_password)
}
}
fn pick_password<'a>(explicit: Option<&'a str>, remembered: &'a Option<String>) -> Option<&'a str> {
explicit.or(remembered.as_deref())
}
fn resolve_target(path_or_dir: &Path, filename: Option<&str>) -> PathBuf {
let as_str = path_or_dir.to_string_lossy();
let looks_like_dir = path_or_dir.is_dir()
|| as_str.ends_with('/')
|| as_str.ends_with(std::path::MAIN_SEPARATOR);
if looks_like_dir {
path_or_dir.join(safe_name(filename.unwrap_or("download")))
} else {
path_or_dir.to_path_buf()
}
}
fn safe_name(name: &str) -> String {
let normalized = name.replace('\\', "/").replace('\0', "");
let base = Path::new(&normalized)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("");
if base.is_empty() || base == "." || base == ".." {
"download".to_string()
} else {
base.to_string()
}
}
fn stream_to_file(body: &mut Box<dyn Read + Send>, target: &Path, max_bytes: u64) -> Result<()> {
let mut file = fs::File::create(target).map_err(|e| {
Api2ConvertError::Network(format!("failed to create {}: {}", target.display(), e))
})?;
let mut buf = [0u8; CHUNK];
let mut total: u64 = 0;
loop {
let n = match body.read(&mut buf) {
Ok(0) => break,
Ok(n) => n,
Err(_) => {
let _ = fs::remove_file(target);
return Err(Api2ConvertError::Network(
"failed while reading the download".to_string(),
));
}
};
total += n as u64;
if max_bytes > 0 && total > max_bytes {
let _ = fs::remove_file(target);
return Err(Api2ConvertError::Network(format!(
"download exceeds the {max_bytes}-byte limit"
)));
}
if file.write_all(&buf[..n]).is_err() {
let _ = fs::remove_file(target);
return Err(Api2ConvertError::Network(format!(
"failed to write {}",
target.display()
)));
}
}
if file.flush().is_err() {
let _ = fs::remove_file(target);
return Err(Api2ConvertError::Network(format!(
"failed to finalize {}",
target.display()
)));
}
Ok(())
}
fn read_capped(body: &mut Box<dyn Read + Send>, max_bytes: u64) -> Result<Vec<u8>> {
let mut out = Vec::new();
let mut buf = [0u8; CHUNK];
loop {
let n = body.read(&mut buf).map_err(|_| {
Api2ConvertError::Network("failed while reading the download".to_string())
})?;
if n == 0 {
break;
}
if max_bytes > 0 && (out.len() as u64 + n as u64) > max_bytes {
return Err(Api2ConvertError::Network(format!(
"download exceeds the {max_bytes}-byte limit"
)));
}
out.extend_from_slice(&buf[..n]);
}
Ok(out)
}