use std::path::Path;
use super::bundle::{is_tracked_rel_path, Bundle, BUNDLE_VERSION};
enum Action {
Create,
Overwrite,
Skip,
}
impl Action {
const fn verb(&self) -> &'static str {
match self {
Self::Create => "create",
Self::Overwrite => "overwrite",
Self::Skip => "skip (exists)",
}
}
}
pub(crate) fn run_import(file: &Path, dry_run: bool, force: bool) -> i32 {
let bundle = match read_bundle(file) {
Ok(bundle) => bundle,
Err(message) => {
eprintln!("import failed: {message}");
return 2;
}
};
let root = crate::paths::config_dir();
let plan = plan_actions(&root, &bundle, force);
if dry_run {
for (rel, _content, action) in &plan {
println!("{} {rel}", action.verb());
}
println!("dry run: nothing written");
return 0;
}
apply(&root, &plan)
}
fn read_bundle(file: &Path) -> Result<Bundle, String> {
let raw = std::fs::read_to_string(file)
.map_err(|err| format!("cannot read {}: {err}", file.display()))?;
let bundle: Bundle = serde_json::from_str(&raw)
.map_err(|err| format!("{} is not a moadim export bundle: {err}", file.display()))?;
if bundle.version != BUNDLE_VERSION {
return Err(format!(
"unsupported bundle version {} (this moadim understands version {BUNDLE_VERSION})",
bundle.version
));
}
for (rel, content) in &bundle.files {
validate_entry(rel, content)?;
}
Ok(bundle)
}
fn validate_entry(rel: &str, content: &str) -> Result<(), String> {
if !is_tracked_rel_path(Path::new(rel)) {
return Err(format!("bundle entry {rel} is not a tracked config path"));
}
if rel.ends_with("routine.toml") {
toml::from_str::<toml::Value>(content)
.map_err(|err| format!("bundle entry {rel} is not valid TOML: {err}"))?;
}
Ok(())
}
fn plan_actions<'bundle>(
root: &Path,
bundle: &'bundle Bundle,
force: bool,
) -> Vec<(&'bundle str, &'bundle str, Action)> {
bundle
.files
.iter()
.map(|(rel, content)| {
let action = if root.join(rel).exists() {
if force {
Action::Overwrite
} else {
Action::Skip
}
} else {
Action::Create
};
(rel.as_str(), content.as_str(), action)
})
.collect()
}
fn apply(root: &Path, plan: &[(&str, &str, Action)]) -> i32 {
let mut written = 0_usize;
let mut skipped = 0_usize;
for (rel, content, action) in plan {
if matches!(action, Action::Skip) {
println!("{} {rel}", action.verb());
skipped += 1;
continue;
}
if let Err(err) = write_file(&root.join(rel), content) {
eprintln!("import failed: cannot write {rel}: {err}");
return 1;
}
println!("{} {rel}", action.verb());
written += 1;
}
println!(
"imported {written} file(s), skipped {skipped} — run `moadim restart` to resync the crontab"
);
0
}
fn write_file(target: &Path, content: &str) -> std::io::Result<()> {
target.parent().map_or(Ok(()), std::fs::create_dir_all)?;
crate::utils::atomic::atomic_write(target, content.as_bytes())
}
#[cfg(test)]
#[path = "import_tests.rs"]
mod import_tests;