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