use std::io::Write;
use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use doiget_core::provenance::{migrate_v1_to_v2, MigrationReport};
pub fn migrate(dry_run: bool) -> Result<()> {
let log_path = resolve_log_path()?;
let report = migrate_v1_to_v2(&log_path, dry_run)
.with_context(|| format!("migrating provenance log at {log_path}"))?;
let stdout = std::io::stdout();
let mut out = stdout.lock();
emit_summary(&mut out, &log_path, &report)?;
Ok(())
}
fn resolve_log_path() -> Result<Utf8PathBuf> {
if let Ok(s) = std::env::var("DOIGET_LOG_PATH") {
if !s.is_empty() {
return Ok(Utf8PathBuf::from(s));
}
}
let cfg = Utf8PathBuf::try_from(
dirs::config_dir().ok_or_else(|| anyhow::anyhow!("no config dir on this platform"))?,
)
.context("config directory path is not valid UTF-8")?;
Ok(cfg.join("doiget").join("access.jsonl"))
}
fn emit_summary<W: Write>(
out: &mut W,
log_path: &camino::Utf8Path,
report: &MigrationReport,
) -> Result<()> {
let mode = if report.dry_run {
"dry-run (no disk writes)"
} else {
"applied"
};
writeln!(out, "provenance migrate v1 -> v2: {mode}").context("write summary header")?;
writeln!(out, " log path: {log_path}").context("write log path")?;
writeln!(out, " rows rewritten: {}", report.rows_rewritten)
.context("write rows count")?;
writeln!(
out,
" first-row v1 chain hash: {}",
report.first_row_v1_chain_hash
)
.context("write v1 anchor")?;
writeln!(
out,
" first-row v2 chain hash: {}",
report.first_row_v2_chain_hash
)
.context("write v2 anchor")?;
if !report.dry_run && report.rows_rewritten > 0 {
writeln!(out, " backup preserved at: {log_path}.v1-backup")
.context("write backup notice")?;
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
use camino::Utf8PathBuf;
use tempfile::TempDir;
struct EnvGuard {
var: &'static str,
prior: Option<std::ffi::OsString>,
}
impl EnvGuard {
fn set(var: &'static str, value: &str) -> Self {
let prior = std::env::var_os(var);
std::env::set_var(var, value);
EnvGuard { var, prior }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match &self.prior {
Some(v) => std::env::set_var(self.var, v),
None => std::env::remove_var(self.var),
}
}
}
fn tmp_dir_utf8(dir: &TempDir) -> Utf8PathBuf {
Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("temp dir path must be UTF-8")
}
#[test]
#[serial_test::serial]
fn migrate_dry_run_on_missing_log_is_noop() {
let dir = TempDir::new().expect("tmp");
let path = tmp_dir_utf8(&dir).join("never-created.jsonl");
assert!(!path.exists(), "precondition");
let _g = EnvGuard::set("DOIGET_LOG_PATH", path.as_str());
migrate(true).expect("dry-run on missing log must succeed");
}
}