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 store::read_text(&path) {
67 Ok(text) => text
69 .trim()
70 .parse()
71 .ok()
72 .filter(|&format| format >= 1)
73 .with_context(|| format!("{} isn't a format number", path.display())),
74 Err(e) if e.kind() == ErrorKind::NotFound => Ok(1),
75 Err(e) => Err(e).with_context(|| format!("reading {}", path.display())),
76 }
77}
78
79pub fn check(dir: &Path) -> Result<()> {
81 match format_of(dir)? {
82 format if format > FORMAT => Err(TooNew { dir: dir.to_path_buf(), format }.into()),
83 _ => Ok(()),
84 }
85}
86
87pub fn migrate(store: &Store) -> Result<Vec<String>> {
90 migrate_with(store, MIGRATIONS, FORMAT)
91}
92
93fn migrate_with(store: &Store, migrations: &[Migration], target: u32) -> Result<Vec<String>> {
95 let dir = store.dir();
96 let behind = |format: u32| -> Result<bool> {
97 if format > target {
98 return Err(TooNew { dir: dir.to_path_buf(), format }.into());
99 }
100 Ok(format < target)
101 };
102 if !behind(format_of(dir)?)? {
103 return Ok(Vec::new());
104 }
105 let _lock = store.lock()?;
106 let from = format_of(dir)?;
108 if !behind(from)? {
109 return Ok(Vec::new());
110 }
111 let backup = back_up(dir, from, target)?;
112 let mut done = vec![format!("backed up {} to {} before migrating it", dir.display(), backup.display())];
113 for step in &migrations[(from - 1) as usize..(target - 1) as usize] {
114 (step.run)(store).with_context(|| format!("migrating {} to format {}", dir.display(), step.to))?;
115 store::write_file(&dir.join(FORMAT_FILE), &format!("{}\n", step.to))?;
116 done.push(format!("migrated {} to data format {}: {}", dir.display(), step.to, step.what));
117 }
118 Ok(done)
119}
120
121fn back_up(dir: &Path, from: u32, to: u32) -> Result<PathBuf> {
124 let at = crate::model::now().format("%Y%m%dT%H%M%S");
125 let backup = store::cache_dir_of(dir, "backups")?.join(format!("{at}-format-{from}-to-{to}"));
126 copy_dir(dir, &backup)?;
127 Ok(backup)
128}
129
130fn copy_dir(from: &Path, to: &Path) -> Result<()> {
132 fs::create_dir_all(to).with_context(|| format!("creating {}", to.display()))?;
133 for entry in fs::read_dir(from).with_context(|| format!("reading {}", from.display()))? {
134 let entry = entry?;
135 let target = to.join(entry.file_name());
136 if entry.file_type()?.is_dir() {
137 copy_dir(&entry.path(), &target)?;
138 } else {
139 fs::copy(entry.path(), &target).with_context(|| format!("copying {}", entry.path().display()))?;
140 }
141 }
142 Ok(())
143}
144
145#[cfg(test)]
146#[path = "../tests/migrate.rs"]
147mod tests;