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