]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/pxar.rs
key: add fingerprint to key config
[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};
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<Box<dyn FnMut(Error) -> Result<(), Error> + Send>>,
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 pattern_list = {
315 let input = exclude.unwrap_or_else(Vec::new);
316 let mut pattern_list = Vec::with_capacity(input.len());
317 for entry in input {
318 pattern_list.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 pattern_list
324 };
325
326 let device_set = if all_file_systems {
327 None
328 } else {
329 Some(HashSet::new())
330 };
331
332 let source = PathBuf::from(source);
333
334 let dir = nix::dir::Dir::open(
335 &source,
336 nix::fcntl::OFlag::O_NOFOLLOW,
337 nix::sys::stat::Mode::empty(),
338 )?;
339
340 let file = OpenOptions::new()
341 .create_new(true)
342 .write(true)
343 .mode(0o640)
344 .open(archive)?;
345
346 let writer = std::io::BufWriter::with_capacity(1024 * 1024, file);
347 let mut feature_flags = Flags::DEFAULT;
348 if no_xattrs {
349 feature_flags ^= Flags::WITH_XATTRS;
350 }
351 if no_fcaps {
352 feature_flags ^= Flags::WITH_FCAPS;
353 }
354 if no_acls {
355 feature_flags ^= Flags::WITH_ACL;
356 }
357 if no_device_nodes {
358 feature_flags ^= Flags::WITH_DEVICE_NODES;
359 }
360 if no_fifos {
361 feature_flags ^= Flags::WITH_FIFOS;
362 }
363 if no_sockets {
364 feature_flags ^= Flags::WITH_SOCKETS;
365 }
366
367 let writer = pxar::encoder::sync::StandardWriter::new(writer);
368 proxmox_backup::pxar::create_archive(
369 dir,
370 writer,
371 pattern_list,
372 feature_flags,
373 device_set,
374 false,
375 |path| {
376 if verbose {
377 println!("{:?}", path);
378 }
379 Ok(())
380 },
381 entries_max as usize,
382 None,
383 )?;
384
385 Ok(())
386 }
387
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 )]
401 /// Mount the archive to the provided mountpoint via FUSE.
402 async 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);
409 let options = OsStr::new("ro,default_permissions");
410
411 let session = fuse::Session::mount_path(&archive, &options, verbose, mountpoint)
412 .await
413 .map_err(|err| format_err!("pxar mount failed: {}", err))?;
414
415 let mut interrupt = signal(SignalKind::interrupt())?;
416
417 select! {
418 res = session.fuse() => res?,
419 _ = interrupt.recv().fuse() => {
420 if verbose {
421 eprintln!("interrupted");
422 }
423 }
424 }
425
426 Ok(())
427 }
428
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.
444 fn dump_archive(archive: String, verbose: bool) -> Result<(), Error> {
445 for entry in pxar::decoder::Decoder::open(archive)? {
446 let entry = entry?;
447
448 if verbose {
449 println!("{}", format_single_line_entry(&entry));
450 } else {
451 println!("{:?}", entry.path());
452 }
453 }
454 Ok(())
455 }
456
457 fn main() {
458 let cmd_def = CliCommandMap::new()
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),
473 )
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),
480 )
481 .insert(
482 "list",
483 CliCommand::new(&API_METHOD_DUMP_ARCHIVE)
484 .arg_param(&["archive"])
485 .completion_cb("archive", tools::complete_file_name),
486 );
487
488 let rpcenv = CliEnvironment::new();
489 run_cli_command(cmd_def, rpcenv, Some(|future| {
490 proxmox_backup::tools::runtime::main(future)
491 }));
492 }