1use std::collections::BTreeSet;
6use std::io;
7use std::path::Path;
8
9use super::{TenantError, Tenants};
10use crate::audit::{Action, Actor, Entry};
11use crate::catalog;
12use crate::envelope;
13use crate::ids::TenantId;
14use crate::store::{self, TRIES};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Backup {
19 pub tenants: u64,
20 pub versions: u64,
21 pub tries: usize,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum BackupError {
28 Exists,
30 KeptChanging,
32 Io,
35 AuditFailed,
37}
38
39impl Tenants {
40 pub fn backup(&self, who: Actor, to: &Path) -> Result<Backup, BackupError> {
51 let done = self.copy_all(to);
52 let status = match done {
53 Ok(_) => 200,
54 Err(BackupError::Exists) => 409,
55 Err(BackupError::KeptChanging) => 503,
56 Err(BackupError::Io | BackupError::AuditFailed) => 500,
57 };
58 self.record_alone(&Entry {
59 actor: who,
60 action: Action::Backup,
61 target: None,
62 version: None,
63 status,
64 })
65 .map_err(|_| BackupError::AuditFailed)?;
66 done
67 }
68
69 fn copy_all(&self, to: &Path) -> Result<Backup, BackupError> {
70 let fail = |_| BackupError::Io;
71 match self.fs.list(to) {
72 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
73 Ok(_) => return Err(BackupError::Exists),
74 Err(e) if e.kind() == io::ErrorKind::NotADirectory => return Err(BackupError::Exists),
75 Err(_) => return Err(BackupError::Io),
76 }
77 let key = envelope::catalog_key(&self.master).map_err(|_| BackupError::Io)?;
78 let tenants = to.join("tenants");
79 self.fs.create_dir_all(&tenants).map_err(fail)?;
80 {
83 use std::os::unix::fs::PermissionsExt;
84 std::fs::set_permissions(to, std::fs::Permissions::from_mode(0o700)).map_err(fail)?;
85 }
86 let registry = to.join("server.sqlite");
87 'tries: for tries in 1..=TRIES {
88 for file in ["server.sqlite", "server.sqlite-wal", "server.sqlite-shm"] {
89 remove_if_there(&to.join(file)).map_err(fail)?;
90 }
91 catalog::backup(&self.conn(), ®istry, &key).map_err(|_| BackupError::Io)?;
92 #[cfg(test)]
93 if let Some(hook) = self.hooks.registry_copied.lock().expect("hook").as_mut() {
94 hook(self);
95 }
96 let listed = tenants_in(®istry, &key)?;
97 let mut versions = 0;
98 for id in &listed {
99 let copied = match self.store(*id) {
100 Ok(store) => store.backup_to(&tenants.join(id.to_string())),
101 Err(TenantError::NotFound | TenantError::Closing) => continue 'tries,
102 Err(_) => return Err(BackupError::Io),
103 };
104 match copied {
105 Ok(copied) => versions += copied.versions,
106 Err(store::BackupError::KeptChanging) if !self.listed(*id)? => {
109 self.stores().remove(id);
110 continue 'tries;
111 }
112 Err(store::BackupError::KeptChanging) => return Err(BackupError::KeptChanging),
113 Err(store::BackupError::Io) => return Err(BackupError::Io),
114 }
115 }
116 let names: BTreeSet<String> = listed.iter().map(ToString::to_string).collect();
117 for name in self.fs.list(&tenants).map_err(fail)? {
118 let name = name.into_string().unwrap_or_default();
119 if !names.contains(&name) {
120 self.fs.remove_dir_all(&tenants.join(&name)).map_err(fail)?;
121 }
122 }
123 self.fs.sync_dir(&tenants).map_err(fail)?;
124 self.fs.sync_dir(to).map_err(fail)?;
125 if let Some(parent) = to.parent() {
126 self.fs.sync_dir(parent).map_err(fail)?;
127 }
128 return Ok(Backup {
129 tenants: listed.len() as u64,
130 versions,
131 tries,
132 });
133 }
134 Err(BackupError::KeptChanging)
135 }
136
137 fn listed(&self, id: TenantId) -> Result<bool, BackupError> {
139 use rusqlite::OptionalExtension;
140 Ok(self
141 .conn()
142 .query_row("SELECT 1 FROM tenant WHERE id = ?1", [id.0], |_| Ok(()))
143 .optional()
144 .map_err(|_| BackupError::Io)?
145 .is_some())
146 }
147}
148
149fn tenants_in(path: &Path, key: &crate::envelope::Key) -> Result<Vec<TenantId>, BackupError> {
151 let fail = |_| BackupError::Io;
152 let conn = catalog::open(path, key, "").map_err(|_| BackupError::Io)?;
153 let mut rows = conn
154 .prepare("SELECT id FROM tenant ORDER BY id")
155 .map_err(fail)?;
156 rows.query_map([], |r| r.get::<_, [u8; 16]>(0))
157 .map_err(fail)?
158 .map(|row| row.map(TenantId).map_err(fail))
159 .collect()
160}
161
162fn remove_if_there(path: &Path) -> io::Result<()> {
163 match std::fs::remove_file(path) {
164 Err(e) if e.kind() != io::ErrorKind::NotFound => Err(e),
165 _ => Ok(()),
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use std::sync::Arc;
172 use std::sync::atomic::{AtomicUsize, Ordering};
173
174 use super::*;
175 use crate::clock::Clock;
176 use crate::fs::Fs;
177 use crate::ids::TenantId;
178 use crate::store::Caller;
179 use crate::store::testing::vcon;
180 use crate::tenants::Removal;
181 use crate::tenants::tests::{Fixture, fixture};
182
183 fn two(f: &Fixture) -> [TenantId; 2] {
185 let caller = Caller {
186 key: crate::envelope::Key::generate().expect("key").id(),
187 };
188 let ids = ["acme", "globex"]
189 .map(|name| f.tenants.create(Actor::Cli, name, None).expect("creates"));
190 for (n, id) in ids.iter().enumerate() {
191 let (found, body) = vcon(&format!("01a07da8-c2bb-83e5-b9a2-27900000000{n}"), "held");
192 f.tenants
193 .store(*id)
194 .expect("store")
195 .create(&caller, &found, &body)
196 .expect("create");
197 }
198 ids
199 }
200
201 fn opened(f: &Fixture, to: &Path) -> Tenants {
203 Tenants::open_beside(to, f.master_key(), f.fs.clone(), f.clock.clone())
204 .expect("the backup opens under the master key")
205 }
206
207 fn assert_whole(backup: &Tenants, tenants: usize) {
208 let now = crate::clock::SystemClock.now();
209 let found = backup
210 .examine(crate::retention::Retention::Years(7), now)
211 .expect("examines");
212 assert_eq!(found.tenants.len(), tenants);
213 for tenant in &found.tenants {
214 let store = tenant
215 .store
216 .as_ref()
217 .expect("each catalog opens under its key");
218 assert!(store.damaged.is_empty(), "{:?}", store.damaged);
219 assert!(store.leftovers.is_empty(), "{:?}", store.leftovers);
220 assert_eq!(store.versions, 1);
221 }
222 }
223
224 fn records(f: &Fixture, action: &str) -> Vec<serde_json::Value> {
225 f.tenants
226 .admin_records()
227 .into_iter()
228 .filter(|r| r["action"] == action)
229 .collect()
230 }
231
232 #[test]
233 fn a_backup_opens_whole_under_the_master_key_and_is_recorded() {
234 let f = fixture();
235 two(&f);
236 let to = f.dir.path().join("backup");
237 let done = f.tenants.backup(Actor::Cli, &to).expect("backs up");
238 assert_eq!(
239 done,
240 Backup {
241 tenants: 2,
242 versions: 2,
243 tries: 1
244 }
245 );
246 let backup = opened(&f, &to);
247 assert_whole(&backup, 2);
248 use std::os::unix::fs::PermissionsExt;
249 let mode = std::fs::metadata(&to)
250 .expect("the backup")
251 .permissions()
252 .mode()
253 & 0o777;
254 assert_eq!(
255 mode, 0o700,
256 "only the service account may enter the backup: {mode:o}"
257 );
258 let names: Vec<String> = backup
259 .list(Actor::Cli)
260 .expect("lists")
261 .into_iter()
262 .map(|t| t.name)
263 .collect();
264 assert_eq!(names, ["acme", "globex"]);
265 let recorded = records(&f, "backup");
266 assert_eq!(recorded.len(), 1);
267 assert_eq!(recorded[0]["status"], 200);
268 }
269
270 #[test]
271 fn a_backup_survives_a_crash_once_it_returns() {
272 let f = fixture();
273 two(&f);
274 let to = f.dir.path().join("backup");
275 f.tenants.backup(Actor::Cli, &to).expect("backs up");
276 f.fs.crash();
277 assert_eq!(
278 f.fs.list(&to.join("tenants"))
279 .expect("the backup is there")
280 .len(),
281 2
282 );
283 assert_whole(&opened(&f, &to), 2);
284 }
285
286 #[test]
287 fn a_path_already_there_is_refused_and_recorded() {
288 let f = fixture();
289 two(&f);
290 let to = f.dir.path().join("backup");
291 f.fs.create_dir_all(&to).expect("dir");
292 assert_eq!(f.tenants.backup(Actor::Cli, &to), Err(BackupError::Exists));
293 assert!(
294 f.fs.list(&to).expect("list").is_empty(),
295 "nothing is written"
296 );
297 assert_eq!(records(&f, "backup")[0]["status"], 409);
298 }
299
300 #[test]
301 fn a_tenant_removed_after_the_registry_is_copied_starts_the_backup_again() {
302 let f = fixture();
303 let [acme, _] = two(&f);
304 let calls = Arc::new(AtomicUsize::new(0));
305 let seen = calls.clone();
306 *f.tenants.hooks.registry_copied.lock().expect("hook") =
307 Some(Box::new(move |tenants: &Tenants| {
308 if seen.fetch_add(1, Ordering::SeqCst) == 0 {
309 tenants
310 .remove(Actor::Cli, acme, Removal::Force)
311 .expect("removes");
312 }
313 }));
314 let to = f.dir.path().join("backup");
315 let done = f.tenants.backup(Actor::Cli, &to).expect("backs up");
316 assert_eq!(
317 done,
318 Backup {
319 tenants: 1,
320 versions: 1,
321 tries: 2
322 }
323 );
324 assert_whole(&opened(&f, &to), 1);
325 let dirs = f.fs.list(&to.join("tenants")).expect("list");
326 assert_eq!(
327 dirs.len(),
328 1,
329 "no directory is left for the removed tenant: {dirs:?}"
330 );
331 }
332
333 #[test]
334 fn a_tenant_removed_while_its_data_is_copied_starts_the_backup_again() {
335 let f = fixture();
336 let [acme, _] = two(&f);
337 let server =
341 Tenants::open_beside(f.dir.path(), f.master_key(), f.fs.clone(), f.clock.clone())
342 .expect("a second registry");
343 let once = std::sync::Mutex::new(Some(server));
344 f.tenants
345 .store(acme)
346 .expect("store")
347 .on_backup_copied(Box::new(move |_: &crate::store::Store| {
348 if let Some(server) = once.lock().expect("once").take() {
349 server
350 .remove(Actor::Cli, acme, Removal::Force)
351 .expect("removes");
352 }
353 }));
354 let to = f.dir.path().join("backup");
355 let done = f.tenants.backup(Actor::Cli, &to).expect("backs up");
356 assert_eq!(done.tenants, 1);
357 assert_eq!(done.tries, 2);
358 assert_whole(&opened(&f, &to), 1);
359 let dirs = f.fs.list(&to.join("tenants")).expect("list");
360 assert_eq!(
361 dirs.len(),
362 1,
363 "the removed tenant's partial copy is gone: {dirs:?}"
364 );
365 }
366
367 #[test]
368 fn a_registry_that_keeps_changing_fails_after_its_tries_and_is_recorded() {
369 let f = fixture();
370 let [acme, _] = two(&f);
371 let calls = Arc::new(AtomicUsize::new(0));
372 let seen = calls.clone();
373 let listed = std::sync::Mutex::new(acme);
374 *f.tenants.hooks.registry_copied.lock().expect("hook") =
375 Some(Box::new(move |tenants: &Tenants| {
376 let n = seen.fetch_add(1, Ordering::SeqCst);
380 let mut listed = listed.lock().expect("listed");
381 tenants
382 .remove(Actor::Cli, *listed, Removal::Force)
383 .expect("removes");
384 *listed = tenants
385 .create(Actor::Cli, &format!("brief {n}"), None)
386 .expect("creates");
387 }));
388 let to = f.dir.path().join("backup");
389 assert_eq!(
390 f.tenants.backup(Actor::Cli, &to),
391 Err(BackupError::KeptChanging)
392 );
393 assert_eq!(calls.load(Ordering::SeqCst), crate::store::TRIES);
394 assert_eq!(records(&f, "backup")[0]["status"], 503);
395 }
396}