lib/backup/
mod.rs

1//! Defines types for backing-up macOS's Apple Books databases.
2
3use std::path::Path;
4
5use chrono::{DateTime, Local};
6use serde::Serialize;
7
8use crate::applebooks::macos::utils::APPLEBOOKS_VERSION;
9use crate::applebooks::macos::ABDatabase;
10use crate::result::Result;
11use crate::strings;
12
13/// The default back-up directory template.
14///
15/// Outputs `[YYYY-MM-DD-HHMMSS]-[VERSION]` e.g. `1970-01-01-120000-v0.1-0000`.
16pub const DIRECTORY_TEMPLATE: &str = "{{ now |  date(format='%Y-%m-%d-%H%M%S')}}-{{ version }}";
17
18/// A struct for running the back-up task.
19#[derive(Debug, Clone, Copy)]
20pub struct BackupRunner;
21
22impl BackupRunner {
23    /// Runs the back-up task.
24    ///
25    /// # Arguments
26    ///
27    /// * `databases` - The directory to back-up.
28    /// * `output` - The ouput directory.
29    /// * `options` - The back-up options.
30    ///
31    /// # Errors
32    ///
33    /// Will return `Err` if any IO errors are encountered.
34    pub fn run<O>(databases: &Path, output: &Path, options: O) -> Result<()>
35    where
36        O: Into<BackupOptions>,
37    {
38        let options: BackupOptions = options.into();
39
40        Self::backup(databases, output, options)?;
41
42        Ok(())
43    }
44
45    /// Backs-up macOS's Apple Books databases to disk.
46    ///
47    /// # Arguments
48    ///
49    /// * `databases` - The directory to back-up.
50    /// * `output` - The ouput directory.
51    /// * `options` - The back-up options.
52    ///
53    /// The `output` strucutre is as follows:
54    ///
55    /// ```plaintext
56    /// [ouput-directory]
57    ///  │
58    ///  ├── [YYYY-MM-DD-HHMMSS-VERSION]
59    ///  │    │
60    ///  │    ├── AEAnnotation
61    ///  │    │   ├── AEAnnotation*.sqlite
62    ///  │    │   └── ...
63    ///  │    │
64    ///  │    └─ BKLibrary
65    ///  │       ├── BKLibrary*.sqlite
66    ///  │       └── ...
67    ///  │
68    ///  ├── [YYYY-MM-DD-HHMMSS-VERSION]
69    ///  │    └── ...
70    ///  └── ...
71    /// ```
72    ///
73    /// See [`ABMacos`][abmacos] for information
74    ///
75    /// # Errors
76    ///
77    /// Will return `Err` if any IO errors are encountered.
78    ///
79    /// [abmacos]: crate::applebooks::macos::ABMacos
80    pub fn backup(databases: &Path, output: &Path, options: BackupOptions) -> Result<()> {
81        let directory_template = if let Some(template) = options.directory_template {
82            Self::validate_template(&template)?;
83            template
84        } else {
85            DIRECTORY_TEMPLATE.to_string()
86        };
87
88        // -> [YYYY-MM-DD-HHMMSS]-[VERSION]
89        let directory_name = Self::render_directory_name(&directory_template)?;
90
91        // -> [ouput-directory]/[YYYY-MM-DD-HHMMSS]-[VERSION]
92        let destination_root = output.join(directory_name);
93
94        for name in &[
95            ABDatabase::Books.to_string(),
96            ABDatabase::Annotations.to_string(),
97        ] {
98            // -> [databases-directory]/[name]
99            let source = databases.join(name.clone());
100            // -> [ouput-directory]/[YYYY-MM-DD-HHMMSS]-[VERSION]/[name]
101            let destination = destination_root.join(name);
102
103            crate::utils::copy_dir(source, destination)?;
104        }
105
106        Ok(())
107    }
108
109    /// Validates a template by rendering it.
110    ///
111    /// Seeing as [`BackupNameContext`] requires no external context, this is a pretty
112    /// straightforward validation check. The template is rendered and an empty [`Result`] is
113    /// returned.
114    ///
115    /// # Arguments
116    ///
117    /// * `template` - The template string to validate.
118    fn validate_template(template: &str) -> Result<()> {
119        Self::render_directory_name(template).map(|_| ())
120    }
121
122    /// Renders the directory name from a template string.
123    ///
124    /// # Arguments
125    ///
126    /// * `template` - The template string to render.
127    fn render_directory_name(template: &str) -> Result<String> {
128        let context = BackupNameContext::default();
129
130        strings::render_and_sanitize(template, context)
131    }
132}
133
134/// A struct representing options for the [`BackupRunner`] struct.
135#[derive(Debug)]
136pub struct BackupOptions {
137    /// The template to use render for rendering the back-up's ouput directory.
138    pub directory_template: Option<String>,
139}
140
141/// A struct represening the template context for back-ups.
142///
143/// This is primarily used for generating directory names.
144#[derive(Debug, Serialize)]
145struct BackupNameContext {
146    /// The current datetime.
147    now: DateTime<Local>,
148
149    /// The currently installed version of Apple Books for macOS.
150    version: String,
151}
152
153impl Default for BackupNameContext {
154    fn default() -> Self {
155        Self {
156            now: Local::now(),
157            version: APPLEBOOKS_VERSION.to_owned(),
158        }
159    }
160}
161
162#[cfg(test)]
163mod test {
164
165    use super::*;
166
167    use crate::defaults::test::TemplatesDirectory;
168    use crate::utils;
169
170    // Tests that the default template returns no error.
171    #[test]
172    fn default_directory_template() {
173        let context = BackupNameContext::default();
174
175        strings::render_and_sanitize(DIRECTORY_TEMPLATE, context).unwrap();
176    }
177
178    // Tests that all valid context fields return no errors.
179    #[test]
180    fn valid_context() {
181        let template =
182            utils::testing::load_template_str(TemplatesDirectory::ValidContext, "valid-backup.txt");
183        let context = BackupNameContext::default();
184
185        strings::render_and_sanitize(&template, context).unwrap();
186    }
187
188    // Tests that an invalid context field returns an error.
189    #[test]
190    #[should_panic(expected = "Failed to render '__tera_one_off'")]
191    fn invalid_context() {
192        let template = utils::testing::load_template_str(
193            TemplatesDirectory::InvalidContext,
194            "invalid-backup.txt",
195        );
196        let context = BackupNameContext::default();
197
198        strings::render_and_sanitize(&template, context).unwrap();
199    }
200}