]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/pxar.rs
src/cli/command.rs: use static array for arg_param
[proxmox-backup.git] / src / bin / pxar.rs
1 extern crate proxmox_backup;
2
3 use failure::*;
4
5 use proxmox::{sortable, identity};
6 use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment};
7 use proxmox::api::schema::*;
8
9 use proxmox_backup::tools;
10 use proxmox_backup::cli::*;
11
12 use serde_json::{Value};
13
14 use std::io::Write;
15 use std::path::{Path, PathBuf};
16 use std::fs::OpenOptions;
17 use std::ffi::OsStr;
18 use std::os::unix::fs::OpenOptionsExt;
19 use std::os::unix::io::AsRawFd;
20 use std::collections::HashSet;
21
22 use proxmox_backup::pxar;
23
24 fn dump_archive_from_reader<R: std::io::Read>(
25 reader: &mut R,
26 feature_flags: u64,
27 verbose: bool,
28 ) -> Result<(), Error> {
29 let mut decoder = pxar::SequentialDecoder::new(reader, feature_flags, |_| Ok(()));
30
31 let stdout = std::io::stdout();
32 let mut out = stdout.lock();
33
34 let mut path = PathBuf::new();
35 decoder.dump_entry(&mut path, verbose, &mut out)?;
36
37 Ok(())
38 }
39
40 fn dump_archive(
41 param: Value,
42 _info: &ApiMethod,
43 _rpcenv: &mut dyn RpcEnvironment,
44 ) -> Result<Value, Error> {
45
46 let archive = tools::required_string_param(&param, "archive")?;
47 let verbose = param["verbose"].as_bool().unwrap_or(false);
48
49 let feature_flags = pxar::flags::DEFAULT;
50
51 if archive == "-" {
52 let stdin = std::io::stdin();
53 let mut reader = stdin.lock();
54 dump_archive_from_reader(&mut reader, feature_flags, verbose)?;
55 } else {
56 if verbose { println!("PXAR dump: {}", archive); }
57 let file = std::fs::File::open(archive)?;
58 let mut reader = std::io::BufReader::new(file);
59 dump_archive_from_reader(&mut reader, feature_flags, verbose)?;
60 }
61
62 Ok(Value::Null)
63 }
64
65 fn extract_archive_from_reader<R: std::io::Read>(
66 reader: &mut R,
67 target: &str,
68 feature_flags: u64,
69 allow_existing_dirs: bool,
70 verbose: bool,
71 pattern: Option<Vec<pxar::MatchPattern>>
72 ) -> Result<(), Error> {
73 let mut decoder = pxar::SequentialDecoder::new(reader, feature_flags, |path| {
74 if verbose {
75 println!("{:?}", path);
76 }
77 Ok(())
78 });
79 decoder.set_allow_existing_dirs(allow_existing_dirs);
80
81 let pattern = pattern.unwrap_or_else(Vec::new);
82 decoder.restore(Path::new(target), &pattern)?;
83
84 Ok(())
85 }
86
87 fn extract_archive(
88 param: Value,
89 _info: &ApiMethod,
90 _rpcenv: &mut dyn RpcEnvironment,
91 ) -> Result<Value, Error> {
92
93 let archive = tools::required_string_param(&param, "archive")?;
94 let target = param["target"].as_str().unwrap_or(".");
95 let verbose = param["verbose"].as_bool().unwrap_or(false);
96 let no_xattrs = param["no-xattrs"].as_bool().unwrap_or(false);
97 let no_fcaps = param["no-fcaps"].as_bool().unwrap_or(false);
98 let no_acls = param["no-acls"].as_bool().unwrap_or(false);
99 let no_device_nodes = param["no-device-nodes"].as_bool().unwrap_or(false);
100 let no_fifos = param["no-fifos"].as_bool().unwrap_or(false);
101 let no_sockets = param["no-sockets"].as_bool().unwrap_or(false);
102 let allow_existing_dirs = param["allow-existing-dirs"].as_bool().unwrap_or(false);
103 let files_from = param["files-from"].as_str();
104 let empty = Vec::new();
105 let arg_pattern = param["pattern"].as_array().unwrap_or(&empty);
106
107 let mut feature_flags = pxar::flags::DEFAULT;
108 if no_xattrs {
109 feature_flags ^= pxar::flags::WITH_XATTRS;
110 }
111 if no_fcaps {
112 feature_flags ^= pxar::flags::WITH_FCAPS;
113 }
114 if no_acls {
115 feature_flags ^= pxar::flags::WITH_ACL;
116 }
117 if no_device_nodes {
118 feature_flags ^= pxar::flags::WITH_DEVICE_NODES;
119 }
120 if no_fifos {
121 feature_flags ^= pxar::flags::WITH_FIFOS;
122 }
123 if no_sockets {
124 feature_flags ^= pxar::flags::WITH_SOCKETS;
125 }
126
127 let mut pattern_list = Vec::new();
128 if let Some(filename) = files_from {
129 let dir = nix::dir::Dir::open("./", nix::fcntl::OFlag::O_RDONLY, nix::sys::stat::Mode::empty())?;
130 if let Some((mut pattern, _, _)) = pxar::MatchPattern::from_file(dir.as_raw_fd(), filename)? {
131 pattern_list.append(&mut pattern);
132 }
133 }
134
135 for s in arg_pattern {
136 let l = s.as_str().ok_or_else(|| format_err!("Invalid pattern string slice"))?;
137 let p = pxar::MatchPattern::from_line(l.as_bytes())?
138 .ok_or_else(|| format_err!("Invalid match pattern in arguments"))?;
139 pattern_list.push(p);
140 }
141
142 let pattern = if pattern_list.is_empty() {
143 None
144 } else {
145 Some(pattern_list)
146 };
147
148 if archive == "-" {
149 let stdin = std::io::stdin();
150 let mut reader = stdin.lock();
151 extract_archive_from_reader(&mut reader, target, feature_flags, allow_existing_dirs, verbose, pattern)?;
152 } else {
153 if verbose { println!("PXAR extract: {}", archive); }
154 let file = std::fs::File::open(archive)?;
155 let mut reader = std::io::BufReader::new(file);
156 extract_archive_from_reader(&mut reader, target, feature_flags, allow_existing_dirs, verbose, pattern)?;
157 }
158
159 Ok(Value::Null)
160 }
161
162 fn create_archive(
163 param: Value,
164 _info: &ApiMethod,
165 _rpcenv: &mut dyn RpcEnvironment,
166 ) -> Result<Value, Error> {
167
168 let archive = tools::required_string_param(&param, "archive")?;
169 let source = tools::required_string_param(&param, "source")?;
170 let verbose = param["verbose"].as_bool().unwrap_or(false);
171 let all_file_systems = param["all-file-systems"].as_bool().unwrap_or(false);
172 let no_xattrs = param["no-xattrs"].as_bool().unwrap_or(false);
173 let no_fcaps = param["no-fcaps"].as_bool().unwrap_or(false);
174 let no_acls = param["no-acls"].as_bool().unwrap_or(false);
175 let no_device_nodes = param["no-device-nodes"].as_bool().unwrap_or(false);
176 let no_fifos = param["no-fifos"].as_bool().unwrap_or(false);
177 let no_sockets = param["no-sockets"].as_bool().unwrap_or(false);
178 let empty = Vec::new();
179 let exclude_pattern = param["exclude"].as_array().unwrap_or(&empty);
180
181 let devices = if all_file_systems { None } else { Some(HashSet::new()) };
182
183 let source = PathBuf::from(source);
184
185 let mut dir = nix::dir::Dir::open(
186 &source, nix::fcntl::OFlag::O_NOFOLLOW, nix::sys::stat::Mode::empty())?;
187
188 let file = OpenOptions::new()
189 .create_new(true)
190 .write(true)
191 .mode(0o640)
192 .open(archive)?;
193
194 let mut writer = std::io::BufWriter::with_capacity(1024*1024, file);
195 let mut feature_flags = pxar::flags::DEFAULT;
196 if no_xattrs {
197 feature_flags ^= pxar::flags::WITH_XATTRS;
198 }
199 if no_fcaps {
200 feature_flags ^= pxar::flags::WITH_FCAPS;
201 }
202 if no_acls {
203 feature_flags ^= pxar::flags::WITH_ACL;
204 }
205 if no_device_nodes {
206 feature_flags ^= pxar::flags::WITH_DEVICE_NODES;
207 }
208 if no_fifos {
209 feature_flags ^= pxar::flags::WITH_FIFOS;
210 }
211 if no_sockets {
212 feature_flags ^= pxar::flags::WITH_SOCKETS;
213 }
214
215 let mut pattern_list = Vec::new();
216 for s in exclude_pattern {
217 let l = s.as_str().ok_or_else(|| format_err!("Invalid pattern string slice"))?;
218 let p = pxar::MatchPattern::from_line(l.as_bytes())?
219 .ok_or_else(|| format_err!("Invalid match pattern in arguments"))?;
220 pattern_list.push(p);
221 }
222
223 let catalog = None::<&mut pxar::catalog::DummyCatalogWriter>;
224 pxar::Encoder::encode(
225 source,
226 &mut dir,
227 &mut writer,
228 catalog,
229 devices,
230 verbose,
231 false,
232 feature_flags,
233 pattern_list,
234 )?;
235
236 writer.flush()?;
237
238 Ok(Value::Null)
239 }
240
241 /// Mount the archive to the provided mountpoint via FUSE.
242 fn mount_archive(
243 param: Value,
244 _info: &ApiMethod,
245 _rpcenv: &mut dyn RpcEnvironment,
246 ) -> Result<Value, Error> {
247 let archive = tools::required_string_param(&param, "archive")?;
248 let mountpoint = tools::required_string_param(&param, "mountpoint")?;
249 let verbose = param["verbose"].as_bool().unwrap_or(false);
250 let no_mt = param["no-mt"].as_bool().unwrap_or(false);
251
252 let archive = Path::new(archive);
253 let mountpoint = Path::new(mountpoint);
254 let options = OsStr::new("ro,default_permissions");
255 let mut session = pxar::fuse::Session::new(&archive, &options, verbose)
256 .map_err(|err| format_err!("pxar mount failed: {}", err))?;
257 // Mount the session and deamonize if verbose is not set
258 session.mount(&mountpoint, !verbose)?;
259 session.run_loop(!no_mt)?;
260
261 Ok(Value::Null)
262 }
263
264 #[sortable]
265 const API_METHOD_CREATE_ARCHIVE: ApiMethod = ApiMethod::new(
266 &ApiHandler::Sync(&create_archive),
267 &ObjectSchema::new(
268 "Create new .pxar archive.",
269 &sorted!([
270 (
271 "archive",
272 false,
273 &StringSchema::new("Archive name").schema()
274 ),
275 (
276 "source",
277 false,
278 &StringSchema::new("Source directory.").schema()
279 ),
280 (
281 "verbose",
282 true,
283 &BooleanSchema::new("Verbose output.")
284 .default(false)
285 .schema()
286 ),
287 (
288 "no-xattrs",
289 true,
290 &BooleanSchema::new("Ignore extended file attributes.")
291 .default(false)
292 .schema()
293 ),
294 (
295 "no-fcaps",
296 true,
297 &BooleanSchema::new("Ignore file capabilities.")
298 .default(false)
299 .schema()
300 ),
301 (
302 "no-acls",
303 true,
304 &BooleanSchema::new("Ignore access control list entries.")
305 .default(false)
306 .schema()
307 ),
308 (
309 "all-file-systems",
310 true,
311 &BooleanSchema::new("Include mounted sudirs.")
312 .default(false)
313 .schema()
314 ),
315 (
316 "no-device-nodes",
317 true,
318 &BooleanSchema::new("Ignore device nodes.")
319 .default(false)
320 .schema()
321 ),
322 (
323 "no-fifos",
324 true,
325 &BooleanSchema::new("Ignore fifos.")
326 .default(false)
327 .schema()
328 ),
329 (
330 "no-sockets",
331 true,
332 &BooleanSchema::new("Ignore sockets.")
333 .default(false)
334 .schema()
335 ),
336 (
337 "exclude",
338 true,
339 &ArraySchema::new(
340 "List of paths or pattern matching files to exclude.",
341 &StringSchema::new("Path or pattern matching files to restore.").schema()
342 ).schema()
343 ),
344 ]),
345 )
346 );
347
348 #[sortable]
349 const API_METHOD_EXTRACT_ARCHIVE: ApiMethod = ApiMethod::new(
350 &ApiHandler::Sync(&extract_archive),
351 &ObjectSchema::new(
352 "Extract an archive.",
353 &sorted!([
354 (
355 "archive",
356 false,
357 &StringSchema::new("Archive name.").schema()
358 ),
359 (
360 "pattern",
361 true,
362 &ArraySchema::new(
363 "List of paths or pattern matching files to restore",
364 &StringSchema::new("Path or pattern matching files to restore.").schema()
365 ).schema()
366 ),
367 (
368 "target",
369 true,
370 &StringSchema::new("Target directory.").schema()
371 ),
372 (
373 "verbose",
374 true,
375 &BooleanSchema::new("Verbose output.")
376 .default(false)
377 .schema()
378 ),
379 (
380 "no-xattrs",
381 true,
382 &BooleanSchema::new("Ignore extended file attributes.")
383 .default(false)
384 .schema()
385 ),
386 (
387 "no-fcaps",
388 true,
389 &BooleanSchema::new("Ignore file capabilities.")
390 .default(false)
391 .schema()
392 ),
393 (
394 "no-acls",
395 true,
396 &BooleanSchema::new("Ignore access control list entries.")
397 .default(false)
398 .schema()
399 ),
400 (
401 "allow-existing-dirs",
402 true,
403 &BooleanSchema::new("Allows directories to already exist on restore.")
404 .default(false)
405 .schema()
406 ),
407 (
408 "files-from",
409 true,
410 &StringSchema::new("Match pattern for files to restore.").schema()
411 ),
412 (
413 "no-device-nodes",
414 true,
415 &BooleanSchema::new("Ignore device nodes.")
416 .default(false)
417 .schema()
418 ),
419 (
420 "no-fifos",
421 true,
422 &BooleanSchema::new("Ignore fifos.")
423 .default(false)
424 .schema()
425 ),
426 (
427 "no-sockets",
428 true,
429 &BooleanSchema::new("Ignore sockets.")
430 .default(false)
431 .schema()
432 ),
433 ]),
434 )
435 );
436
437 #[sortable]
438 const API_METHOD_MOUNT_ARCHIVE: ApiMethod = ApiMethod::new(
439 &ApiHandler::Sync(&mount_archive),
440 &ObjectSchema::new(
441 "Mount the archive as filesystem via FUSE.",
442 &sorted!([
443 (
444 "archive",
445 false,
446 &StringSchema::new("Archive name.").schema()
447 ),
448 (
449 "mountpoint",
450 false,
451 &StringSchema::new("Mountpoint for the filesystem root.").schema()
452 ),
453 (
454 "verbose",
455 true,
456 &BooleanSchema::new("Verbose output, keeps process running in foreground (for debugging).")
457 .default(false)
458 .schema()
459 ),
460 (
461 "no-mt",
462 true,
463 &BooleanSchema::new("Run in single threaded mode (for debugging).")
464 .default(false)
465 .schema()
466 ),
467 ]),
468 )
469 );
470
471 #[sortable]
472 const API_METHOD_DUMP_ARCHIVE: ApiMethod = ApiMethod::new(
473 &ApiHandler::Sync(&dump_archive),
474 &ObjectSchema::new(
475 "List the contents of an archive.",
476 &sorted!([
477 ( "archive", false, &StringSchema::new("Archive name.").schema()),
478 ( "verbose", true, &BooleanSchema::new("Verbose output.")
479 .default(false)
480 .schema()
481 ),
482 ])
483 )
484 );
485
486 fn main() {
487
488 let cmd_def = CliCommandMap::new()
489 .insert("create", CliCommand::new(&API_METHOD_CREATE_ARCHIVE)
490 .arg_param(&["archive", "source", "exclude"])
491 .completion_cb("archive", tools::complete_file_name)
492 .completion_cb("source", tools::complete_file_name)
493 .into()
494 )
495 .insert("extract", CliCommand::new(&API_METHOD_EXTRACT_ARCHIVE)
496 .arg_param(&["archive", "pattern"])
497 .completion_cb("archive", tools::complete_file_name)
498 .completion_cb("target", tools::complete_file_name)
499 .completion_cb("files-from", tools::complete_file_name)
500 .into()
501 )
502 .insert("mount", CliCommand::new(&API_METHOD_MOUNT_ARCHIVE)
503 .arg_param(&["archive", "mountpoint"])
504 .completion_cb("archive", tools::complete_file_name)
505 .completion_cb("mountpoint", tools::complete_file_name)
506 .into()
507 )
508 .insert("list", CliCommand::new(&API_METHOD_DUMP_ARCHIVE)
509 .arg_param(&["archive"])
510 .completion_cb("archive", tools::complete_file_name)
511 .into()
512 );
513
514 run_cli_command(cmd_def.into());
515 }