1use crate::store::{self, Store};
13use anyhow::{Context, Result};
14use std::fmt;
15use std::fs;
16use std::io::ErrorKind;
17use std::path::{Path, PathBuf};
18
19pub const FORMAT: u32 = 1;
21
22pub const FORMAT_FILE: &str = "format";
25
26pub struct Migration {
28 pub to: u32,
30 pub what: &'static str,
32 pub run: fn(&Store) -> Result<()>,
34}
35
36pub const MIGRATIONS: &[Migration] = &[];
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct TooNew {
42 pub dir: PathBuf,
44 pub format: u32,
46}
47
48impl fmt::Display for TooNew {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(
51 f,
52 "{} was written by a newer kanbrain (data format {}; this one knows up to {FORMAT}): update it with \
53 `kb update`",
54 self.dir.display(),
55 self.format
56 )
57 }
58}
59
60impl std::error::Error for TooNew {}
61
62pub fn format_of(dir: &Path) -> Result<u32> {
64 let path = dir.join(FORMAT_FILE);
65 match fs::read_to_string(&path) {
66 Ok(text) => text.trim().parse().with_context(|| format!("{} isn't a format number", path.display())),
67 Err(e) if e.kind() == ErrorKind::NotFound => Ok(1),
68 Err(e) => Err(e).with_context(|| format!("reading {}", path.display())),
69 }
70}
71
72pub fn check(dir: &Path) -> Result<()> {
74 match format_of(dir)? {
75 format if format > FORMAT => Err(TooNew { dir: dir.to_path_buf(), format }.into()),
76 _ => Ok(()),
77 }
78}
79
80pub fn migrate(store: &Store) -> Result<Vec<String>> {
83 migrate_with(store, MIGRATIONS, FORMAT)
84}
85
86fn migrate_with(store: &Store, migrations: &[Migration], target: u32) -> Result<Vec<String>> {
88 let dir = store.dir();
89 let behind = |format: u32| -> Result<bool> {
90 if format > target {
91 return Err(TooNew { dir: dir.to_path_buf(), format }.into());
92 }
93 Ok(format < target)
94 };
95 if !behind(format_of(dir)?)? {
96 return Ok(Vec::new());
97 }
98 let _lock = store.lock()?;
99 let from = format_of(dir)?;
101 if !behind(from)? {
102 return Ok(Vec::new());
103 }
104 let backup = back_up(dir, from, target)?;
105 let mut done = vec![format!("backed up {} to {} before migrating it", dir.display(), backup.display())];
106 for step in &migrations[(from - 1) as usize..(target - 1) as usize] {
107 (step.run)(store).with_context(|| format!("migrating {} to format {}", dir.display(), step.to))?;
108 store::write_file(&dir.join(FORMAT_FILE), &format!("{}\n", step.to))?;
109 done.push(format!("migrated {} to data format {}: {}", dir.display(), step.to, step.what));
110 }
111 Ok(done)
112}
113
114fn back_up(dir: &Path, from: u32, to: u32) -> Result<PathBuf> {
117 let at = crate::model::now().format("%Y%m%dT%H%M%S");
118 let backup = store::cache_dir_of(dir, "backups")?.join(format!("{at}-format-{from}-to-{to}"));
119 copy_dir(dir, &backup)?;
120 Ok(backup)
121}
122
123fn copy_dir(from: &Path, to: &Path) -> Result<()> {
125 fs::create_dir_all(to).with_context(|| format!("creating {}", to.display()))?;
126 for entry in fs::read_dir(from).with_context(|| format!("reading {}", from.display()))? {
127 let entry = entry?;
128 let target = to.join(entry.file_name());
129 if entry.file_type()?.is_dir() {
130 copy_dir(&entry.path(), &target)?;
131 } else {
132 fs::copy(entry.path(), &target).with_context(|| format!("copying {}", entry.path().display()))?;
133 }
134 }
135 Ok(())
136}
137
138#[cfg(test)]
139#[path = "../tests/migrate.rs"]
140mod tests;