]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/pxar.rs
key: add fingerprint to key config
[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;
5444fa94 20use proxmox_backup::pxar::{fuse, format_single_line_entry, ENCODER_MAX_ENTRIES, 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,
d9b8e2c7 30 on_error: Option<Box<dyn FnMut(Error) -> Result<(), Error> + Send>>,
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> {
239e49f9 314 let pattern_list = {
c443f58b 315 let input = exclude.unwrap_or_else(Vec::new);
239e49f9 316 let mut pattern_list = Vec::with_capacity(input.len());
c443f58b 317 for entry in input {
239e49f9 318 pattern_list.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 }
239e49f9 323 pattern_list
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
1ef46b81 332 let source = PathBuf::from(source);
02c7d8e5 333
c443f58b
WB
334 let dir = nix::dir::Dir::open(
335 &source,
336 nix::fcntl::OFlag::O_NOFOLLOW,
337 nix::sys::stat::Mode::empty(),
338 )?;
02c7d8e5 339
af309d4d 340 let file = OpenOptions::new()
02c7d8e5
DM
341 .create_new(true)
342 .write(true)
af309d4d 343 .mode(0o640)
02c7d8e5
DM
344 .open(archive)?;
345
c443f58b 346 let writer = std::io::BufWriter::with_capacity(1024 * 1024, file);
5444fa94 347 let mut feature_flags = Flags::DEFAULT;
b344461b 348 if no_xattrs {
5444fa94 349 feature_flags ^= Flags::WITH_XATTRS;
b344461b
CE
350 }
351 if no_fcaps {
5444fa94 352 feature_flags ^= Flags::WITH_FCAPS;
b344461b 353 }
9b384433 354 if no_acls {
5444fa94 355 feature_flags ^= Flags::WITH_ACL;
9b384433 356 }
81a9905e 357 if no_device_nodes {
5444fa94 358 feature_flags ^= Flags::WITH_DEVICE_NODES;
81a9905e
CE
359 }
360 if no_fifos {
5444fa94 361 feature_flags ^= Flags::WITH_FIFOS;
81a9905e
CE
362 }
363 if no_sockets {
5444fa94 364 feature_flags ^= Flags::WITH_SOCKETS;
62d123e5
CE
365 }
366
c443f58b
WB
367 let writer = pxar::encoder::sync::StandardWriter::new(writer);
368 proxmox_backup::pxar::create_archive(
369 dir,
370 writer,
239e49f9 371 pattern_list,
62d123e5 372 feature_flags,
c443f58b 373 device_set,
fc6047fc 374 false,
c443f58b
WB
375 |path| {
376 if verbose {
377 println!("{:?}", path);
378 }
379 Ok(())
380 },
6fc053ed 381 entries_max as usize,
c443f58b 382 None,
62d123e5 383 )?;
c60d34bd 384
c443f58b 385 Ok(())
c60d34bd
DM
386}
387
c443f58b
WB
388#[api(
389 input: {
390 properties: {
391 archive: { description: "Archive name." },
392 mountpoint: { description: "Mountpoint for the file system." },
393 verbose: {
394 description: "Verbose output, running in the foreground (for debugging).",
395 optional: true,
396 default: false,
397 },
398 },
399 },
400)]
f71e8cc9 401/// Mount the archive to the provided mountpoint via FUSE.
c443f58b
WB
402async fn mount_archive(
403 archive: String,
404 mountpoint: String,
405 verbose: bool,
406) -> Result<(), Error> {
407 let archive = Path::new(&archive);
408 let mountpoint = Path::new(&mountpoint);
f71e8cc9 409 let options = OsStr::new("ro,default_permissions");
c443f58b
WB
410
411 let session = fuse::Session::mount_path(&archive, &options, verbose, mountpoint)
412 .await
f71e8cc9 413 .map_err(|err| format_err!("pxar mount failed: {}", err))?;
f71e8cc9 414
c443f58b 415 let mut interrupt = signal(SignalKind::interrupt())?;
f71e8cc9 416
c443f58b
WB
417 select! {
418 res = session.fuse() => res?,
419 _ = interrupt.recv().fuse() => {
420 if verbose {
421 eprintln!("interrupted");
422 }
423 }
424 }
255f378a 425
c443f58b
WB
426 Ok(())
427}
255f378a 428
c443f58b
WB
429#[api(
430 input: {
431 properties: {
432 archive: {
433 description: "Archive name.",
434 },
435 verbose: {
436 description: "Verbose output.",
437 optional: true,
438 default: false,
439 },
440 },
441 },
442)]
443/// List the contents of an archive.
444fn dump_archive(archive: String, verbose: bool) -> Result<(), Error> {
445 for entry in pxar::decoder::Decoder::open(archive)? {
446 let entry = entry?;
255f378a 447
c443f58b
WB
448 if verbose {
449 println!("{}", format_single_line_entry(&entry));
450 } else {
451 println!("{:?}", entry.path());
452 }
453 }
454 Ok(())
455}
255f378a 456
c60d34bd 457fn main() {
c60d34bd 458 let cmd_def = CliCommandMap::new()
c443f58b
WB
459 .insert(
460 "create",
461 CliCommand::new(&API_METHOD_CREATE_ARCHIVE)
462 .arg_param(&["archive", "source"])
463 .completion_cb("archive", tools::complete_file_name)
464 .completion_cb("source", tools::complete_file_name),
465 )
466 .insert(
467 "extract",
468 CliCommand::new(&API_METHOD_EXTRACT_ARCHIVE)
469 .arg_param(&["archive", "target"])
470 .completion_cb("archive", tools::complete_file_name)
471 .completion_cb("target", tools::complete_file_name)
472 .completion_cb("files-from", tools::complete_file_name),
c60d34bd 473 )
c443f58b
WB
474 .insert(
475 "mount",
476 CliCommand::new(&API_METHOD_MOUNT_ARCHIVE)
477 .arg_param(&["archive", "mountpoint"])
478 .completion_cb("archive", tools::complete_file_name)
479 .completion_cb("mountpoint", tools::complete_file_name),
f71e8cc9 480 )
c443f58b
WB
481 .insert(
482 "list",
483 CliCommand::new(&API_METHOD_DUMP_ARCHIVE)
484 .arg_param(&["archive"])
485 .completion_cb("archive", tools::complete_file_name),
c60d34bd
DM
486 );
487
7b22acd0 488 let rpcenv = CliEnvironment::new();
c443f58b
WB
489 run_cli_command(cmd_def, rpcenv, Some(|future| {
490 proxmox_backup::tools::runtime::main(future)
491 }));
c60d34bd 492}