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