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
40const _: () = {
44 assert!(MIGRATIONS.len() == (FORMAT - 1) as usize, "MIGRATIONS needs one step per format after the first");
45 let mut i = 0;
46 while i < MIGRATIONS.len() {
47 assert!(MIGRATIONS[i].to as usize == i + 2, "MIGRATIONS[i] brings a brain to format i + 2");
48 i += 1;
49 }
50};
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct TooNew {
55 pub dir: PathBuf,
57 pub format: u32,
59}
60
61impl fmt::Display for TooNew {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 write!(
64 f,
65 "{} was written by a newer kanbrain (data format {}; this one knows up to {FORMAT}): update it with \
66 `kb update`",
67 self.dir.display(),
68 self.format
69 )
70 }
71}
72
73impl std::error::Error for TooNew {}
74
75pub fn format_of(dir: &Path) -> Result<u32> {
77 let path = dir.join(FORMAT_FILE);
78 match store::read_text(&path) {
79 Ok(text) => text
81 .trim()
82 .parse()
83 .ok()
84 .filter(|&format| format >= 1)
85 .with_context(|| format!("{} isn't a format number", path.display())),
86 Err(e) if e.kind() == ErrorKind::NotFound => Ok(1),
87 Err(e) => Err(e).with_context(|| format!("reading {}", path.display())),
88 }
89}
90
91pub fn check(dir: &Path) -> Result<()> {
93 match format_of(dir)? {
94 format if format > FORMAT => Err(TooNew { dir: dir.to_path_buf(), format }.into()),
95 _ => Ok(()),
96 }
97}
98
99pub(crate) fn refuse_too_new(dir: &Path) -> Result<()> {
103 match check(dir) {
104 Err(e) if e.is::<TooNew>() => Err(e),
105 _ => Ok(()),
106 }
107}
108
109pub(crate) fn too_new(dir: &Path) -> bool {
111 refuse_too_new(dir).is_err()
112}
113
114pub fn migrate(store: &Store) -> Result<Vec<String>> {
117 migrate_with(store, MIGRATIONS, FORMAT)
118}
119
120fn migrate_with(store: &Store, migrations: &[Migration], target: u32) -> Result<Vec<String>> {
122 let dir = store.dir();
123 let behind = |format: u32| -> Result<bool> {
124 if format > target {
125 return Err(TooNew { dir: dir.to_path_buf(), format }.into());
126 }
127 Ok(format < target)
128 };
129 if !behind(format_of(dir)?)? {
130 return Ok(Vec::new());
131 }
132 let _lock = store.lock()?;
133 let from = format_of(dir)?;
135 if !behind(from)? {
136 return Ok(Vec::new());
137 }
138 let backup = back_up(dir, from, target)?;
139 let mut done = vec![format!("backed up {} to {} before migrating it", dir.display(), backup.display())];
140 for step in &migrations[(from - 1) as usize..(target - 1) as usize] {
141 (step.run)(store).with_context(|| format!("migrating {} to format {}", dir.display(), step.to))?;
142 store::write_file(&dir.join(FORMAT_FILE), &format!("{}\n", step.to))?;
143 done.push(format!("migrated {} to data format {}: {}", dir.display(), step.to, step.what));
144 }
145 Ok(done)
146}
147
148fn back_up(dir: &Path, from: u32, to: u32) -> Result<PathBuf> {
151 let at = crate::model::now().format("%Y%m%dT%H%M%S");
152 let backup = store::cache_dir_of(dir, "backups")?.join(format!("{at}-format-{from}-to-{to}"));
153 copy_dir(dir, &backup)?;
154 Ok(backup)
155}
156
157fn copy_dir(from: &Path, to: &Path) -> Result<()> {
159 fs::create_dir_all(to).with_context(|| format!("creating {}", to.display()))?;
160 for entry in fs::read_dir(from).with_context(|| format!("reading {}", from.display()))? {
161 let entry = entry?;
162 let target = to.join(entry.file_name());
163 if entry.file_type()?.is_dir() {
164 copy_dir(&entry.path(), &target)?;
165 } else {
166 fs::copy(entry.path(), &target).with_context(|| format!("copying {}", entry.path().display()))?;
167 }
168 }
169 Ok(())
170}
171
172#[cfg(test)]
173#[path = "../tests/migrate.rs"]
174mod tests;