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}