use sha2::{Digest, Sha256};
use std::io::Write;
use std::path::Path;
use super::compose::{compose_summary_md, split_front_matter, SummaryComposeInput};
use super::paths::{summary_rel_path_with_layout, SummaryDiskLayout};
pub fn write_if_new(abs_path: &Path, bytes: &[u8]) -> anyhow::Result<bool> {
if abs_path.exists() {
return Ok(false);
}
let parent = abs_path.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(parent)
.map_err(|e| anyhow::anyhow!("create_dir_all {:?}: {e}", parent))?;
let tmp_name = format!(".tmp_{}.md", uuid_v4_hex());
let tmp_path = parent.join(&tmp_name);
{
let mut f = std::fs::File::create(&tmp_path)
.map_err(|e| anyhow::anyhow!("create tempfile {:?}: {e}", tmp_path))?;
f.write_all(bytes)
.map_err(|e| anyhow::anyhow!("write tempfile {:?}: {e}", tmp_path))?;
f.sync_all()
.map_err(|e| anyhow::anyhow!("fsync tempfile {:?}: {e}", tmp_path))?;
}
match std::fs::rename(&tmp_path, abs_path) {
Ok(()) => {
#[cfg(unix)]
if let Some(parent) = abs_path.parent() {
if let Ok(dir) = std::fs::File::open(parent) {
let _ = dir.sync_all();
}
}
Ok(true)
}
Err(e) => {
let _ = std::fs::remove_file(&tmp_path);
if abs_path.exists() {
Ok(false)
} else {
Err(anyhow::anyhow!(
"rename {:?} -> {:?}: {e}",
tmp_path,
abs_path
))
}
}
}
}
#[derive(Debug, Clone)]
pub struct StagedSummary {
pub summary_id: String,
pub content_path: String,
pub content_sha256: String,
}
pub fn stage_summary(
content_root: &Path,
input: &SummaryComposeInput<'_>,
scope_slug: &str,
) -> anyhow::Result<StagedSummary> {
stage_summary_with_layout(content_root, input, scope_slug, SummaryDiskLayout::Standard)
}
pub fn stage_summary_with_layout(
content_root: &Path,
input: &SummaryComposeInput<'_>,
scope_slug: &str,
layout: SummaryDiskLayout<'_>,
) -> anyhow::Result<StagedSummary> {
let rel_path = summary_rel_path_with_layout(
input.tree_kind,
scope_slug,
input.level,
input.summary_id,
layout,
);
let abs_path = {
let mut abs = content_root.to_path_buf();
for component in rel_path.split('/') {
abs.push(component);
}
abs
};
let composed = compose_summary_md(input);
let body_bytes = composed.body.as_bytes();
let sha256 = sha256_hex(body_bytes);
if abs_path.exists() {
let disk_sha = read_body_sha256(&abs_path).unwrap_or_default();
if disk_sha == sha256 {
return Ok(StagedSummary {
summary_id: input.summary_id.to_string(),
content_path: rel_path,
content_sha256: sha256,
});
}
let _ = std::fs::remove_file(&abs_path);
}
let full_bytes = composed.full.as_bytes();
write_if_new(&abs_path, full_bytes)?;
Ok(StagedSummary {
summary_id: input.summary_id.to_string(),
content_path: rel_path,
content_sha256: sha256,
})
}
fn read_body_sha256(path: &Path) -> anyhow::Result<String> {
let raw = std::fs::read(path)?;
let content = std::str::from_utf8(&raw)?;
let (_fm, body) = split_front_matter(content)
.ok_or_else(|| anyhow::anyhow!("no front-matter in {:?}", path))?;
Ok(sha256_hex(body.as_bytes()))
}
pub fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
let digest = hasher.finalize();
let mut out = String::with_capacity(64);
for b in digest {
use std::fmt::Write;
let _ = write!(out, "{b:02x}");
}
out
}
fn uuid_v4_hex() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let t = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!(
"{:08x}{:016x}",
t,
n.wrapping_mul(0x9e37_79b9_7f4a_7c15).wrapping_add(t as u64)
)
}
#[cfg(test)]
#[path = "atomic_tests.rs"]
mod tests;