1#![allow(clippy::let_and_return, clippy::let_unit_value)]
5#![warn(clippy::dbg_macro, clippy::unwrap_used)]
6
7#[macro_use]
8mod redefine;
9
10mod args;
11#[doc(hidden)]
12pub mod btrfs;
13mod ops;
14mod repo;
15#[doc(hidden)]
16pub mod snapshot;
17#[allow(clippy::unwrap_used)]
18#[cfg(any(test, feature = "test"))]
19pub mod test;
20#[doc(hidden)]
21pub mod util;
22
23use std::borrow::Cow;
24use std::ffi::OsString;
25use std::fs::canonicalize;
26use std::path::Path;
27use std::path::PathBuf;
28
29use anyhow::Context as _;
30use anyhow::Error;
31use anyhow::Result;
32
33use clap::error::ErrorKind;
34use clap::Parser as _;
35
36use crate::args::Args;
37use crate::args::Backup;
38use crate::args::Command;
39use crate::args::Purge;
40use crate::args::RemoteCommand;
41use crate::args::Restore;
42use crate::args::Snapshot;
43use crate::args::Tag;
44use crate::btrfs::trace_commands;
45use crate::btrfs::Btrfs;
46use crate::ops::RemoteOps;
47use crate::repo::backup as backup_subvol;
48use crate::repo::purge as purge_subvol;
49use crate::repo::restore as restore_subvol;
50use crate::repo::Repo;
51use crate::util::canonicalize_non_strict;
52
53
54fn create_repo(directory: &Path, remote_command: Option<(String, Vec<String>)>) -> Result<Repo> {
57 let mut builder = Repo::builder();
58
59 if let Some((command, args)) = remote_command {
60 let ops = RemoteOps::new(command.clone(), args.clone());
61 let () = builder.set_file_ops(ops);
62
63 let btrfs = Btrfs::with_command_prefix(command, args);
64 let () = builder.set_btrfs_ops(btrfs);
65 }
66
67 let repo = builder.build(directory).with_context(|| {
68 format!(
69 "failed to create btrfs snapshot repository at {}",
70 directory.display()
71 )
72 })?;
73
74 Ok(repo)
75}
76
77
78fn with_repo_and_subvols<F>(repo_path: Option<&Path>, subvols: &[PathBuf], f: F) -> Result<()>
82where
83 F: FnMut(&Repo, &Path) -> Result<()>,
84{
85 let mut f = f;
86
87 let repo = if let Some(repo_path) = repo_path {
88 Some(create_repo(repo_path, None)?)
89 } else {
90 None
91 };
92
93 let () = subvols.iter().try_for_each::<_, Result<()>>(|subvol| {
94 let repo = if let Some(repo) = &repo {
95 Cow::Borrowed(repo)
96 } else {
97 let directory = subvol.parent().with_context(|| {
98 format!(
99 "subvolume {} does not have a parent; need repository path",
100 subvol.display()
101 )
102 })?;
103 Cow::Owned(create_repo(directory, None)?)
104 };
105
106 f(&repo, subvol)
107 })?;
108
109 Ok(())
110}
111
112
113fn backup(backup: Backup) -> Result<()> {
115 let Backup {
116 mut subvolumes,
117 destination,
118 source,
119 tag: Tag { tag },
120 remote_command: RemoteCommand { remote_command },
121 } = backup;
122
123 let () = subvolumes.iter_mut().try_for_each(|subvol| {
124 *subvol = canonicalize(&*subvol)?;
125 Result::<(), Error>::Ok(())
126 })?;
127
128 let dst = create_repo(&destination, remote_command)?;
129
130 with_repo_and_subvols(source.as_deref(), subvolumes.as_slice(), |src, subvol| {
131 let _snapshot = backup_subvol(src, &dst, subvol, &tag)
132 .with_context(|| format!("failed to backup subvolume {}", subvol.display()))?;
133 Ok(())
134 })
135}
136
137
138fn restore(restore: Restore) -> Result<()> {
140 let Restore {
141 mut subvolumes,
142 destination,
143 source,
144 remote_command: RemoteCommand { remote_command },
145 snapshots_only,
146 } = restore;
147
148 let () = subvolumes.iter_mut().try_for_each(|subvol| {
149 *subvol = canonicalize_non_strict(subvol)?;
150 Result::<(), Error>::Ok(())
151 })?;
152
153 let src = create_repo(&source, remote_command)?;
154
155 with_repo_and_subvols(
156 destination.as_deref(),
157 subvolumes.as_slice(),
158 |dst, subvol| {
159 let _snapshot = restore_subvol(&src, dst, subvol, snapshots_only)
160 .with_context(|| format!("failed to restore subvolume {}", subvol.display()))?;
161 Ok(())
162 },
163 )
164}
165
166
167fn purge(purge: Purge) -> Result<()> {
169 let Purge {
170 mut subvolumes,
171 source,
172 destination,
173 tag: Tag { tag },
174 remote_command: RemoteCommand { remote_command },
175 keep_for,
176 } = purge;
177
178 let () = subvolumes.iter_mut().try_for_each(|subvol| {
179 *subvol = canonicalize_non_strict(subvol)?;
180 Result::<(), Error>::Ok(())
181 })?;
182
183 if let Some(destination) = destination {
184 let repo = create_repo(&destination, remote_command)?;
185
186 let () = subvolumes
187 .iter()
188 .try_for_each(|subvol| purge_subvol(&repo, subvol, &tag, keep_for))?;
189 }
190
191 with_repo_and_subvols(source.as_deref(), subvolumes.as_slice(), |repo, subvol| {
194 purge_subvol(repo, subvol, &tag, keep_for)
195 })
196}
197
198
199fn snapshot(snapshot: Snapshot) -> Result<()> {
201 let Snapshot {
202 repository,
203 mut subvolumes,
204 tag: Tag { tag },
205 } = snapshot;
206
207 let () = subvolumes.iter_mut().try_for_each(|subvol| {
208 *subvol = canonicalize(&*subvol)?;
209 Result::<(), Error>::Ok(())
210 })?;
211
212 with_repo_and_subvols(
213 repository.as_deref(),
214 subvolumes.as_slice(),
215 |repo, subvol| {
216 let _snapshot = repo
217 .snapshot(subvol, &tag)
218 .with_context(|| format!("failed to snapshot subvolume {}", subvol.display()))?;
219 Ok(())
220 },
221 )
222}
223
224
225pub fn run<A, T>(args: A) -> Result<()>
227where
228 A: IntoIterator<Item = T>,
229 T: Into<OsString> + Clone,
230{
231 let args = match Args::try_parse_from(args) {
232 Ok(args) => args,
233 Err(err) => match err.kind() {
234 ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
235 print!("{}", err);
236 return Ok(())
237 },
238 _ => return Err(err.into()),
239 },
240 };
241
242 if args.trace {
243 let () = trace_commands();
244 }
245
246 match args.command {
247 Command::Backup(backup) => self::backup(backup),
248 Command::Purge(purge) => self::purge(purge),
249 Command::Restore(restore) => self::restore(restore),
250 Command::Snapshot(snapshot) => self::snapshot(snapshot),
251 }
252}
253
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 use std::ffi::OsStr;
260
261
262 #[test]
264 fn version() {
265 let args = [OsStr::new("btrfs-backup"), OsStr::new("--version")];
266 let () = run(args).unwrap();
267 }
268
269 #[test]
271 fn help() {
272 let args = [OsStr::new("btrfs-backup"), OsStr::new("--help")];
273 let () = run(args).unwrap();
274 }
275}