]>
Commit | Line | Data |
---|---|---|
c443f58b | 1 | use std::collections::HashSet; |
f71e8cc9 | 2 | use std::ffi::OsStr; |
c443f58b | 3 | use std::fs::OpenOptions; |
af309d4d | 4 | use std::os::unix::fs::OpenOptionsExt; |
c443f58b | 5 | use std::path::{Path, PathBuf}; |
d9b8e2c7 WB |
6 | use std::sync::Arc; |
7 | use std::sync::atomic::{AtomicBool, Ordering}; | |
c60d34bd | 8 | |
d9b8e2c7 | 9 | use anyhow::{bail, format_err, Error}; |
c443f58b WB |
10 | use futures::future::FutureExt; |
11 | use futures::select; | |
12 | use tokio::signal::unix::{signal, SignalKind}; | |
c60d34bd | 13 | |
c443f58b | 14 | use pathpatterns::{MatchEntry, MatchType, PatternFlag}; |
2b7f8dd5 | 15 | use pbs_client::pxar::{fuse, format_single_line_entry, ENCODER_MAX_ENTRIES, Flags, PxarExtractOptions}; |
c60d34bd | 16 | |
c443f58b WB |
17 | use proxmox::api::cli::*; |
18 | use proxmox::api::api; | |
c60d34bd | 19 | |
129dda47 CE |
20 | fn extract_archive_from_reader<R: std::io::Read>( |
21 | reader: &mut R, | |
22 | target: &str, | |
5444fa94 | 23 | feature_flags: Flags, |
129dda47 | 24 | verbose: bool, |
72064fd0 | 25 | options: PxarExtractOptions, |
129dda47 | 26 | ) -> Result<(), Error> { |
2b7f8dd5 | 27 | pbs_client::pxar::extract_archive( |
c443f58b WB |
28 | pxar::decoder::Decoder::from_std(reader)?, |
29 | Path::new(target), | |
c443f58b | 30 | feature_flags, |
c443f58b WB |
31 | |path| { |
32 | if verbose { | |
33 | println!("{:?}", path); | |
34 | } | |
35 | }, | |
72064fd0 | 36 | options, |
c443f58b | 37 | ) |
9eae781a DM |
38 | } |
39 | ||
c443f58b WB |
40 | #[api( |
41 | input: { | |
42 | properties: { | |
43 | archive: { | |
44 | description: "Archive name.", | |
45 | }, | |
46 | pattern: { | |
47 | description: "List of paths or pattern matching files to restore", | |
48 | type: Array, | |
49 | items: { | |
50 | type: String, | |
51 | description: "Path or pattern matching files to restore.", | |
52 | }, | |
53 | optional: true, | |
54 | }, | |
55 | target: { | |
56 | description: "Target directory", | |
57 | optional: true, | |
58 | }, | |
59 | verbose: { | |
60 | description: "Verbose output.", | |
61 | optional: true, | |
62 | default: false, | |
63 | }, | |
64 | "no-xattrs": { | |
65 | description: "Ignore extended file attributes.", | |
66 | optional: true, | |
67 | default: false, | |
68 | }, | |
69 | "no-fcaps": { | |
70 | description: "Ignore file capabilities.", | |
71 | optional: true, | |
72 | default: false, | |
73 | }, | |
74 | "no-acls": { | |
75 | description: "Ignore access control list entries.", | |
76 | optional: true, | |
77 | default: false, | |
78 | }, | |
79 | "allow-existing-dirs": { | |
80 | description: "Allows directories to already exist on restore.", | |
81 | optional: true, | |
82 | default: false, | |
83 | }, | |
84 | "files-from": { | |
85 | description: "File containing match pattern for files to restore.", | |
86 | optional: true, | |
87 | }, | |
88 | "no-device-nodes": { | |
89 | description: "Ignore device nodes.", | |
90 | optional: true, | |
91 | default: false, | |
92 | }, | |
93 | "no-fifos": { | |
94 | description: "Ignore fifos.", | |
95 | optional: true, | |
96 | default: false, | |
97 | }, | |
98 | "no-sockets": { | |
99 | description: "Ignore sockets.", | |
100 | optional: true, | |
101 | default: false, | |
102 | }, | |
d9b8e2c7 WB |
103 | strict: { |
104 | description: "Stop on errors. Otherwise most errors will simply warn.", | |
105 | optional: true, | |
106 | default: false, | |
107 | }, | |
c443f58b WB |
108 | }, |
109 | }, | |
110 | )] | |
111 | /// Extract an archive. | |
367c0ff7 | 112 | #[allow(clippy::too_many_arguments)] |
1ef46b81 | 113 | fn extract_archive( |
c443f58b WB |
114 | archive: String, |
115 | pattern: Option<Vec<String>>, | |
116 | target: Option<String>, | |
117 | verbose: bool, | |
118 | no_xattrs: bool, | |
119 | no_fcaps: bool, | |
120 | no_acls: bool, | |
121 | allow_existing_dirs: bool, | |
122 | files_from: Option<String>, | |
123 | no_device_nodes: bool, | |
124 | no_fifos: bool, | |
125 | no_sockets: bool, | |
d9b8e2c7 | 126 | strict: bool, |
c443f58b | 127 | ) -> Result<(), Error> { |
5444fa94 | 128 | let mut feature_flags = Flags::DEFAULT; |
b344461b | 129 | if no_xattrs { |
591b120d | 130 | feature_flags.remove(Flags::WITH_XATTRS); |
b344461b CE |
131 | } |
132 | if no_fcaps { | |
591b120d | 133 | feature_flags.remove(Flags::WITH_FCAPS); |
b344461b | 134 | } |
9b384433 | 135 | if no_acls { |
591b120d | 136 | feature_flags.remove(Flags::WITH_ACL); |
9b384433 | 137 | } |
81a9905e | 138 | if no_device_nodes { |
591b120d | 139 | feature_flags.remove(Flags::WITH_DEVICE_NODES); |
81a9905e CE |
140 | } |
141 | if no_fifos { | |
591b120d | 142 | feature_flags.remove(Flags::WITH_FIFOS); |
81a9905e CE |
143 | } |
144 | if no_sockets { | |
591b120d | 145 | feature_flags.remove(Flags::WITH_SOCKETS); |
81a9905e | 146 | } |
1ef46b81 | 147 | |
c443f58b WB |
148 | let pattern = pattern.unwrap_or_else(Vec::new); |
149 | let target = target.as_ref().map_or_else(|| ".", String::as_str); | |
150 | ||
151 | let mut match_list = Vec::new(); | |
152 | if let Some(filename) = &files_from { | |
153 | for line in proxmox_backup::tools::file_get_non_comment_lines(filename)? { | |
154 | let line = line | |
155 | .map_err(|err| format_err!("error reading {}: {}", filename, err))?; | |
156 | match_list.push( | |
157 | MatchEntry::parse_pattern(line, PatternFlag::PATH_NAME, MatchType::Include) | |
158 | .map_err(|err| format_err!("bad pattern in file '{}': {}", filename, err))?, | |
159 | ); | |
a0ec687c CE |
160 | } |
161 | } | |
129dda47 | 162 | |
c443f58b WB |
163 | for entry in pattern { |
164 | match_list.push( | |
165 | MatchEntry::parse_pattern(entry, PatternFlag::PATH_NAME, MatchType::Include) | |
166 | .map_err(|err| format_err!("error in pattern: {}", err))?, | |
167 | ); | |
a0ec687c CE |
168 | } |
169 | ||
d44185c4 WB |
170 | let extract_match_default = match_list.is_empty(); |
171 | ||
d9b8e2c7 WB |
172 | let was_ok = Arc::new(AtomicBool::new(true)); |
173 | let on_error = if strict { | |
174 | // by default errors are propagated up | |
175 | None | |
176 | } else { | |
177 | let was_ok = Arc::clone(&was_ok); | |
178 | // otherwise we want to log them but not act on them | |
179 | Some(Box::new(move |err| { | |
180 | was_ok.store(false, Ordering::Release); | |
181 | eprintln!("error: {}", err); | |
182 | Ok(()) | |
183 | }) as Box<dyn FnMut(Error) -> Result<(), Error> + Send>) | |
184 | }; | |
185 | ||
72064fd0 FG |
186 | let options = PxarExtractOptions { |
187 | match_list: &match_list, | |
188 | allow_existing_dirs, | |
189 | extract_match_default, | |
190 | on_error, | |
191 | }; | |
192 | ||
9eae781a DM |
193 | if archive == "-" { |
194 | let stdin = std::io::stdin(); | |
195 | let mut reader = stdin.lock(); | |
c443f58b WB |
196 | extract_archive_from_reader( |
197 | &mut reader, | |
198 | &target, | |
199 | feature_flags, | |
c443f58b | 200 | verbose, |
72064fd0 | 201 | options, |
c443f58b | 202 | )?; |
9eae781a | 203 | } else { |
c443f58b WB |
204 | if verbose { |
205 | println!("PXAR extract: {}", archive); | |
206 | } | |
9eae781a DM |
207 | let file = std::fs::File::open(archive)?; |
208 | let mut reader = std::io::BufReader::new(file); | |
c443f58b WB |
209 | extract_archive_from_reader( |
210 | &mut reader, | |
211 | &target, | |
212 | feature_flags, | |
c443f58b | 213 | verbose, |
72064fd0 | 214 | options, |
c443f58b | 215 | )?; |
9eae781a | 216 | } |
1ef46b81 | 217 | |
d9b8e2c7 WB |
218 | if !was_ok.load(Ordering::Acquire) { |
219 | bail!("there were errors"); | |
220 | } | |
221 | ||
c443f58b | 222 | Ok(()) |
1ef46b81 DM |
223 | } |
224 | ||
c443f58b WB |
225 | #[api( |
226 | input: { | |
227 | properties: { | |
228 | archive: { | |
229 | description: "Archive name.", | |
230 | }, | |
231 | source: { | |
232 | description: "Source directory.", | |
233 | }, | |
234 | verbose: { | |
235 | description: "Verbose output.", | |
236 | optional: true, | |
237 | default: false, | |
238 | }, | |
239 | "no-xattrs": { | |
240 | description: "Ignore extended file attributes.", | |
241 | optional: true, | |
242 | default: false, | |
243 | }, | |
244 | "no-fcaps": { | |
245 | description: "Ignore file capabilities.", | |
246 | optional: true, | |
247 | default: false, | |
248 | }, | |
249 | "no-acls": { | |
250 | description: "Ignore access control list entries.", | |
251 | optional: true, | |
252 | default: false, | |
253 | }, | |
254 | "all-file-systems": { | |
255 | description: "Include mounted sudirs.", | |
256 | optional: true, | |
257 | default: false, | |
258 | }, | |
259 | "no-device-nodes": { | |
260 | description: "Ignore device nodes.", | |
261 | optional: true, | |
262 | default: false, | |
263 | }, | |
264 | "no-fifos": { | |
265 | description: "Ignore fifos.", | |
266 | optional: true, | |
267 | default: false, | |
268 | }, | |
269 | "no-sockets": { | |
270 | description: "Ignore sockets.", | |
271 | optional: true, | |
272 | default: false, | |
273 | }, | |
274 | exclude: { | |
275 | description: "List of paths or pattern matching files to exclude.", | |
276 | optional: true, | |
277 | type: Array, | |
278 | items: { | |
279 | description: "Path or pattern matching files to restore", | |
280 | type: String, | |
281 | }, | |
282 | }, | |
283 | "entries-max": { | |
284 | description: "Max number of entries loaded at once into memory", | |
285 | optional: true, | |
286 | default: ENCODER_MAX_ENTRIES as isize, | |
287 | minimum: 0, | |
288 | maximum: std::isize::MAX, | |
289 | }, | |
290 | }, | |
291 | }, | |
292 | )] | |
293 | /// Create a new .pxar archive. | |
72064fd0 | 294 | #[allow(clippy::too_many_arguments)] |
6afb60ab | 295 | async fn create_archive( |
c443f58b WB |
296 | archive: String, |
297 | source: String, | |
298 | verbose: bool, | |
299 | no_xattrs: bool, | |
300 | no_fcaps: bool, | |
301 | no_acls: bool, | |
302 | all_file_systems: bool, | |
303 | no_device_nodes: bool, | |
304 | no_fifos: bool, | |
305 | no_sockets: bool, | |
306 | exclude: Option<Vec<String>>, | |
307 | entries_max: isize, | |
308 | ) -> Result<(), Error> { | |
77486a60 | 309 | let patterns = { |
c443f58b | 310 | let input = exclude.unwrap_or_else(Vec::new); |
77486a60 | 311 | let mut patterns = Vec::with_capacity(input.len()); |
c443f58b | 312 | for entry in input { |
77486a60 | 313 | patterns.push( |
c443f58b WB |
314 | MatchEntry::parse_pattern(entry, PatternFlag::PATH_NAME, MatchType::Exclude) |
315 | .map_err(|err| format_err!("error in exclude pattern: {}", err))?, | |
316 | ); | |
317 | } | |
77486a60 | 318 | patterns |
c443f58b | 319 | }; |
02c7d8e5 | 320 | |
c443f58b WB |
321 | let device_set = if all_file_systems { |
322 | None | |
323 | } else { | |
324 | Some(HashSet::new()) | |
325 | }; | |
2eeaacb9 | 326 | |
2b7f8dd5 | 327 | let options = pbs_client::pxar::PxarCreateOptions { |
77486a60 FG |
328 | entries_max: entries_max as usize, |
329 | device_set, | |
330 | patterns, | |
331 | verbose, | |
332 | skip_lost_and_found: false, | |
333 | }; | |
334 | ||
335 | ||
1ef46b81 | 336 | let source = PathBuf::from(source); |
02c7d8e5 | 337 | |
c443f58b WB |
338 | let dir = nix::dir::Dir::open( |
339 | &source, | |
340 | nix::fcntl::OFlag::O_NOFOLLOW, | |
341 | nix::sys::stat::Mode::empty(), | |
342 | )?; | |
02c7d8e5 | 343 | |
af309d4d | 344 | let file = OpenOptions::new() |
02c7d8e5 DM |
345 | .create_new(true) |
346 | .write(true) | |
af309d4d | 347 | .mode(0o640) |
02c7d8e5 DM |
348 | .open(archive)?; |
349 | ||
c443f58b | 350 | let writer = std::io::BufWriter::with_capacity(1024 * 1024, file); |
5444fa94 | 351 | let mut feature_flags = Flags::DEFAULT; |
b344461b | 352 | if no_xattrs { |
591b120d | 353 | feature_flags.remove(Flags::WITH_XATTRS); |
b344461b CE |
354 | } |
355 | if no_fcaps { | |
591b120d | 356 | feature_flags.remove(Flags::WITH_FCAPS); |
b344461b | 357 | } |
9b384433 | 358 | if no_acls { |
591b120d | 359 | feature_flags.remove(Flags::WITH_ACL); |
9b384433 | 360 | } |
81a9905e | 361 | if no_device_nodes { |
591b120d | 362 | feature_flags.remove(Flags::WITH_DEVICE_NODES); |
81a9905e CE |
363 | } |
364 | if no_fifos { | |
591b120d | 365 | feature_flags.remove(Flags::WITH_FIFOS); |
81a9905e CE |
366 | } |
367 | if no_sockets { | |
591b120d | 368 | feature_flags.remove(Flags::WITH_SOCKETS); |
62d123e5 CE |
369 | } |
370 | ||
c443f58b | 371 | let writer = pxar::encoder::sync::StandardWriter::new(writer); |
2b7f8dd5 | 372 | pbs_client::pxar::create_archive( |
c443f58b WB |
373 | dir, |
374 | writer, | |
62d123e5 | 375 | feature_flags, |
6afb60ab | 376 | move |path| { |
c443f58b WB |
377 | if verbose { |
378 | println!("{:?}", path); | |
379 | } | |
380 | Ok(()) | |
381 | }, | |
c443f58b | 382 | None, |
77486a60 | 383 | options, |
6afb60ab | 384 | ).await?; |
c60d34bd | 385 | |
c443f58b | 386 | Ok(()) |
c60d34bd DM |
387 | } |
388 | ||
c443f58b WB |
389 | #[api( |
390 | input: { | |
391 | properties: { | |
392 | archive: { description: "Archive name." }, | |
393 | mountpoint: { description: "Mountpoint for the file system." }, | |
394 | verbose: { | |
395 | description: "Verbose output, running in the foreground (for debugging).", | |
396 | optional: true, | |
397 | default: false, | |
398 | }, | |
399 | }, | |
400 | }, | |
401 | )] | |
f71e8cc9 | 402 | /// Mount the archive to the provided mountpoint via FUSE. |
c443f58b WB |
403 | async fn mount_archive( |
404 | archive: String, | |
405 | mountpoint: String, | |
406 | verbose: bool, | |
407 | ) -> Result<(), Error> { | |
408 | let archive = Path::new(&archive); | |
409 | let mountpoint = Path::new(&mountpoint); | |
f71e8cc9 | 410 | let options = OsStr::new("ro,default_permissions"); |
c443f58b WB |
411 | |
412 | let session = fuse::Session::mount_path(&archive, &options, verbose, mountpoint) | |
413 | .await | |
f71e8cc9 | 414 | .map_err(|err| format_err!("pxar mount failed: {}", err))?; |
f71e8cc9 | 415 | |
c443f58b | 416 | let mut interrupt = signal(SignalKind::interrupt())?; |
f71e8cc9 | 417 | |
c443f58b WB |
418 | select! { |
419 | res = session.fuse() => res?, | |
420 | _ = interrupt.recv().fuse() => { | |
421 | if verbose { | |
422 | eprintln!("interrupted"); | |
423 | } | |
424 | } | |
425 | } | |
255f378a | 426 | |
c443f58b WB |
427 | Ok(()) |
428 | } | |
255f378a | 429 | |
c443f58b WB |
430 | #[api( |
431 | input: { | |
432 | properties: { | |
433 | archive: { | |
434 | description: "Archive name.", | |
435 | }, | |
436 | verbose: { | |
437 | description: "Verbose output.", | |
438 | optional: true, | |
439 | default: false, | |
440 | }, | |
441 | }, | |
442 | }, | |
443 | )] | |
444 | /// List the contents of an archive. | |
445 | fn dump_archive(archive: String, verbose: bool) -> Result<(), Error> { | |
446 | for entry in pxar::decoder::Decoder::open(archive)? { | |
447 | let entry = entry?; | |
255f378a | 448 | |
c443f58b WB |
449 | if verbose { |
450 | println!("{}", format_single_line_entry(&entry)); | |
451 | } else { | |
452 | println!("{:?}", entry.path()); | |
453 | } | |
454 | } | |
455 | Ok(()) | |
456 | } | |
255f378a | 457 | |
c60d34bd | 458 | fn main() { |
c60d34bd | 459 | let cmd_def = CliCommandMap::new() |
c443f58b WB |
460 | .insert( |
461 | "create", | |
462 | CliCommand::new(&API_METHOD_CREATE_ARCHIVE) | |
463 | .arg_param(&["archive", "source"]) | |
2b7f8dd5 WB |
464 | .completion_cb("archive", pbs_tools::fs::complete_file_name) |
465 | .completion_cb("source", pbs_tools::fs::complete_file_name), | |
c443f58b WB |
466 | ) |
467 | .insert( | |
468 | "extract", | |
469 | CliCommand::new(&API_METHOD_EXTRACT_ARCHIVE) | |
470 | .arg_param(&["archive", "target"]) | |
2b7f8dd5 WB |
471 | .completion_cb("archive", pbs_tools::fs::complete_file_name) |
472 | .completion_cb("target", pbs_tools::fs::complete_file_name) | |
473 | .completion_cb("files-from", pbs_tools::fs::complete_file_name), | |
c60d34bd | 474 | ) |
c443f58b WB |
475 | .insert( |
476 | "mount", | |
477 | CliCommand::new(&API_METHOD_MOUNT_ARCHIVE) | |
478 | .arg_param(&["archive", "mountpoint"]) | |
2b7f8dd5 WB |
479 | .completion_cb("archive", pbs_tools::fs::complete_file_name) |
480 | .completion_cb("mountpoint", pbs_tools::fs::complete_file_name), | |
f71e8cc9 | 481 | ) |
c443f58b WB |
482 | .insert( |
483 | "list", | |
484 | CliCommand::new(&API_METHOD_DUMP_ARCHIVE) | |
485 | .arg_param(&["archive"]) | |
2b7f8dd5 | 486 | .completion_cb("archive", pbs_tools::fs::complete_file_name), |
c60d34bd DM |
487 | ); |
488 | ||
7b22acd0 | 489 | let rpcenv = CliEnvironment::new(); |
c443f58b | 490 | run_cli_command(cmd_def, rpcenv, Some(|future| { |
d420962f | 491 | pbs_runtime::main(future) |
c443f58b | 492 | })); |
c60d34bd | 493 | } |