use crate::core::shell_escape::sh_squote;
use crate::core::types::{
NasArchive, Resource, DEFAULT_ARCHIVE_SCHEDULE, DEFAULT_MAX_SMALL_BYTES, DEFAULT_MIN_AGE_DAYS,
};
mod mover;
mod units;
#[cfg(test)]
mod tests;
pub use mover::archive_script;
pub fn archive_of(resource: &Resource) -> Result<NasArchive, String> {
let path = resource
.path
.as_deref()
.ok_or_else(|| "nas_archive requires `path` (the source root)".to_string())?;
NasArchive::new(
path,
resource.archive.destination.as_deref(),
resource.archive.dirs.clone(),
resource
.archive
.max_small_bytes
.unwrap_or(DEFAULT_MAX_SMALL_BYTES),
resource
.archive
.min_age_days
.unwrap_or(DEFAULT_MIN_AGE_DAYS),
resource.archive.leave_symlink.unwrap_or(true),
resource
.archive
.schedule
.as_deref()
.unwrap_or(DEFAULT_ARCHIVE_SCHEDULE),
)
}
pub(crate) fn slug(path: &str) -> String {
let s: String = path
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
let t = s.trim_matches('-').to_string();
if t.is_empty() {
"root".to_string()
} else {
t
}
}
pub(crate) fn script_path(path: &str) -> String {
format!("/usr/local/sbin/forjar-archive-{}.sh", slug(path))
}
pub(crate) fn service_name(path: &str) -> String {
format!("forjar-archive-{}", slug(path))
}
fn reject(msg: &str) -> String {
format!("echo {} >&2; exit 1", sh_squote(&format!("ERROR: {msg}")))
}
pub fn check_script(resource: &Resource) -> String {
let a = match archive_of(resource) {
Ok(a) => a,
Err(e) => return reject(&e),
};
let script = script_path(&a.path);
let unit = service_name(&a.path);
let mut assertions = vec![
crate::resources::verdict::assert_that(
&format!("test -x {}", sh_squote(&script)),
&format!("archive-script-present:{unit}"),
&format!("archive-script-missing:{unit}"),
),
crate::resources::verdict::assert_that(
&format!(
"systemctl is-enabled {} >/dev/null 2>&1",
sh_squote(&format!("{unit}.timer"))
),
&format!("archive-timer-enabled:{unit}"),
&format!("archive-timer-disabled:{unit}"),
),
crate::resources::verdict::assert_that(
&format!("test -d {}", sh_squote(&a.destination)),
&format!("archive-destination-present:{}", a.destination),
&format!("archive-destination-missing:{}", a.destination),
),
];
for d in &a.dirs {
let src = sh_squote(&a.source_of(d));
let dest = sh_squote(&a.dest_of(d));
assertions.push(crate::resources::verdict::assert_block(
&format!("test -L {src}"),
&format!(
"if\n [ \"$(readlink -f {src})\" = \"$(readlink -f {dest} 2>/dev/null)\" ] && [ -d {dest} ]\n \
then\n echo 'archived:{d}'\n else\n echo 'archive-broken:{d}'; __fj_diverged=1\n fi",
d = d
),
&format!("echo 'archive-pending:{d}'"),
));
}
crate::resources::verdict::check_script_from(&assertions)
}
pub fn apply_script(resource: &Resource) -> String {
let a = match archive_of(resource) {
Ok(a) => a,
Err(e) => return reject(&e),
};
let script = script_path(&a.path);
let unit = service_name(&a.path);
format!(
"set -e\n\
mkdir -p {script_dir}\n\
cat > {script_q} <<'FORJAR_ARCHIVE_EOF'\n{body}\nFORJAR_ARCHIVE_EOF\n\
chmod 0755 {script_q}\n\
cat > {svc} <<'FORJAR_SVC_EOF'\n{service}\nFORJAR_SVC_EOF\n\
cat > {tmr} <<'FORJAR_TMR_EOF'\n{timer}\nFORJAR_TMR_EOF\n\
systemctl daemon-reload\n\
systemctl enable --now {unit}.timer\n",
script_dir = sh_squote("/usr/local/sbin"),
script_q = sh_squote(&script),
body = archive_script(&a),
svc = sh_squote(&format!("/etc/systemd/system/{unit}.service")),
service = units::service_unit(&a, &script),
tmr = sh_squote(&format!("/etc/systemd/system/{unit}.timer")),
timer = units::timer_unit(&a),
unit = unit,
)
}
pub fn state_query_script(resource: &Resource) -> String {
let a = match archive_of(resource) {
Ok(a) => a,
Err(e) => return reject(&e),
};
let mut out = String::from("set -u\n");
for d in &a.dirs {
let src = sh_squote(&a.source_of(d));
out.push_str(&format!(
"if [ -L {src} ]; then echo 'archived {d}'; \
elif [ -d {src} ]; then echo \"pending {d} $(du -sh {src} 2>/dev/null | cut -f1)\"; \
else echo 'absent {d}'; fi\n",
src = src,
d = d
));
}
out
}