use crate::store::{self, Store};
use anyhow::{Context, Result};
use std::fmt;
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
pub const FORMAT: u32 = 1;
pub const FORMAT_FILE: &str = "format";
pub struct Migration {
pub to: u32,
pub what: &'static str,
pub run: fn(&Store) -> Result<()>,
}
pub const MIGRATIONS: &[Migration] = &[];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TooNew {
pub dir: PathBuf,
pub format: u32,
}
impl fmt::Display for TooNew {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} was written by a newer kanbrain (data format {}; this one knows up to {FORMAT}): update it with \
`kb update`",
self.dir.display(),
self.format
)
}
}
impl std::error::Error for TooNew {}
pub fn format_of(dir: &Path) -> Result<u32> {
let path = dir.join(FORMAT_FILE);
match store::read_text(&path) {
Ok(text) => text
.trim()
.parse()
.ok()
.filter(|&format| format >= 1)
.with_context(|| format!("{} isn't a format number", path.display())),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(1),
Err(e) => Err(e).with_context(|| format!("reading {}", path.display())),
}
}
pub fn check(dir: &Path) -> Result<()> {
match format_of(dir)? {
format if format > FORMAT => Err(TooNew { dir: dir.to_path_buf(), format }.into()),
_ => Ok(()),
}
}
pub fn migrate(store: &Store) -> Result<Vec<String>> {
migrate_with(store, MIGRATIONS, FORMAT)
}
fn migrate_with(store: &Store, migrations: &[Migration], target: u32) -> Result<Vec<String>> {
let dir = store.dir();
let behind = |format: u32| -> Result<bool> {
if format > target {
return Err(TooNew { dir: dir.to_path_buf(), format }.into());
}
Ok(format < target)
};
if !behind(format_of(dir)?)? {
return Ok(Vec::new());
}
let _lock = store.lock()?;
let from = format_of(dir)?;
if !behind(from)? {
return Ok(Vec::new());
}
let backup = back_up(dir, from, target)?;
let mut done = vec![format!("backed up {} to {} before migrating it", dir.display(), backup.display())];
for step in &migrations[(from - 1) as usize..(target - 1) as usize] {
(step.run)(store).with_context(|| format!("migrating {} to format {}", dir.display(), step.to))?;
store::write_file(&dir.join(FORMAT_FILE), &format!("{}\n", step.to))?;
done.push(format!("migrated {} to data format {}: {}", dir.display(), step.to, step.what));
}
Ok(done)
}
fn back_up(dir: &Path, from: u32, to: u32) -> Result<PathBuf> {
let at = crate::model::now().format("%Y%m%dT%H%M%S");
let backup = store::cache_dir_of(dir, "backups")?.join(format!("{at}-format-{from}-to-{to}"));
copy_dir(dir, &backup)?;
Ok(backup)
}
fn copy_dir(from: &Path, to: &Path) -> Result<()> {
fs::create_dir_all(to).with_context(|| format!("creating {}", to.display()))?;
for entry in fs::read_dir(from).with_context(|| format!("reading {}", from.display()))? {
let entry = entry?;
let target = to.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_dir(&entry.path(), &target)?;
} else {
fs::copy(entry.path(), &target).with_context(|| format!("copying {}", entry.path().display()))?;
}
}
Ok(())
}
#[cfg(test)]
#[path = "../tests/migrate.rs"]
mod tests;