]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/pxar.rs
13677305492ace9a5068ebecc9fe8eb996479fa6
[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
7 use anyhow::{format_err, Error};
8 use futures::future::FutureExt;
9 use futures::select;
10 use tokio::signal::unix::{signal, SignalKind};
11
12 use pathpatterns::{MatchEntry, MatchType, PatternFlag};
13
14 use proxmox::api::cli::*;
15 use proxmox::api::api;
16
17 use proxmox_backup::tools;
18 use proxmox_backup::pxar::{flags, fuse, format_single_line_entry, ENCODER_MAX_ENTRIES};
19
20 fn extract_archive_from_reader<R: std::io::Read>(
21 reader: &mut R,
22 target: &str,
23 feature_flags: u64,
24 allow_existing_dirs: bool,
25 verbose: bool,
26 match_list: &[MatchEntry],
27 ) -> Result<(), Error> {
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 )
40 }
41
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.
109 fn extract_archive(
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;
124 if no_xattrs {
125 feature_flags ^= flags::WITH_XATTRS;
126 }
127 if no_fcaps {
128 feature_flags ^= flags::WITH_FCAPS;
129 }
130 if no_acls {
131 feature_flags ^= flags::WITH_ACL;
132 }
133 if no_device_nodes {
134 feature_flags ^= flags::WITH_DEVICE_NODES;
135 }
136 if no_fifos {
137 feature_flags ^= flags::WITH_FIFOS;
138 }
139 if no_sockets {
140 feature_flags ^= flags::WITH_SOCKETS;
141 }
142
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 );
155 }
156 }
157
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 );
163 }
164
165 if archive == "-" {
166 let stdin = std::io::stdin();
167 let mut reader = stdin.lock();
168 extract_archive_from_reader(
169 &mut reader,
170 &target,
171 feature_flags,
172 allow_existing_dirs,
173 verbose,
174 &match_list,
175 )?;
176 } else {
177 if verbose {
178 println!("PXAR extract: {}", archive);
179 }
180 let file = std::fs::File::open(archive)?;
181 let mut reader = std::io::BufReader::new(file);
182 extract_archive_from_reader(
183 &mut reader,
184 &target,
185 feature_flags,
186 allow_existing_dirs,
187 verbose,
188 &match_list,
189 )?;
190 }
191
192 Ok(())
193 }
194
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.
264 fn create_archive(
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 pattern_list = {
279 let input = exclude.unwrap_or_else(Vec::new);
280 let mut pattern_list = Vec::with_capacity(input.len());
281 for entry in input {
282 pattern_list.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 pattern_list
288 };
289
290 let device_set = if all_file_systems {
291 None
292 } else {
293 Some(HashSet::new())
294 };
295
296 let source = PathBuf::from(source);
297
298 let dir = nix::dir::Dir::open(
299 &source,
300 nix::fcntl::OFlag::O_NOFOLLOW,
301 nix::sys::stat::Mode::empty(),
302 )?;
303
304 let file = OpenOptions::new()
305 .create_new(true)
306 .write(true)
307 .mode(0o640)
308 .open(archive)?;
309
310 let writer = std::io::BufWriter::with_capacity(1024 * 1024, file);
311 let mut feature_flags = flags::DEFAULT;
312 if no_xattrs {
313 feature_flags ^= flags::WITH_XATTRS;
314 }
315 if no_fcaps {
316 feature_flags ^= flags::WITH_FCAPS;
317 }
318 if no_acls {
319 feature_flags ^= flags::WITH_ACL;
320 }
321 if no_device_nodes {
322 feature_flags ^= flags::WITH_DEVICE_NODES;
323 }
324 if no_fifos {
325 feature_flags ^= flags::WITH_FIFOS;
326 }
327 if no_sockets {
328 feature_flags ^= flags::WITH_SOCKETS;
329 }
330
331 let writer = pxar::encoder::sync::StandardWriter::new(writer);
332 proxmox_backup::pxar::create_archive(
333 dir,
334 writer,
335 pattern_list,
336 feature_flags,
337 device_set,
338 false,
339 |path| {
340 if verbose {
341 println!("{:?}", path);
342 }
343 Ok(())
344 },
345 entries_max as usize,
346 None,
347 )?;
348
349 Ok(())
350 }
351
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 )]
365 /// Mount the archive to the provided mountpoint via FUSE.
366 async 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);
373 let options = OsStr::new("ro,default_permissions");
374
375 let session = fuse::Session::mount_path(&archive, &options, verbose, mountpoint)
376 .await
377 .map_err(|err| format_err!("pxar mount failed: {}", err))?;
378
379 let mut interrupt = signal(SignalKind::interrupt())?;
380
381 select! {
382 res = session.fuse() => res?,
383 _ = interrupt.recv().fuse() => {
384 if verbose {
385 eprintln!("interrupted");
386 }
387 }
388 }
389
390 Ok(())
391 }
392
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.
408 fn dump_archive(archive: String, verbose: bool) -> Result<(), Error> {
409 for entry in pxar::decoder::Decoder::open(archive)? {
410 let entry = entry?;
411
412 if verbose {
413 println!("{}", format_single_line_entry(&entry));
414 } else {
415 println!("{:?}", entry.path());
416 }
417 }
418 Ok(())
419 }
420
421 fn main() {
422 let cmd_def = CliCommandMap::new()
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),
437 )
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),
444 )
445 .insert(
446 "list",
447 CliCommand::new(&API_METHOD_DUMP_ARCHIVE)
448 .arg_param(&["archive"])
449 .completion_cb("archive", tools::complete_file_name),
450 );
451
452 let rpcenv = CliEnvironment::new();
453 run_cli_command(cmd_def, rpcenv, Some(|future| {
454 proxmox_backup::tools::runtime::main(future)
455 }));
456 }