use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::runtime::{LoadedGraft, graft_record_root, load_graft_record, load_graft_records};
use crate::{GraftPlanDocument, NodeId, Registry};
use nichlink::lexicon;
use super::super::filesystem::atomic_write;
use super::super::validation::package_root;
pub fn external_graft_root() -> PathBuf {
graft_record_root(&package_root())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExternalGraftPlanFile {
pub selector: String,
pub document: GraftPlanDocument,
pub root: PathBuf,
}
impl ExternalGraftPlanFile {
pub fn plan_path(&self) -> PathBuf {
self.root.join(lexicon::GRAFT_PLAN_FILE)
}
pub fn target(&self) -> NodeId {
self.document.target
}
pub fn target_path(&self) -> &str {
&self.document.target_path
}
pub fn graft(&self) -> &str {
&self.document.graft
}
pub fn full(&self) -> bool {
self.document.full
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExternalGraftPlanEntry {
pub selector: String,
pub root: PathBuf,
pub document: Result<GraftPlanDocument, String>,
}
impl ExternalGraftPlanEntry {
pub fn plan_path(&self) -> PathBuf {
self.root.join(lexicon::GRAFT_PLAN_FILE)
}
pub fn document(&self) -> Result<&GraftPlanDocument, &str> {
self.document.as_ref().map_err(String::as_str)
}
}
fn checked_selector(selector: &str) -> Result<&str, String> {
let selector = selector.trim();
crate::validate_graft_selector(selector)?;
Ok(selector)
}
pub fn external_graft_directory(selector: &str) -> Result<PathBuf, String> {
let selector = checked_selector(selector)?;
let root = external_graft_root().join(selector);
if !root.is_dir() {
return Err(format!(
"external graft `{selector}` does not exist at {}",
root.display()
));
}
Ok(root)
}
pub fn create_external_graft(
registry: &Registry,
target: NodeId,
graft: impl Into<String>,
full: bool,
) -> Result<ExternalGraftPlanFile, String> {
let target_path = registry
.path_for(target)
.ok_or_else(|| format!("graft target `{target}` is not registered"))?;
let selector = checked_selector(&graft.into())?.to_owned();
let root = external_graft_root().join(&selector);
if root.exists() {
return Err(format!(
"external graft `{selector}` already exists at {}",
root.join(lexicon::GRAFT_PLAN_FILE).display()
));
}
let document = GraftPlanDocument::new(target, target_path, selector.clone(), full);
fs::create_dir_all(&root)
.map_err(|error| format!("cannot create external graft directory: {error}"))?;
if let Err(error) = atomic_write(&root.join(lexicon::GRAFT_PLAN_FILE), &document.render()) {
let _ = fs::remove_dir_all(&root);
return Err(format!("cannot write external graft plan: {error}"));
}
Ok(ExternalGraftPlanFile {
selector,
document,
root,
})
}
pub fn read_external_graft(selector: &str) -> Result<ExternalGraftPlanFile, String> {
let selector = checked_selector(selector)?;
let document = load_graft_record(&package_root(), selector)?;
Ok(ExternalGraftPlanFile {
selector: selector.to_owned(),
document,
root: external_graft_root().join(selector),
})
}
pub fn list_external_grafts() -> Result<Vec<ExternalGraftPlanEntry>, String> {
let root = external_graft_root();
let loaded = load_graft_records(&package_root())?;
Ok(loaded
.into_iter()
.map(|entry| match entry {
LoadedGraft::Record(record) => ExternalGraftPlanEntry {
root: root.join(&record.selector),
selector: record.selector.clone(),
document: Ok(record.document),
},
LoadedGraft::Unreadable { selector, reason } => ExternalGraftPlanEntry {
root: root.join(&selector),
selector,
document: Err(reason),
},
})
.collect())
}
pub fn rewrite_external_graft(selector: &str, full: bool) -> Result<ExternalGraftPlanFile, String> {
let mut plan = read_external_graft(selector)?;
if plan.document.full == full {
return Ok(plan);
}
plan.document.full = full;
let path = plan.plan_path();
atomic_write(&path, &plan.document.render())?;
Ok(plan)
}
pub fn remove_external_graft(selector: &str) -> Result<PathBuf, String> {
let selector = checked_selector(selector)?;
let root = external_graft_directory(selector)?;
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| format!("clock error: {error}"))?
.as_nanos();
let trash = package_root()
.join(lexicon::NICHLINK_DIR)
.join("trash")
.join(lexicon::EXTERNAL_GRAFT_DIR)
.join(format!("{selector}-{stamp}"));
fs::create_dir_all(trash.parent().expect("trash has a parent"))
.map_err(|error| format!("cannot create NichLink trash: {error}"))?;
fs::rename(&root, &trash).map_err(|error| {
format!(
"cannot move {} to {}: {error}",
root.display(),
trash.display()
)
})?;
Ok(trash)
}
#[cfg(test)]
#[path = "plan_tests.rs"]
mod plan_tests;