tuwunel_database/engine/
backup.rs1use std::path::Path;
2
3use rocksdb::backup::{BackupEngine, BackupEngineInfo, BackupEngineOptions, RestoreOptions};
4use tuwunel_core::{
5 Config, Err, Result, err, error, implement, info, itertools::Itertools,
6 utils::time::rfc2822_from_seconds, warn,
7};
8
9use super::Engine;
10use crate::{Context, util::map_err};
11
12#[implement(Engine)]
24#[tracing::instrument(level = "debug", skip(self))]
25pub fn backup(&self) -> Result {
26 let to_keep = self.ctx.server.config.database_backups_to_keep;
27
28 if to_keep <= 0 {
29 return Err!(Config(
30 "database_backups_to_keep",
31 "Set above zero to enable backups; no backup was created."
32 ));
33 }
34
35 let mut engine = backup_engine(&self.ctx)?;
36 let flush = !self.is_read_only();
37
38 engine
39 .create_new_backup_flush(&self.db, flush)
40 .map_err(map_err)?;
41
42 let backups = engine.get_backup_info();
43 let backup = backups
44 .last()
45 .expect("backup engine info is not empty");
46
47 info!(
48 backup_id = backup.backup_id,
49 size = backup.size,
50 num_files = backup.num_files,
51 "Created database backup"
52 );
53
54 engine
55 .purge_old_backups(usize::try_from(to_keep)?)
56 .inspect_err(|e| error!(?e, "Failed to purge old backup"))
57 .ok();
58
59 Ok(())
60}
61
62#[implement(Engine)]
67#[tracing::instrument(level = "debug", skip(self))]
68pub fn backup_purge(&self, keep: usize) -> Result {
69 let mut engine = backup_engine(&self.ctx)?;
70
71 engine.purge_old_backups(keep).map_err(map_err)
72}
73
74#[implement(Engine)]
80pub fn backup_list(&self) -> Result<impl Iterator<Item = String> + Send> {
81 let info = backup_engine(&self.ctx)?.get_backup_info();
82
83 if info.is_empty() {
84 return Err!("No backups found.");
85 }
86
87 let list = info.into_iter().map(|info| {
88 format!(
89 "#{} {}: {} bytes, {} files",
90 info.backup_id,
91 rfc2822_from_seconds(info.timestamp),
92 info.size,
93 info.num_files,
94 )
95 });
96
97 Ok(list)
98}
99
100#[implement(Engine)]
105pub fn backup_count(&self) -> Result<usize> {
106 let info = backup_engine(&self.ctx)?.get_backup_info();
107
108 Ok(info.len())
109}
110
111#[implement(Engine)]
116pub fn backup_verify(&self, backup_id: u32) -> Result<u32> {
117 let engine = backup_engine(&self.ctx)?;
118 let backup = find_backup(&engine, backup_id)?;
119
120 engine
121 .verify_backup(backup.backup_id)
122 .map_err(map_err)?;
123
124 Ok(backup.backup_id)
125}
126
127pub(crate) fn restore(ctx: &Context, backup_id: u32) -> Result {
130 let mut engine = backup_engine(ctx)?;
131 let backup = find_backup(&engine, backup_id)?;
132 let path = &ctx.server.config.database_path;
133
134 warn!(
135 backup_id = backup.backup_id,
136 timestamp = %rfc2822_from_seconds(backup.timestamp),
137 size = backup.size,
138 num_files = backup.num_files,
139 ?path,
140 "Restoring database backup"
141 );
142
143 engine
144 .restore_from_backup(path, path, &RestoreOptions::default(), backup.backup_id)
145 .map_err(map_err)?;
146
147 info!(backup_id = backup.backup_id, "Restored database backup");
148
149 Ok(())
150}
151
152fn find_backup(engine: &BackupEngine, backup_id: u32) -> Result<BackupEngineInfo> {
154 let mut backups = engine.get_backup_info();
155
156 if backups.is_empty() {
157 return Err!("No backups found.");
158 }
159
160 let found = match backup_id {
161 | 0 => backups.pop(),
162 | id => backups
163 .iter()
164 .position(|info| info.backup_id == id)
165 .map(|pos| backups.swap_remove(pos)),
166 };
167
168 found.ok_or_else(|| {
169 let available = backups
170 .iter()
171 .map(|info| info.backup_id)
172 .join(", ");
173
174 err!("Backup #{backup_id} not found; available: {available}")
175 })
176}
177
178fn backup_engine(ctx: &Context) -> Result<BackupEngine> {
179 let path = backup_path(&ctx.server.config)?;
180 let options = BackupEngineOptions::new(path).map_err(map_err)?;
181
182 BackupEngine::open(&options, &*ctx.env.lock()?).map_err(map_err)
183}
184
185fn backup_path(config: &Config) -> Result<&Path> {
186 config
187 .database_backup_path
188 .as_deref()
189 .filter(|path| !path.as_os_str().is_empty())
190 .ok_or_else(|| err!(Config("database_backup_path", "Configure path to enable backups")))
191}