use crate::io::{archive, file_checksum, files_all, read_file, write_file, ApiResult, StringConversion};
use crate::prelude::{copy, create_dir_all, PathBuf};
use crate::util::{ChecksumAlgorithm, Label};
use bon::Builder;
use color_eyre::eyre::eyre;
use core::fmt;
use derive_more::Display;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::{error, warn};
const LINE_ENDING: &str = if cfg!(windows) { "\r\n" } else { "\n" };
pub trait Save {
fn save<P>(&self, destination: P) -> ApiResult<PathBuf>
where
P: Into<PathBuf> + Clone;
}
#[derive(Builder, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[builder(start_fn = init)]
pub struct Bag {
pub base_directory: String,
#[builder(default = Vec::new())]
pub payload: Vec<String>,
#[builder(default)]
pub checksum_algorithm: ChecksumAlgorithm,
#[builder(default = String::from("1.0"))]
pub version: String,
#[builder(default = String::from("UTF-8"))]
pub encoding: String,
pub info: Option<BagInfo>,
}
#[derive(Builder, Clone, Debug, Display, Serialize, Deserialize, JsonSchema)]
#[display("Bag-Info")]
#[builder(start_fn = init)]
pub struct BagInfo {
pub organization: Option<Vec<String>>,
pub organization_address: Option<Vec<String>>,
pub contact_name: Option<Vec<String>>,
pub contact_phone: Option<Vec<String>>,
pub contact_email: Option<Vec<String>>,
pub description: Option<Vec<String>>,
pub date: Option<String>,
pub identifier: Option<Vec<String>>,
pub size: Option<String>,
pub count: Option<Vec<(u32, Option<u32>)>>,
}
impl Bag {
pub fn verify<P>(path: P) -> ApiResult<()>
where
P: Into<PathBuf>,
{
let base_directory = path.into();
let files = files_all(base_directory.clone(), None)
.into_iter()
.filter(|x| x.is_file())
.collect::<Vec<_>>();
match files
.iter()
.flat_map(|x| x.file_name())
.flat_map(|x| x.to_str())
.find(|x| x.starts_with("manifest"))
{
| Some(name) => {
let algorithm = name["manifest.".len()..]
.split(".")
.collect::<Vec<_>>()
.first()
.unwrap_or(&"")
.to_lowercase();
let manifest = format!("manifest-{}.txt", algorithm.clone());
let checksum_algorithm: &'static ring::digest::Algorithm = ChecksumAlgorithm::from(algorithm).into();
let required = ["bagit.txt", &manifest, "data/"]
.iter()
.map(|x| base_directory.join(x))
.collect::<Vec<_>>();
if required.iter().all(|x| x.exists()) {
let manifest_path = base_directory.join(manifest);
let content = read_file(manifest_path.clone()).unwrap_or_default();
let pairs = content
.lines()
.map(|line| line.split_whitespace().collect::<Vec<_>>())
.collect::<Vec<_>>();
let compare_checksums = pairs
.iter()
.map(|x| {
let checksum = x.first().copied().unwrap_or("");
let payload_path = base_directory.join(x.get(1).copied().unwrap_or(""));
let calculated = file_checksum(payload_path, Some(checksum_algorithm))
.map(|checksum| checksum.checksum_value)
.unwrap_or_default();
calculated.eq(checksum)
})
.collect::<Vec<_>>();
match compare_checksums.iter().enumerate().find(|(_, result)| !*result) {
| Some((index, _)) => {
let path = pairs.get(index).and_then(|p| p.get(1)).copied().unwrap_or("");
Err(eyre!("Checksum mismatch in payload file = {}", path))
}
| None => Ok(()),
}
} else {
Err(eyre!("Missing required file(s) and/or folder(s)"))
}
}
| None => Err(eyre!("No manifest file found")),
}
}
pub fn with_payload(&self) -> Self {
let base_directory = PathBuf::from(self.base_directory.clone());
let payload = files_all(base_directory.clone(), None)
.into_iter()
.filter(|x| x.is_file())
.flat_map(|x| x.strip_prefix(base_directory.to_absolute_string()).ok().map(|p| p.to_path_buf()))
.map(|x| x.display().to_string())
.collect::<Vec<_>>();
let Bag {
checksum_algorithm,
version,
encoding,
info,
..
} = self;
Bag::init()
.base_directory(self.base_directory.clone())
.checksum_algorithm(checksum_algorithm.clone())
.version(version.clone())
.encoding(encoding.clone())
.maybe_info(info.clone())
.payload(payload)
.build()
}
}
impl Save for Bag {
fn save<P>(&self, destination: P) -> ApiResult<PathBuf>
where
P: Into<PathBuf> + Clone,
{
let Bag { info, version, encoding, .. } = self;
let dest = destination.clone().into();
let payload_directory = dest.join("data");
match create_dir_all(payload_directory.clone()) {
| Ok(_) => {
let base = PathBuf::from(self.base_directory.clone());
let bag = self.with_payload();
let checksum_algorithm: &'static ring::digest::Algorithm = self.checksum_algorithm.clone().into();
bag.payload.clone().into_iter().for_each(|x| {
let payload = PathBuf::from(x);
let from = base.join(&payload);
let to = dest.join("data").join(&payload);
if let Some(parent) = to.parent() {
if let Err(why) = create_dir_all(parent) {
error!(
directory = parent.to_path_buf().to_absolute_string(),
"=> {} Create - {why}",
Label::fail()
);
return;
}
if let Err(why) = copy(from.clone(), to.clone()) {
error!(
from = from.to_absolute_string(),
to = to.to_absolute_string(),
"=> {} Copy - {why}",
Label::fail()
);
}
}
});
let bag_declaration_content = format!("BagIt-Version: {version}\nTag-File-Character-Encoding: {encoding}\n");
let payload_manifest_content = bag
.payload
.into_iter()
.fold(String::new(), |acc, x| {
let payload = PathBuf::from(x.clone());
let to = dest.join("data").join(&payload);
match file_checksum(to.clone(), Some(checksum_algorithm)) {
| Some(checksum) => {
format!("{acc}{checksum} data/{x}{LINE_ENDING}")
}
| None => {
warn!(payload = payload.to_absolute_string(), "=> {} Calculate checksum", Label::fail());
acc
}
}
})
.replace("\\", "/");
write_file(dest.clone().join("bagit.txt"), bag_declaration_content)
.map(|_| dest.clone())
.and_then(|_| match info.clone() {
| Some(bag_info) => bag_info.save(destination.clone()),
| None => Ok(dest.clone()),
})
.and_then(|_| {
let file_name = format!("manifest-{}.txt", self.checksum_algorithm);
let file_path = dest.clone().join(&file_name);
write_file(file_path, payload_manifest_content).map(|_| dest.clone())
})
.and_then(|_| archive(dest.clone(), None))
}
| Err(why) => Err(eyre!("Failed to create bag - {why}")),
}
}
}
impl fmt::Display for Bag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Bag v{} ({}): {}", self.version, self.checksum_algorithm, self.base_directory)
}
}
impl BagInfo {
pub fn entries(&self) -> Vec<(String, String)> {
let BagInfo {
organization,
organization_address,
contact_name,
contact_phone,
contact_email,
description,
date,
identifier,
size,
count,
} = self;
let repeatable = [
("Source-Organization", organization),
("Organization-Address", organization_address),
("Contact-Name", contact_name),
("Contact-Phone", contact_phone),
("Contact-Email", contact_email),
("External-Description", description),
("External-Identifier", identifier),
]
.into_iter()
.flat_map(|(key, values)| {
values
.iter()
.flat_map(|items| items.iter())
.map(move |value| (key.to_string(), value.clone()))
});
let count = count.iter().flat_map(|items| items.iter()).map(|(index, total)| {
let total = total.map_or_else(|| "?".to_string(), |value| value.to_string());
("Bag-Count".to_string(), format!("{index} of {total}"))
});
let single = [("Bagging-Date", date.clone()), ("Bag-Size", size.clone())]
.into_iter()
.filter_map(|(key, value)| value.map(|v| (key.to_string(), v)));
repeatable.chain(count).chain(single).collect()
}
}
impl Default for BagInfo {
fn default() -> Self {
BagInfo::init().build()
}
}
impl Save for BagInfo {
fn save<P>(&self, destination: P) -> ApiResult<PathBuf>
where
P: Into<PathBuf> + Clone,
{
let dest = destination.clone().into();
let content = self
.entries()
.into_iter()
.fold(String::new(), |acc, (key, value)| format!("{acc}{key}: {value}{LINE_ENDING}"));
write_file(dest.join("bag-info.txt"), content).map(|_| dest)
}
}
impl From<ChecksumAlgorithm> for &'static ring::digest::Algorithm {
fn from(value: ChecksumAlgorithm) -> Self {
match value {
| ChecksumAlgorithm::Sha256 => &ring::digest::SHA256,
| ChecksumAlgorithm::Sha512 => &ring::digest::SHA512,
| _ => &ring::digest::SHA256,
}
}
}