use super::{hash, store};
use crate::corpus::Corpus;
use crate::model::Doc;
use anyhow::{Context, Result, bail};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct Fetched {
pub path: String,
pub state: String,
}
pub fn fetch(c: &Corpus, doc: &Doc, force: bool) -> Result<Vec<Fetched>> {
let entries: Vec<_> = doc.files().into_iter().filter(|f| f.store == "store").collect();
if entries.is_empty() {
return Ok(vec![]);
}
let s = store::open(&c.cfg)?.context("no [files.store] is configured")?;
let base = c.cfg.root.join(doc.base_dir());
if !force {
let other: Vec<String> = entries
.iter()
.filter(|f| {
let dst = base.join(&f.path);
dst.is_file() && !hash::file(&dst).map(|h| h.0 == f.sha256).unwrap_or(false)
})
.map(|f| f.path.clone())
.collect();
if !other.is_empty() {
bail!(
"{} already exist(s) with other content; nothing was fetched. Move it aside, or run spec fetch {} --force to overwrite",
other.join(", "),
doc.id
);
}
}
let mut out = vec![];
for f in entries {
let dst = base.join(&f.path);
if dst.is_file() && hash::file(&dst).map(|h| h.0 == f.sha256).unwrap_or(false) {
out.push(Fetched { path: f.path, state: "present".into() });
continue;
}
let name = dst.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let part = dst.with_file_name(format!(".{name}.specfold-part"));
if let Err(e) = s.get(&f.sha256, &part) {
let _ = std::fs::remove_file(&part);
return Err(e);
}
let (got, _) = hash::file(&part)?;
if got != f.sha256 {
let _ = std::fs::remove_file(&part);
return Err(anyhow::Error::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("{} arrived with sha256 {got}, expected {}; nothing was written", f.path, f.sha256),
)));
}
std::fs::rename(&part, &dst)?;
out.push(Fetched { path: f.path, state: "fetched".into() });
}
Ok(out)
}