]>
Commit | Line | Data |
---|---|---|
1 | use std::collections::HashSet; | |
2 | use std::ffi::OsStr; | |
3 | use std::fs::OpenOptions; | |
4 | use std::os::unix::fs::OpenOptionsExt; | |
5 | use std::path::{Path, PathBuf}; | |
6 | use std::sync::Arc; | |
7 | use std::sync::atomic::{AtomicBool, Ordering}; | |
8 | ||
9 | use anyhow::{bail, format_err, Error}; | |
10 | use futures::future::FutureExt; | |
11 | use futures::select; | |
12 | use tokio::signal::unix::{signal, SignalKind}; | |
13 | ||
14 | use pathpatterns::{MatchEntry, MatchType, PatternFlag}; | |
15 | ||
16 | use proxmox::api::cli::*; | |
17 | use proxmox::api::api; | |
18 | ||
19 | use proxmox_backup::tools; | |
20 | use proxmox_backup::pxar::{fuse, format_single_line_entry, ENCODER_MAX_ENTRIES, ErrorHandler, Flags}; | |
21 | ||
22 | fn extract_archive_from_reader<R: std::io::Read>( | |
23 | reader: &mut R, | |
24 | target: &str, | |
25 | feature_flags: Flags, | |
26 | allow_existing_dirs: bool, | |
27 | verbose: bool, | |
28 | match_list: &[MatchEntry], | |
29 | extract_match_default: bool, | |
30 | on_error: Option<ErrorHandler>, | |
31 | ) -> Result<(), Error> { | |
32 | proxmox_backup::pxar::extract_archive( | |
33 | pxar::decoder::Decoder::from_std(reader)?, | |
34 | Path::new(target), | |
35 | &match_list, | |
36 | extract_match_default, | |
37 | feature_flags, | |
38 | allow_existing_dirs, | |
39 | |path| { | |
40 | if verbose { | |
41 | println!("{:?}", path); | |
42 | } | |
43 | }, | |
44 | on_error, | |
45 | ) | |
46 | } | |
47 | ||
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 | }, | |
111 | strict: { | |
112 | description: "Stop on errors. Otherwise most errors will simply warn.", | |
113 | optional: true, | |
114 | default: false, | |
115 | }, | |
116 | }, | |
117 | }, | |
118 | )] | |
119 | /// Extract an archive. | |
120 | fn extract_archive( | |
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, | |
133 | strict: bool, | |
134 | ) -> Result<(), Error> { | |
135 | let mut feature_flags = Flags::DEFAULT; | |
136 | if no_xattrs { | |
137 | feature_flags ^= Flags::WITH_XATTRS; | |
138 | } | |
139 | if no_fcaps { | |
140 | feature_flags ^= Flags::WITH_FCAPS; | |
141 | } | |
142 | if no_acls { | |
143 | feature_flags ^= Flags::WITH_ACL; | |
144 | } | |
145 | if no_device_nodes { | |
146 | feature_flags ^= Flags::WITH_DEVICE_NODES; | |
147 | } | |
148 | if no_fifos { | |
149 | feature_flags ^= Flags::WITH_FIFOS; | |
150 | } | |
151 | if no_sockets { | |
152 | feature_flags ^= Flags::WITH_SOCKETS; | |
153 | } | |
154 | ||
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 | ); | |
167 | } | |
168 | } | |
169 | ||
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 | ); | |
175 | } | |
176 | ||
177 | let extract_match_default = match_list.is_empty(); | |
178 | ||
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 | ||
193 | if archive == "-" { | |
194 | let stdin = std::io::stdin(); | |
195 | let mut reader = stdin.lock(); | |
196 | extract_archive_from_reader( | |
197 | &mut reader, | |
198 | &target, | |
199 | feature_flags, | |
200 | allow_existing_dirs, | |
201 | verbose, | |
202 | &match_list, | |
203 | extract_match_default, | |
204 | on_error, | |
205 | )?; | |
206 | } else { | |
207 | if verbose { | |
208 | println!("PXAR extract: {}", archive); | |
209 | } | |
210 | let file = std::fs::File::open(archive)?; | |
211 | let mut reader = std::io::BufReader::new(file); | |
212 | extract_archive_from_reader( | |
213 | &mut reader, | |
214 | &target, | |
215 | feature_flags, | |
216 | allow_existing_dirs, | |
217 | verbose, | |
218 | &match_list, | |
219 | extract_match_default, | |
220 | on_error, | |
221 | )?; | |
222 | } | |
223 | ||
224 | if !was_ok.load(Ordering::Acquire) { | |
225 | bail!("there were errors"); | |
226 | } | |
227 | ||
228 | Ok(()) | |
229 | } | |
230 | ||
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. | |
300 | fn create_archive( | |
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> { | |
314 | let patterns = { | |
315 | let input = exclude.unwrap_or_else(Vec::new); | |
316 | let mut patterns = Vec::with_capacity(input.len()); | |
317 | for entry in input { | |
318 | patterns.push( | |
319 | MatchEntry::parse_pattern(entry, PatternFlag::PATH_NAME, MatchType::Exclude) | |
320 | .map_err(|err| format_err!("error in exclude pattern: {}", err))?, | |
321 | ); | |
322 | } | |
323 | patterns | |
324 | }; | |
325 | ||
326 | let device_set = if all_file_systems { | |
327 | None | |
328 | } else { | |
329 | Some(HashSet::new()) | |
330 | }; | |
331 | ||
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 | ||
341 | let source = PathBuf::from(source); | |
342 | ||
343 | let dir = nix::dir::Dir::open( | |
344 | &source, | |
345 | nix::fcntl::OFlag::O_NOFOLLOW, | |
346 | nix::sys::stat::Mode::empty(), | |
347 | )?; | |
348 | ||
349 | let file = OpenOptions::new() | |
350 | .create_new(true) | |
351 | .write(true) | |
352 | .mode(0o640) | |
353 | .open(archive)?; | |
354 | ||
355 | let writer = std::io::BufWriter::with_capacity(1024 * 1024, file); | |
356 | let mut feature_flags = Flags::DEFAULT; | |
357 | if no_xattrs { | |
358 | feature_flags ^= Flags::WITH_XATTRS; | |
359 | } | |
360 | if no_fcaps { | |
361 | feature_flags ^= Flags::WITH_FCAPS; | |
362 | } | |
363 | if no_acls { | |
364 | feature_flags ^= Flags::WITH_ACL; | |
365 | } | |
366 | if no_device_nodes { | |
367 | feature_flags ^= Flags::WITH_DEVICE_NODES; | |
368 | } | |
369 | if no_fifos { | |
370 | feature_flags ^= Flags::WITH_FIFOS; | |
371 | } | |
372 | if no_sockets { | |
373 | feature_flags ^= Flags::WITH_SOCKETS; | |
374 | } | |
375 | ||
376 | let writer = pxar::encoder::sync::StandardWriter::new(writer); | |
377 | proxmox_backup::pxar::create_archive( | |
378 | dir, | |
379 | writer, | |
380 | feature_flags, | |
381 | |path| { | |
382 | if verbose { | |
383 | println!("{:?}", path); | |
384 | } | |
385 | Ok(()) | |
386 | }, | |
387 | None, | |
388 | options, | |
389 | )?; | |
390 | ||
391 | Ok(()) | |
392 | } | |
393 | ||
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 | )] | |
407 | /// Mount the archive to the provided mountpoint via FUSE. | |
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); | |
415 | let options = OsStr::new("ro,default_permissions"); | |
416 | ||
417 | let session = fuse::Session::mount_path(&archive, &options, verbose, mountpoint) | |
418 | .await | |
419 | .map_err(|err| format_err!("pxar mount failed: {}", err))?; | |
420 | ||
421 | let mut interrupt = signal(SignalKind::interrupt())?; | |
422 | ||
423 | select! { | |
424 | res = session.fuse() => res?, | |
425 | _ = interrupt.recv().fuse() => { | |
426 | if verbose { | |
427 | eprintln!("interrupted"); | |
428 | } | |
429 | } | |
430 | } | |
431 | ||
432 | Ok(()) | |
433 | } | |
434 | ||
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?; | |
453 | ||
454 | if verbose { | |
455 | println!("{}", format_single_line_entry(&entry)); | |
456 | } else { | |
457 | println!("{:?}", entry.path()); | |
458 | } | |
459 | } | |
460 | Ok(()) | |
461 | } | |
462 | ||
463 | fn main() { | |
464 | let cmd_def = CliCommandMap::new() | |
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), | |
479 | ) | |
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), | |
486 | ) | |
487 | .insert( | |
488 | "list", | |
489 | CliCommand::new(&API_METHOD_DUMP_ARCHIVE) | |
490 | .arg_param(&["archive"]) | |
491 | .completion_cb("archive", tools::complete_file_name), | |
492 | ); | |
493 | ||
494 | let rpcenv = CliEnvironment::new(); | |
495 | run_cli_command(cmd_def, rpcenv, Some(|future| { | |
496 | proxmox_backup::tools::runtime::main(future) | |
497 | })); | |
498 | } |