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