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