use std::io::Write;
use anyhow::{Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use doiget_core::paper_tex_source::{
paper_source_bundle, resolve_arxiv_src_base, BundleFilter, SourceFile,
};
use doiget_core::{ArxivId, ErrorCode, Ref};
use super::fetch::{build_resolve_context, cli_exit_code, CliExit};
use super::output::print_err;
use super::output::OutputMode;
pub async fn run(
ref_: String,
out_dir: Utf8PathBuf,
figures_only: bool,
mode: OutputMode,
quiet_was_explicit: bool,
) -> Result<()> {
let parsed = super::parse_ref_or_exit(&ref_)?;
let id: ArxivId = match parsed {
Ref::Arxiv(a) => a,
Ref::Doi(_) => {
let code = ErrorCode::NoOaAvailable;
print_err(format_args!(
"error[{}]: no source bundle for a bare DOI — if an arXiv preprint exists, \
pass its id (e.g. `doiget source arxiv:2401.12345 --out ./src`)",
code.as_wire()
));
return Err(anyhow::Error::new(CliExit(cli_exit_code(code))));
}
};
let base = resolve_arxiv_src_base().map_err(|e| anyhow::anyhow!("{e}"))?;
let ctx = build_resolve_context().context("building fetch context")?;
let filter = if figures_only {
BundleFilter::FiguresOnly
} else {
BundleFilter::All
};
let files = match paper_source_bundle(&base, &id, filter, &ctx).await {
Ok(f) => f,
Err(e) => {
let code = ErrorCode::from(&e);
print_err(format_args!("error[{}]: {e}", code.as_wire()));
if code == ErrorCode::TextUnavailable {
print_err(format_args!(
" = note: no {} found (no matching files, PDF-only, or single-file \
submission). Fetch the PDF instead: `doiget fetch arxiv:{}`",
if figures_only {
"figures"
} else {
"source bundle"
},
id.as_str()
));
}
return Err(anyhow::Error::new(CliExit(cli_exit_code(code))));
}
};
let written = write_files(&out_dir, &files)?;
if mode == OutputMode::Quiet && quiet_was_explicit {
return Ok(());
}
let stdout = std::io::stdout();
let mut out = stdout.lock();
if mode == OutputMode::Json {
let payload = serde_json::json!({
"ok": true,
"arxiv_id": id.as_str(),
"out_dir": out_dir.as_str(),
"figures_only": figures_only,
"count": written.len(),
"files": written.iter().map(|p| p.as_str()).collect::<Vec<_>>(),
});
let s = serde_json::to_string_pretty(&payload).context("serializing source JSON")?;
writeln!(out, "{s}").context("writing source JSON to stdout")?;
return Ok(());
}
writeln!(out, "wrote {} file(s) to {out_dir}", written.len())
.context("writing source summary")?;
for rel in &written {
writeln!(out, " {rel}").context("writing source file line")?;
}
Ok(())
}
fn write_files(out_dir: &Utf8Path, files: &[SourceFile]) -> Result<Vec<Utf8PathBuf>> {
std::fs::create_dir_all(out_dir.as_std_path())
.with_context(|| format!("creating output dir {out_dir}"))?;
let mut written: Vec<Utf8PathBuf> = Vec::with_capacity(files.len());
for f in files {
let rel = f.path();
let dest = out_dir.join(rel);
if !dest.starts_with(out_dir) {
anyhow::bail!("refusing to write outside the output dir (zip-slip guard): {rel}");
}
if let Some(parent) = dest.parent() {
if parent != out_dir {
std::fs::create_dir_all(parent.as_std_path())
.with_context(|| format!("creating {parent}"))?;
}
}
std::fs::write(dest.as_std_path(), &f.bytes).with_context(|| format!("writing {dest}"))?;
written.push(rel.to_owned());
}
written.sort();
Ok(written)
}