Skip to main content

kanbrain_core/
migrate.rs

1//! Data format versions and migrations. A brain's files (entries, `tags.md`, `last-id`, a project's cards and
2//! registry) follow a format numbered in its `format` file, [`FORMAT`] for those this version writes. Formats
3//! change only with a migration, which brings a brain from the one before: when a brain is opened to be changed,
4//! and for every registered brain on `kb update`, the migrations it lacks run in order, under its write lock,
5//! after a copy of it goes to the user's cache. A brain in a newer format than this version knows is refused
6//! entirely, read or written: an older `kb` reading a teammate's `.brain/` from git would misread what it doesn't
7//! know, and rewriting it would lose it.
8//!
9//! To change the format: add the step to [`MIGRATIONS`], bump [`FORMAT`], add a fixture of the old format to the
10//! tests, and say so in the release notes. A step is never removed, since a brain may skip versions, and works on
11//! the files' text rather than today's types, which later formats change.
12
13use crate::store::{self, Store};
14use anyhow::{Context, Result};
15use std::fmt;
16use std::fs;
17use std::io::ErrorKind;
18use std::path::{Path, PathBuf};
19
20/// The data format this version reads and writes.
21pub const FORMAT: u32 = 1;
22
23/// The file beside a brain's entries holding its format. A brain without one is format 1: from before formats
24/// were numbered, which the first format is.
25pub const FORMAT_FILE: &str = "format";
26
27/// One step from a format to the next.
28pub struct Migration {
29    /// The format it brings a brain to, from the one before.
30    pub to: u32,
31    /// What it changes, as `kb update` reports it.
32    pub what: &'static str,
33    /// Changes the brain's files. It runs under the brain's write lock, once: the format is written after it.
34    pub run: fn(&Store) -> Result<()>,
35}
36
37/// Every step, in order: `MIGRATIONS[i]` brings a brain from format `i + 1` to `i + 2`.
38pub const MIGRATIONS: &[Migration] = &[];
39
40/// A brain in a format newer than [`FORMAT`]: written by a newer kanbrain, so this one leaves it alone.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct TooNew {
43    /// The brain's folder.
44    pub dir: PathBuf,
45    /// Its format.
46    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
63/// The format of the brain in `dir`.
64pub 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
73/// Refuses, with [`TooNew`], a brain in a format newer than [`FORMAT`].
74pub 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
81/// Brings the brain in `store` to [`FORMAT`], and returns what it did, one line each; nothing for a brain already
82/// there.
83pub fn migrate(store: &Store) -> Result<Vec<String>> {
84    migrate_with(store, MIGRATIONS, FORMAT)
85}
86
87/// [`migrate`], with the steps and the format they lead to given, so tests can run steps of their own.
88fn 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    // Again under the lock: another `kb` may have migrated it meanwhile.
101    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
115/// Copies the brain in `dir`, every file and folder in it, to `<cache>/kanbrain/backups/<key>/<time>-<from>-<to>`,
116/// and returns where.
117fn 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
124/// Copies `from` into `to`, created, recursively.
125fn 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;