]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/pxar.rs
move client to pbs-client subcrate
[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};
2b7f8dd5 15use pbs_client::pxar::{fuse, format_single_line_entry, ENCODER_MAX_ENTRIES, Flags, PxarExtractOptions};
c60d34bd 16
c443f58b
WB
17use proxmox::api::cli::*;
18use proxmox::api::api;
c60d34bd 19
129dda47
CE
20fn extract_archive_from_reader<R: std::io::Read>(
21 reader: &mut R,
22 target: &str,
5444fa94 23 feature_flags: Flags,
129dda47 24 verbose: bool,
72064fd0 25 options: PxarExtractOptions,
129dda47 26) -> Result<(), Error> {
2b7f8dd5 27 pbs_client::pxar::extract_archive(
c443f58b
WB
28 pxar::decoder::Decoder::from_std(reader)?,
29 Path::new(target),
c443f58b 30 feature_flags,
c443f58b
WB
31 |path| {
32 if verbose {
33 println!("{:?}", path);
34 }
35 },
72064fd0 36 options,
c443f58b 37 )
9eae781a
DM
38}
39
c443f58b
WB
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 },
d9b8e2c7
WB
103 strict: {
104 description: "Stop on errors. Otherwise most errors will simply warn.",
105 optional: true,
106 default: false,
107 },
c443f58b
WB
108 },
109 },
110)]
111/// Extract an archive.
367c0ff7 112#[allow(clippy::too_many_arguments)]
1ef46b81 113fn extract_archive(
c443f58b
WB
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,
d9b8e2c7 126 strict: bool,
c443f58b 127) -> Result<(), Error> {
5444fa94 128 let mut feature_flags = Flags::DEFAULT;
b344461b 129 if no_xattrs {
591b120d 130 feature_flags.remove(Flags::WITH_XATTRS);
b344461b
CE
131 }
132 if no_fcaps {
591b120d 133 feature_flags.remove(Flags::WITH_FCAPS);
b344461b 134 }
9b384433 135 if no_acls {
591b120d 136 feature_flags.remove(Flags::WITH_ACL);
9b384433 137 }
81a9905e 138 if no_device_nodes {
591b120d 139 feature_flags.remove(Flags::WITH_DEVICE_NODES);
81a9905e
CE
140 }
141 if no_fifos {
591b120d 142 feature_flags.remove(Flags::WITH_FIFOS);
81a9905e
CE
143 }
144 if no_sockets {
591b120d 145 feature_flags.remove(Flags::WITH_SOCKETS);
81a9905e 146 }
1ef46b81 147
c443f58b
WB
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_backup::tools::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 );
a0ec687c
CE
160 }
161 }
129dda47 162
c443f58b
WB
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 );
a0ec687c
CE
168 }
169
d44185c4
WB
170 let extract_match_default = match_list.is_empty();
171
d9b8e2c7
WB
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
72064fd0
FG
186 let options = PxarExtractOptions {
187 match_list: &match_list,
188 allow_existing_dirs,
189 extract_match_default,
190 on_error,
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,
c443f58b 200 verbose,
72064fd0 201 options,
c443f58b 202 )?;
9eae781a 203 } else {
c443f58b
WB
204 if verbose {
205 println!("PXAR extract: {}", archive);
206 }
9eae781a
DM
207 let file = std::fs::File::open(archive)?;
208 let mut reader = std::io::BufReader::new(file);
c443f58b
WB
209 extract_archive_from_reader(
210 &mut reader,
211 &target,
212 feature_flags,
c443f58b 213 verbose,
72064fd0 214 options,
c443f58b 215 )?;
9eae781a 216 }
1ef46b81 217
d9b8e2c7
WB
218 if !was_ok.load(Ordering::Acquire) {
219 bail!("there were errors");
220 }
221
c443f58b 222 Ok(())
1ef46b81
DM
223}
224
c443f58b
WB
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: std::isize::MAX,
289 },
290 },
291 },
292)]
293/// Create a new .pxar archive.
72064fd0 294#[allow(clippy::too_many_arguments)]
6afb60ab 295async fn create_archive(
c443f58b
WB
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> {
77486a60 309 let patterns = {
c443f58b 310 let input = exclude.unwrap_or_else(Vec::new);
77486a60 311 let mut patterns = Vec::with_capacity(input.len());
c443f58b 312 for entry in input {
77486a60 313 patterns.push(
c443f58b
WB
314 MatchEntry::parse_pattern(entry, PatternFlag::PATH_NAME, MatchType::Exclude)
315 .map_err(|err| format_err!("error in exclude pattern: {}", err))?,
316 );
317 }
77486a60 318 patterns
c443f58b 319 };
02c7d8e5 320
c443f58b
WB
321 let device_set = if all_file_systems {
322 None
323 } else {
324 Some(HashSet::new())
325 };
2eeaacb9 326
2b7f8dd5 327 let options = pbs_client::pxar::PxarCreateOptions {
77486a60
FG
328 entries_max: entries_max as usize,
329 device_set,
330 patterns,
331 verbose,
332 skip_lost_and_found: false,
333 };
334
335
1ef46b81 336 let source = PathBuf::from(source);
02c7d8e5 337
c443f58b
WB
338 let dir = nix::dir::Dir::open(
339 &source,
340 nix::fcntl::OFlag::O_NOFOLLOW,
341 nix::sys::stat::Mode::empty(),
342 )?;
02c7d8e5 343
af309d4d 344 let file = OpenOptions::new()
02c7d8e5
DM
345 .create_new(true)
346 .write(true)
af309d4d 347 .mode(0o640)
02c7d8e5
DM
348 .open(archive)?;
349
c443f58b 350 let writer = std::io::BufWriter::with_capacity(1024 * 1024, file);
5444fa94 351 let mut feature_flags = Flags::DEFAULT;
b344461b 352 if no_xattrs {
591b120d 353 feature_flags.remove(Flags::WITH_XATTRS);
b344461b
CE
354 }
355 if no_fcaps {
591b120d 356 feature_flags.remove(Flags::WITH_FCAPS);
b344461b 357 }
9b384433 358 if no_acls {
591b120d 359 feature_flags.remove(Flags::WITH_ACL);
9b384433 360 }
81a9905e 361 if no_device_nodes {
591b120d 362 feature_flags.remove(Flags::WITH_DEVICE_NODES);
81a9905e
CE
363 }
364 if no_fifos {
591b120d 365 feature_flags.remove(Flags::WITH_FIFOS);
81a9905e
CE
366 }
367 if no_sockets {
591b120d 368 feature_flags.remove(Flags::WITH_SOCKETS);
62d123e5
CE
369 }
370
c443f58b 371 let writer = pxar::encoder::sync::StandardWriter::new(writer);
2b7f8dd5 372 pbs_client::pxar::create_archive(
c443f58b
WB
373 dir,
374 writer,
62d123e5 375 feature_flags,
6afb60ab 376 move |path| {
c443f58b
WB
377 if verbose {
378 println!("{:?}", path);
379 }
380 Ok(())
381 },
c443f58b 382 None,
77486a60 383 options,
6afb60ab 384 ).await?;
c60d34bd 385
c443f58b 386 Ok(())
c60d34bd
DM
387}
388
c443f58b
WB
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)]
f71e8cc9 402/// Mount the archive to the provided mountpoint via FUSE.
c443f58b
WB
403async 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);
f71e8cc9 410 let options = OsStr::new("ro,default_permissions");
c443f58b
WB
411
412 let session = fuse::Session::mount_path(&archive, &options, verbose, mountpoint)
413 .await
f71e8cc9 414 .map_err(|err| format_err!("pxar mount failed: {}", err))?;
f71e8cc9 415
c443f58b 416 let mut interrupt = signal(SignalKind::interrupt())?;
f71e8cc9 417
c443f58b
WB
418 select! {
419 res = session.fuse() => res?,
420 _ = interrupt.recv().fuse() => {
421 if verbose {
422 eprintln!("interrupted");
423 }
424 }
425 }
255f378a 426
c443f58b
WB
427 Ok(())
428}
255f378a 429
c443f58b
WB
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.
445fn dump_archive(archive: String, verbose: bool) -> Result<(), Error> {
446 for entry in pxar::decoder::Decoder::open(archive)? {
447 let entry = entry?;
255f378a 448
c443f58b
WB
449 if verbose {
450 println!("{}", format_single_line_entry(&entry));
451 } else {
452 println!("{:?}", entry.path());
453 }
454 }
455 Ok(())
456}
255f378a 457
c60d34bd 458fn main() {
c60d34bd 459 let cmd_def = CliCommandMap::new()
c443f58b
WB
460 .insert(
461 "create",
462 CliCommand::new(&API_METHOD_CREATE_ARCHIVE)
463 .arg_param(&["archive", "source"])
2b7f8dd5
WB
464 .completion_cb("archive", pbs_tools::fs::complete_file_name)
465 .completion_cb("source", pbs_tools::fs::complete_file_name),
c443f58b
WB
466 )
467 .insert(
468 "extract",
469 CliCommand::new(&API_METHOD_EXTRACT_ARCHIVE)
470 .arg_param(&["archive", "target"])
2b7f8dd5
WB
471 .completion_cb("archive", pbs_tools::fs::complete_file_name)
472 .completion_cb("target", pbs_tools::fs::complete_file_name)
473 .completion_cb("files-from", pbs_tools::fs::complete_file_name),
c60d34bd 474 )
c443f58b
WB
475 .insert(
476 "mount",
477 CliCommand::new(&API_METHOD_MOUNT_ARCHIVE)
478 .arg_param(&["archive", "mountpoint"])
2b7f8dd5
WB
479 .completion_cb("archive", pbs_tools::fs::complete_file_name)
480 .completion_cb("mountpoint", pbs_tools::fs::complete_file_name),
f71e8cc9 481 )
c443f58b
WB
482 .insert(
483 "list",
484 CliCommand::new(&API_METHOD_DUMP_ARCHIVE)
485 .arg_param(&["archive"])
2b7f8dd5 486 .completion_cb("archive", pbs_tools::fs::complete_file_name),
c60d34bd
DM
487 );
488
7b22acd0 489 let rpcenv = CliEnvironment::new();
c443f58b 490 run_cli_command(cmd_def, rpcenv, Some(|future| {
d420962f 491 pbs_runtime::main(future)
c443f58b 492 }));
c60d34bd 493}