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)?;
124 Result::<(), Error>::Ok(())
125 })?;
126
127 let dst = create_repo(&destination, remote_command)?;
128
129 with_repo_and_subvols(source.as_deref(), subvolumes.as_slice(), |src, subvol| {
130 let _snapshot = backup_subvol(src, &dst, subvol, &tag)
131 .with_context(|| format!("failed to backup subvolume {}", subvol.display()))?;
132 Ok(())
133 })
134}
135
136
137fn restore(restore: Restore) -> Result<()> {
139 let Restore {
140 mut subvolumes,
141 destination,
142 source,
143 remote_command: RemoteCommand { remote_command },
144 snapshots_only,
145 } = restore;
146
147 let () = subvolumes.iter_mut().try_for_each(|subvol| {
148 *subvol = canonicalize_non_strict(subvol)?;
149 Result::<(), Error>::Ok(())
150 })?;
151
152 let src = create_repo(&source, remote_command)?;
153
154 with_repo_and_subvols(
155 destination.as_deref(),
156 subvolumes.as_slice(),
157 |dst, subvol| {
158 let _snapshot = restore_subvol(&src, dst, subvol, snapshots_only)
159 .with_context(|| format!("failed to restore subvolume {}", subvol.display()))?;
160 Ok(())
161 },
162 )
163}
164
165
166fn purge(purge: Purge) -> Result<()> {
168 let Purge {
169 mut subvolumes,
170 source,
171 destination,
172 tag: Tag { tag },
173 remote_command: RemoteCommand { remote_command },
174 keep_for,
175 } = purge;
176
177 let () = subvolumes.iter_mut().try_for_each(|subvol| {
178 *subvol = canonicalize_non_strict(subvol)?;
179 Result::<(), Error>::Ok(())
180 })?;
181
182 if let Some(destination) = destination {
183 let repo = create_repo(&destination, remote_command)?;
184
185 let () = subvolumes
186 .iter()
187 .try_for_each(|subvol| purge_subvol(&repo, subvol, &tag, keep_for))?;
188 }
189
190 with_repo_and_subvols(source.as_deref(), subvolumes.as_slice(), |repo, subvol| {
193 purge_subvol(repo, subvol, &tag, keep_for)
194 })
195}
196
197
198fn snapshot(snapshot: Snapshot) -> Result<()> {
200 let Snapshot {
201 repository,
202 mut subvolumes,
203 tag: Tag { tag },
204 } = snapshot;
205
206 let () = subvolumes.iter_mut().try_for_each(|subvol| {
207 *subvol = canonicalize(&*subvol)?;
208 Result::<(), Error>::Ok(())
209 })?;
210
211 with_repo_and_subvols(
212 repository.as_deref(),
213 subvolumes.as_slice(),
214 |repo, subvol| {
215 let _snapshot = repo
216 .snapshot(subvol, &tag)
217 .with_context(|| format!("failed to snapshot subvolume {}", subvol.display()))?;
218 Ok(())
219 },
220 )
221}
222
223
224pub fn run<A, T>(args: A) -> Result<()>
226where
227 A: IntoIterator<Item = T>,
228 T: Into<OsString> + Clone,
229{
230 let args = match Args::try_parse_from(args) {
231 Ok(args) => args,
232 Err(err) => match err.kind() {
233 ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
234 print!("{err}");
235 return Ok(())
236 },
237 _ => return Err(err.into()),
238 },
239 };
240
241 if args.trace {
242 let () = trace_commands();
243 }
244
245 match args.command {
246 Command::Backup(backup) => self::backup(backup),
247 Command::Purge(purge) => self::purge(purge),
248 Command::Restore(restore) => self::restore(restore),
249 Command::Snapshot(snapshot) => self::snapshot(snapshot),
250 }
251}
252
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 use std::ffi::OsStr;
259
260
261 #[test]
263 fn version() {
264 let args = [OsStr::new("btrfs-backup"), OsStr::new("--version")];
265 let () = run(args).unwrap();
266 }
267
268 #[test]
270 fn help() {
271 let args = [OsStr::new("btrfs-backup"), OsStr::new("--help")];
272 let () = run(args).unwrap();
273 }
274}