]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/pxar.rs
pxar: factor out PxarCreateOptions
[proxmox-backup.git] / src / bin / pxar.rs
CommitLineData
c443f58b 1use std::collections::HashSet;
f71e8cc9 2use std::ffi::OsStr;
c443f58b 3use std::fs::OpenOptions;
af309d4d 4use std::os::unix::fs::OpenOptionsExt;
c443f58b 5use std::path::{Path, PathBuf};
d9b8e2c7
WB
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
c60d34bd 8
d9b8e2c7 9use anyhow::{bail, format_err, Error};
c443f58b
WB
10use futures::future::FutureExt;
11use futures::select;
12use tokio::signal::unix::{signal, SignalKind};
c60d34bd 13
c443f58b 14use pathpatterns::{MatchEntry, MatchType, PatternFlag};
c60d34bd 15
c443f58b
WB
16use proxmox::api::cli::*;
17use proxmox::api::api;
c60d34bd 18
c443f58b 19use proxmox_backup::tools;
e97025ab 20use proxmox_backup::pxar::{fuse, format_single_line_entry, ENCODER_MAX_ENTRIES, ErrorHandler, Flags};
c60d34bd 21
129dda47
CE
22fn 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 120fn 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 300fn 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
408async 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.
450fn 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 463fn 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}