use std::collections::BTreeSet;
use std::path::{Component, Path, PathBuf};
use aion_package::AwlSource;
use sha2::{Digest, Sha256};
use crate::filesystem::ConfinedDir;
use crate::worker::lowercase_hex;
const DOCUMENTS_ROOT: &str = "workers/documents";
#[derive(Debug, thiserror::Error)]
pub enum DocumentStoreError {
#[error(
"the Aion home could not be resolved, so the deployed document has nowhere to live: {message}"
)]
HomeUnresolved {
message: String,
},
#[error(
"the deployed package names `{name}` inside its archived AWL tree, which is not a \
relative path of ordinary components; nothing was written"
)]
UnsafeName {
name: String,
},
#[error("the deployed document could not be written under `{path}`: {message}")]
Write {
path: String,
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StagedDocument {
pub path: PathBuf,
pub digest: String,
}
pub fn stage(
root: &Path,
source: &AwlSource,
workflow_type: &str,
) -> Result<StagedDocument, DocumentStoreError> {
let digest = lowercase_hex(&Sha256::digest(source.document().as_bytes()));
let version = version_directory(workflow_type, &digest);
let absolute = root.join(&version);
let staging = root.join(format!("{version}.partial"));
remove_tree(&staging)?;
let dir =
ConfinedDir::open_or_create(&staging).map_err(|error| write_failure(&staging, &error))?;
let document_name = safe_component(source.document_name())?;
dir.atomic_write(&document_name, source.document().as_bytes())
.map_err(|error| write_failure(&staging.join(&document_name), &error))?;
for (schema_path, bytes) in source.schemas() {
let relative = safe_relative(schema_path)?;
if let Some(parent) = relative
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
dir.create_dir_all(parent)
.map_err(|error| write_failure(&staging.join(parent), &error))?;
}
dir.atomic_write(&relative, bytes)
.map_err(|error| write_failure(&staging.join(&relative), &error))?;
}
drop(dir);
if absolute.exists() {
remove_tree(&staging)?;
} else {
std::fs::rename(&staging, &absolute).map_err(|error| write_failure(&absolute, &error))?;
}
Ok(StagedDocument {
path: absolute.join(document_name),
digest,
})
}
pub fn root() -> Result<PathBuf, DocumentStoreError> {
crate::config::aion_home()
.map(|home| home.path.join(DOCUMENTS_ROOT))
.map_err(|error| DocumentStoreError::HomeUnresolved {
message: error.to_string(),
})
}
pub fn prune(root: &Path, live: &BTreeSet<PathBuf>) -> Result<Vec<PathBuf>, DocumentStoreError> {
if !root.exists() {
return Ok(Vec::new());
}
let entries = std::fs::read_dir(root).map_err(|error| write_failure(root, &error))?;
let mut removed = Vec::new();
for entry in entries {
let entry = entry.map_err(|error| write_failure(root, &error))?;
let path = entry.path();
if !path.is_dir() {
continue;
}
if live.iter().any(|document| document.starts_with(&path)) {
continue;
}
remove_tree(&path)?;
removed.push(path);
}
Ok(removed)
}
fn remove_tree(path: &Path) -> Result<(), DocumentStoreError> {
match std::fs::remove_dir_all(path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(write_failure(path, &error)),
}
}
fn version_directory(workflow_type: &str, digest: &str) -> String {
let sanitised: String = workflow_type
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() || character == '_' || character == '-' {
character
} else {
'_'
}
})
.collect();
let sanitised = if sanitised.is_empty() {
"workflow".to_owned()
} else {
sanitised
};
format!("{sanitised}@{digest}")
}
fn safe_component(name: &str) -> Result<PathBuf, DocumentStoreError> {
let path = Path::new(name);
let mut components = path.components();
match (components.next(), components.next()) {
(Some(Component::Normal(single)), None) => Ok(PathBuf::from(single)),
_ => Err(DocumentStoreError::UnsafeName {
name: name.to_owned(),
}),
}
}
fn safe_relative(name: &str) -> Result<PathBuf, DocumentStoreError> {
let path = Path::new(name);
let mut safe = PathBuf::new();
let mut any = false;
for component in path.components() {
match component {
Component::Normal(part) => {
safe.push(part);
any = true;
}
_ => {
return Err(DocumentStoreError::UnsafeName {
name: name.to_owned(),
});
}
}
}
if any {
Ok(safe)
} else {
Err(DocumentStoreError::UnsafeName {
name: name.to_owned(),
})
}
}
fn write_failure(path: &Path, error: &std::io::Error) -> DocumentStoreError {
DocumentStoreError::Write {
path: path.display().to_string(),
message: error.to_string(),
}
}
#[cfg(test)]
#[path = "documents_tests.rs"]
mod tests;