]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/pxar.rs
pxar: factor out PxarCreateOptions
[proxmox-backup.git] / src / bin / pxar.rs
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 }