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