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