use super::convert::Converter;
use super::error::DataError;
use super::manifest::{Entry, Manifest};
use super::render::{bare_rows, read_back, table_text};
use molframe_adapters::{DownloadOptions, fetch_verified};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Status {
Missing,
Current,
Stale(String),
Invalid(String),
}
impl Status {
#[must_use]
pub const fn word(&self) -> &'static str {
match self {
Self::Missing => "missing",
Self::Current => "current",
Self::Stale(_) => "stale",
Self::Invalid(_) => "invalid",
}
}
#[must_use]
pub fn detail(&self) -> Option<&str> {
match self {
Self::Stale(why) | Self::Invalid(why) => Some(why),
Self::Missing | Self::Current => None,
}
}
}
impl std::fmt::Display for Status {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.detail() {
Some(why) => write!(formatter, "{}: {why}", self.word()),
None => formatter.write_str(self.word()),
}
}
}
#[derive(Clone, Debug)]
pub struct Verified {
pub entry: Entry,
pub path: PathBuf,
pub status: Status,
}
#[derive(Clone, Debug)]
pub struct Fetched {
pub path: PathBuf,
pub bytes: usize,
pub rows: usize,
}
#[must_use]
pub fn list(manifest: &Manifest, directory: &Path) -> Vec<Verified> {
manifest
.entries()
.iter()
.map(|entry| check(entry, directory))
.collect()
}
pub fn verify(
manifest: &Manifest,
directory: &Path,
name: Option<&str>,
) -> Result<Vec<Verified>, DataError> {
let chosen: Vec<&Entry> = match name {
Some(name) => vec![manifest.entry(name)?],
None => manifest.entries().iter().collect(),
};
Ok(chosen
.into_iter()
.map(|entry| check(entry, directory))
.collect())
}
fn check(entry: &Entry, directory: &Path) -> Verified {
let path = directory.join(entry.file_name());
let status = status_of(entry, &path);
Verified {
entry: entry.clone(),
path,
status,
}
}
fn status_of(entry: &Entry, path: &Path) -> Status {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Status::Missing,
Err(error) => return Status::Invalid(error.to_string()),
};
let stored = match read_back(&text) {
Ok(stored) => stored,
Err(error) => return Status::Invalid(error.to_string()),
};
if stored.rows == 0 {
return Status::Invalid("the table has no rows".to_owned());
}
let wanted = entry.sha256.to_ascii_lowercase();
let differs = [
("name", stored.name.as_str(), entry.name.as_str()),
("version", stored.version.as_str(), entry.version.as_str()),
("licence", stored.licence.as_str(), entry.licence.as_str()),
]
.into_iter()
.find(|(_, have, want)| have != want);
if let Some((key, have, want)) = differs {
return Status::Stale(format!("{key} is `{have}`, the manifest says `{want}`"));
}
match stored.digest {
Some(digest) if digest != wanted => Status::Stale(
"it was made from different source bytes than the manifest pins".to_owned(),
),
_ => Status::Current,
}
}
pub fn fetch(
manifest: &Manifest,
name: &str,
directory: &Path,
replace: bool,
options: DownloadOptions,
) -> Result<Fetched, DataError> {
let entry = manifest.entry(name)?;
let convert = |message: String| DataError::Convert {
name: entry.name.clone(),
message,
};
let converter = match &entry.converter {
Some(id) => Some(Converter::named(id).map_err(&convert)?),
None => None,
};
let target = directory.join(entry.file_name());
if target.exists() && !replace {
return Err(DataError::Exists { path: target });
}
let mut appended = Vec::new();
for relative in &entry.append {
let path = manifest.base().join(relative);
let text = std::fs::read_to_string(&path).map_err(|source| DataError::Read {
path: path.clone(),
source,
})?;
appended.extend(bare_rows(&text));
}
let download = fetch_verified(&entry.url, &entry.sha256, options).map_err(|source| {
DataError::Download {
name: entry.name.clone(),
source,
}
})?;
let text = String::from_utf8(download.bytes)
.map_err(|error| convert(format!("the download is not UTF-8 text: {error}")))?;
let stored = match converter {
Some(converter) => {
let mut rows = converter.rows(&text).map_err(&convert)?;
rows.extend(appended);
table_text(entry, &rows)
}
None if appended.is_empty() => text,
None => return Err(convert("`append` needs a converter".to_owned())),
};
let check = read_back(&stored).map_err(|error| convert(error.to_string()))?;
if check.rows == 0 || check.name != entry.name {
return Err(convert(format!(
"the stored table must declare the name `{}` and have rows",
entry.name
)));
}
write_atomic(&target, stored.as_bytes())?;
Ok(Fetched {
path: target,
bytes: stored.len(),
rows: check.rows,
})
}
fn write_atomic(target: &Path, bytes: &[u8]) -> Result<(), DataError> {
let fail = |source| DataError::Write {
path: target.to_owned(),
source,
};
if let Some(parent) = target
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
std::fs::create_dir_all(parent).map_err(fail)?;
}
let mut partial = target.as_os_str().to_owned();
partial.push(".part");
let partial = PathBuf::from(partial);
std::fs::write(&partial, bytes).map_err(fail)?;
std::fs::rename(&partial, target).map_err(fail)
}
#[cfg(test)]
#[path = "store_tests.rs"]
mod tests;