]> git.proxmox.com Git - proxmox-backup.git/blob - pbs-client/src/catalog_shell.rs
dbc23ef642ba51dbaec233c3d309fd57417a5a5b
[proxmox-backup.git] / pbs-client / src / catalog_shell.rs
1 use std::collections::HashMap;
2 use std::ffi::{CStr, CString, OsStr, OsString};
3 use std::future::Future;
4 use std::io::Write;
5 use std::mem;
6 use std::ops::ControlFlow;
7 use std::os::unix::ffi::{OsStrExt, OsStringExt};
8 use std::path::{Path, PathBuf};
9 use std::pin::Pin;
10
11 use anyhow::{bail, format_err, Error};
12 use nix::dir::Dir;
13 use nix::fcntl::OFlag;
14 use nix::sys::stat::Mode;
15
16 use pathpatterns::{MatchEntry, MatchList, MatchPattern, MatchType, PatternFlag};
17 use proxmox::tools::fs::{create_path, CreateOptions};
18 use proxmox_router::cli::{self, CliCommand, CliCommandMap, CliHelper, CommandLineInterface};
19 use proxmox_schema::api;
20 use pxar::{EntryKind, Metadata};
21
22 use pbs_runtime::block_in_place;
23 use pbs_datastore::catalog::{self, DirEntryAttribute};
24
25 use crate::pxar::Flags;
26 use crate::pxar::fuse::{Accessor, FileEntry};
27
28 type CatalogReader = pbs_datastore::catalog::CatalogReader<std::fs::File>;
29
30 const MAX_SYMLINK_COUNT: usize = 40;
31
32 static mut SHELL: Option<usize> = None;
33
34 /// This list defines all the shell commands and their properties
35 /// using the api schema
36 pub fn catalog_shell_cli() -> CommandLineInterface {
37 CommandLineInterface::Nested(
38 CliCommandMap::new()
39 .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND))
40 .insert(
41 "cd",
42 CliCommand::new(&API_METHOD_CD_COMMAND)
43 .arg_param(&["path"])
44 .completion_cb("path", complete_path),
45 )
46 .insert(
47 "ls",
48 CliCommand::new(&API_METHOD_LS_COMMAND)
49 .arg_param(&["path"])
50 .completion_cb("path", complete_path),
51 )
52 .insert(
53 "stat",
54 CliCommand::new(&API_METHOD_STAT_COMMAND)
55 .arg_param(&["path"])
56 .completion_cb("path", complete_path),
57 )
58 .insert(
59 "select",
60 CliCommand::new(&API_METHOD_SELECT_COMMAND)
61 .arg_param(&["path"])
62 .completion_cb("path", complete_path),
63 )
64 .insert(
65 "deselect",
66 CliCommand::new(&API_METHOD_DESELECT_COMMAND)
67 .arg_param(&["path"])
68 .completion_cb("path", complete_path),
69 )
70 .insert(
71 "clear-selected",
72 CliCommand::new(&API_METHOD_CLEAR_SELECTED_COMMAND),
73 )
74 .insert(
75 "list-selected",
76 CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND),
77 )
78 .insert(
79 "restore-selected",
80 CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND)
81 .arg_param(&["target"])
82 .completion_cb("target", cli::complete_file_name),
83 )
84 .insert(
85 "restore",
86 CliCommand::new(&API_METHOD_RESTORE_COMMAND)
87 .arg_param(&["target"])
88 .completion_cb("target", cli::complete_file_name),
89 )
90 .insert(
91 "find",
92 CliCommand::new(&API_METHOD_FIND_COMMAND).arg_param(&["pattern"]),
93 )
94 .insert(
95 "exit",
96 CliCommand::new(&API_METHOD_EXIT),
97 )
98 .insert_help(),
99 )
100 }
101
102 fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
103 let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
104 match shell.complete_path(complete_me) {
105 Ok(list) => list,
106 Err(err) => {
107 eprintln!("error during completion: {}", err);
108 Vec::new()
109 }
110 }
111 }
112
113 // just an empty wrapper so that it is displayed in help/docs, we check
114 // in the readloop for 'exit' again break
115 #[api(input: { properties: {} })]
116 /// Exit the shell
117 async fn exit() -> Result<(), Error> {
118 Ok(())
119 }
120
121 #[api(input: { properties: {} })]
122 /// List the current working directory.
123 async fn pwd_command() -> Result<(), Error> {
124 Shell::with(move |shell| shell.pwd()).await
125 }
126
127 #[api(
128 input: {
129 properties: {
130 path: {
131 type: String,
132 optional: true,
133 description: "target path."
134 }
135 }
136 }
137 )]
138 /// Change the current working directory to the new directory
139 async fn cd_command(path: Option<String>) -> Result<(), Error> {
140 let path = path.as_ref().map(Path::new);
141 Shell::with(move |shell| shell.cd(path)).await
142 }
143
144 #[api(
145 input: {
146 properties: {
147 path: {
148 type: String,
149 optional: true,
150 description: "target path."
151 }
152 }
153 }
154 )]
155 /// List the content of working directory or given path.
156 async fn ls_command(path: Option<String>) -> Result<(), Error> {
157 let path = path.as_ref().map(Path::new);
158 Shell::with(move |shell| shell.ls(path)).await
159 }
160
161 #[api(
162 input: {
163 properties: {
164 path: {
165 type: String,
166 description: "target path."
167 }
168 }
169 }
170 )]
171 /// Read the metadata for a given directory entry.
172 ///
173 /// This is expensive because the data has to be read from the pxar archive, which means reading
174 /// over the network.
175 async fn stat_command(path: String) -> Result<(), Error> {
176 Shell::with(move |shell| shell.stat(PathBuf::from(path))).await
177 }
178
179 #[api(
180 input: {
181 properties: {
182 path: {
183 type: String,
184 description: "target path."
185 }
186 }
187 }
188 )]
189 /// Select an entry for restore.
190 ///
191 /// This will return an error if the entry is already present in the list or
192 /// if an invalid path was provided.
193 async fn select_command(path: String) -> Result<(), Error> {
194 Shell::with(move |shell| shell.select(PathBuf::from(path))).await
195 }
196
197 #[api(
198 input: {
199 properties: {
200 path: {
201 type: String,
202 description: "path to entry to remove from list."
203 }
204 }
205 }
206 )]
207 /// Deselect an entry for restore.
208 ///
209 /// This will return an error if the entry was not found in the list of entries
210 /// selected for restore.
211 async fn deselect_command(path: String) -> Result<(), Error> {
212 Shell::with(move |shell| shell.deselect(PathBuf::from(path))).await
213 }
214
215 #[api( input: { properties: { } })]
216 /// Clear the list of files selected for restore.
217 async fn clear_selected_command() -> Result<(), Error> {
218 Shell::with(move |shell| shell.deselect_all()).await
219 }
220
221 #[api(
222 input: {
223 properties: {
224 patterns: {
225 type: Boolean,
226 description: "List match patterns instead of the matching files.",
227 optional: true,
228 default: false,
229 }
230 }
231 }
232 )]
233 /// List entries currently selected for restore.
234 async fn list_selected_command(patterns: bool) -> Result<(), Error> {
235 Shell::with(move |shell| shell.list_selected(patterns)).await
236 }
237
238 #[api(
239 input: {
240 properties: {
241 pattern: {
242 type: String,
243 description: "Match pattern for matching files in the catalog."
244 },
245 select: {
246 type: bool,
247 optional: true,
248 default: false,
249 description: "Add matching filenames to list for restore."
250 }
251 }
252 }
253 )]
254 /// Find entries in the catalog matching the given match pattern.
255 async fn find_command(pattern: String, select: bool) -> Result<(), Error> {
256 Shell::with(move |shell| shell.find(pattern, select)).await
257 }
258
259 #[api(
260 input: {
261 properties: {
262 target: {
263 type: String,
264 description: "target path for restore on local filesystem."
265 }
266 }
267 }
268 )]
269 /// Restore the selected entries to the given target path.
270 ///
271 /// Target must not exist on the clients filesystem.
272 async fn restore_selected_command(target: String) -> Result<(), Error> {
273 Shell::with(move |shell| shell.restore_selected(PathBuf::from(target))).await
274 }
275
276 #[api(
277 input: {
278 properties: {
279 target: {
280 type: String,
281 description: "target path for restore on local filesystem."
282 },
283 pattern: {
284 type: String,
285 optional: true,
286 description: "match pattern to limit files for restore."
287 }
288 }
289 }
290 )]
291 /// Restore the sub-archive given by the current working directory to target.
292 ///
293 /// By further providing a pattern, the restore can be limited to a narrower
294 /// subset of this sub-archive.
295 /// If pattern is not present or empty, the full archive is restored to target.
296 async fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> {
297 Shell::with(move |shell| shell.restore(PathBuf::from(target), pattern)).await
298 }
299
300 /// TODO: Should we use this to fix `step()`? Make path resolution behave more like described in
301 /// the path_resolution(7) man page.
302 ///
303 /// The `Path` type's component iterator does not tell us anything about trailing slashes or
304 /// trailing `Component::CurDir` entries. Since we only support regular paths we'll roll our own
305 /// here:
306 enum PathComponent<'a> {
307 Root,
308 CurDir,
309 ParentDir,
310 Normal(&'a OsStr),
311 TrailingSlash,
312 }
313
314 struct PathComponentIter<'a> {
315 path: &'a [u8],
316 state: u8, // 0=beginning, 1=ongoing, 2=trailing, 3=finished (fused)
317 }
318
319 impl std::iter::FusedIterator for PathComponentIter<'_> {}
320
321 impl<'a> Iterator for PathComponentIter<'a> {
322 type Item = PathComponent<'a>;
323
324 fn next(&mut self) -> Option<Self::Item> {
325 if self.path.is_empty() {
326 return None;
327 }
328
329 if self.state == 0 {
330 self.state = 1;
331 if self.path[0] == b'/' {
332 // absolute path
333 self.path = &self.path[1..];
334 return Some(PathComponent::Root);
335 }
336 }
337
338 // skip slashes
339 let had_slashes = self.path[0] == b'/';
340 while self.path.get(0).copied() == Some(b'/') {
341 self.path = &self.path[1..];
342 }
343
344 Some(match self.path {
345 [] if had_slashes => PathComponent::TrailingSlash,
346 [] => return None,
347 [b'.'] | [b'.', b'/', ..] => {
348 self.path = &self.path[1..];
349 PathComponent::CurDir
350 }
351 [b'.', b'.'] | [b'.', b'.', b'/', ..] => {
352 self.path = &self.path[2..];
353 PathComponent::ParentDir
354 }
355 _ => {
356 let end = self
357 .path
358 .iter()
359 .position(|&b| b == b'/')
360 .unwrap_or(self.path.len());
361 let (out, rest) = self.path.split_at(end);
362 self.path = rest;
363 PathComponent::Normal(OsStr::from_bytes(out))
364 }
365 })
366 }
367 }
368
369 pub struct Shell {
370 /// Readline instance handling input and callbacks
371 rl: rustyline::Editor<CliHelper>,
372
373 /// Interactive prompt.
374 prompt: String,
375
376 /// Calalog reader instance to navigate
377 catalog: CatalogReader,
378
379 /// List of selected paths for restore
380 selected: HashMap<OsString, MatchEntry>,
381
382 /// pxar accessor instance for the current pxar archive
383 accessor: Accessor,
384
385 /// The current position in the archive.
386 position: Vec<PathStackEntry>,
387 }
388
389 #[derive(Clone)]
390 struct PathStackEntry {
391 /// This is always available. We mainly navigate through the catalog.
392 catalog: catalog::DirEntry,
393
394 /// Whenever we need something from the actual archive we fill this out. This is cached along
395 /// the entire path.
396 pxar: Option<FileEntry>,
397 }
398
399 impl PathStackEntry {
400 fn new(dir_entry: catalog::DirEntry) -> Self {
401 Self {
402 pxar: None,
403 catalog: dir_entry,
404 }
405 }
406 }
407
408 impl Shell {
409 /// Create a new shell for the given catalog and pxar archive.
410 pub async fn new(
411 mut catalog: CatalogReader,
412 archive_name: &str,
413 archive: Accessor,
414 ) -> Result<Self, Error> {
415 let cli_helper = CliHelper::new(catalog_shell_cli());
416 let mut rl = rustyline::Editor::<CliHelper>::new();
417 rl.set_helper(Some(cli_helper));
418
419 let catalog_root = catalog.root()?;
420 let archive_root = catalog
421 .lookup(&catalog_root, archive_name.as_bytes())?
422 .ok_or_else(|| format_err!("archive not found in catalog"))?;
423 let position = vec![PathStackEntry::new(archive_root)];
424
425 let mut this = Self {
426 rl,
427 prompt: String::new(),
428 catalog,
429 selected: HashMap::new(),
430 accessor: archive,
431 position,
432 };
433 this.update_prompt();
434 Ok(this)
435 }
436
437 async fn with<'a, Fut, R, F>(call: F) -> Result<R, Error>
438 where
439 F: FnOnce(&'a mut Shell) -> Fut,
440 Fut: Future<Output = Result<R, Error>>,
441 F: 'a,
442 Fut: 'a,
443 R: 'static,
444 {
445 let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
446 call(&mut *shell).await
447 }
448
449 pub async fn shell(mut self) -> Result<(), Error> {
450 let this = &mut self;
451 unsafe {
452 SHELL = Some(this as *mut Shell as usize);
453 }
454 while let Ok(line) = this.rl.readline(&this.prompt) {
455 if line == "exit" {
456 break;
457 }
458 let helper = this.rl.helper().unwrap();
459 let args = match cli::shellword_split(&line) {
460 Ok(args) => args,
461 Err(err) => {
462 println!("Error: {}", err);
463 continue;
464 }
465 };
466
467 let _ =
468 cli::handle_command_future(helper.cmd_def(), "", args, cli::CliEnvironment::new())
469 .await;
470 this.rl.add_history_entry(line);
471 this.update_prompt();
472 }
473 Ok(())
474 }
475
476 fn update_prompt(&mut self) {
477 self.prompt = "pxar:".to_string();
478 if self.position.len() <= 1 {
479 self.prompt.push('/');
480 } else {
481 for p in self.position.iter().skip(1) {
482 if !p.catalog.name.starts_with(b"/") {
483 self.prompt.push('/');
484 }
485 match std::str::from_utf8(&p.catalog.name) {
486 Ok(entry) => self.prompt.push_str(entry),
487 Err(_) => self.prompt.push_str("<non-utf8-dir>"),
488 }
489 }
490 }
491 self.prompt.push_str(" > ");
492 }
493
494 async fn pwd(&mut self) -> Result<(), Error> {
495 let stack = Self::lookup(
496 &self.position,
497 &mut self.catalog,
498 &self.accessor,
499 None,
500 &mut Some(0),
501 )
502 .await?;
503 let path = Self::format_path_stack(&stack);
504 println!("{:?}", path);
505 Ok(())
506 }
507
508 fn new_path_stack(&self) -> Vec<PathStackEntry> {
509 self.position[..1].to_vec()
510 }
511
512 async fn resolve_symlink(
513 stack: &mut Vec<PathStackEntry>,
514 catalog: &mut CatalogReader,
515 accessor: &Accessor,
516 follow_symlinks: &mut Option<usize>,
517 ) -> Result<(), Error> {
518 if let Some(ref mut symlink_count) = follow_symlinks {
519 *symlink_count += 1;
520 if *symlink_count > MAX_SYMLINK_COUNT {
521 bail!("too many levels of symbolic links");
522 }
523
524 let file = Self::walk_pxar_archive(accessor, &mut stack[..]).await?;
525
526 let path = match file.entry().kind() {
527 EntryKind::Symlink(symlink) => Path::new(symlink.as_os_str()),
528 _ => bail!("symlink in the catalog was not a symlink in the archive"),
529 };
530
531 let new_stack =
532 Self::lookup(&stack, &mut *catalog, accessor, Some(path), follow_symlinks).await?;
533
534 *stack = new_stack;
535
536 Ok(())
537 } else {
538 bail!("target is a symlink");
539 }
540 }
541
542 /// Walk a path and add it to the path stack.
543 ///
544 /// If the symlink count is used, symlinks will be followed, until we hit the cap and error
545 /// out.
546 async fn step(
547 stack: &mut Vec<PathStackEntry>,
548 catalog: &mut CatalogReader,
549 accessor: &Accessor,
550 component: std::path::Component<'_>,
551 follow_symlinks: &mut Option<usize>,
552 ) -> Result<(), Error> {
553 use std::path::Component;
554 match component {
555 Component::Prefix(_) => bail!("invalid path component (prefix)"),
556 Component::RootDir => stack.truncate(1),
557 Component::CurDir => {
558 if stack.last().unwrap().catalog.is_symlink() {
559 Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?;
560 }
561 }
562 Component::ParentDir => drop(stack.pop()),
563 Component::Normal(entry) => {
564 if stack.last().unwrap().catalog.is_symlink() {
565 Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?;
566 }
567 match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? {
568 Some(dir) => stack.push(PathStackEntry::new(dir)),
569 None => bail!("no such file or directory: {:?}", entry),
570 }
571 }
572 }
573
574 Ok(())
575 }
576
577 fn step_nofollow(
578 stack: &mut Vec<PathStackEntry>,
579 catalog: &mut CatalogReader,
580 component: std::path::Component<'_>,
581 ) -> Result<(), Error> {
582 use std::path::Component;
583 match component {
584 Component::Prefix(_) => bail!("invalid path component (prefix)"),
585 Component::RootDir => stack.truncate(1),
586 Component::CurDir => {
587 if stack.last().unwrap().catalog.is_symlink() {
588 bail!("target is a symlink");
589 }
590 }
591 Component::ParentDir => drop(stack.pop()),
592 Component::Normal(entry) => {
593 if stack.last().unwrap().catalog.is_symlink() {
594 bail!("target is a symlink");
595 } else {
596 match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? {
597 Some(dir) => stack.push(PathStackEntry::new(dir)),
598 None => bail!("no such file or directory: {:?}", entry),
599 }
600 }
601 }
602 }
603 Ok(())
604 }
605
606 /// The pxar accessor is required to resolve symbolic links
607 async fn walk_catalog(
608 stack: &mut Vec<PathStackEntry>,
609 catalog: &mut CatalogReader,
610 accessor: &Accessor,
611 path: &Path,
612 follow_symlinks: &mut Option<usize>,
613 ) -> Result<(), Error> {
614 for c in path.components() {
615 Self::step(stack, catalog, accessor, c, follow_symlinks).await?;
616 }
617 Ok(())
618 }
619
620 /// Non-async version cannot follow symlinks.
621 fn walk_catalog_nofollow(
622 stack: &mut Vec<PathStackEntry>,
623 catalog: &mut CatalogReader,
624 path: &Path,
625 ) -> Result<(), Error> {
626 for c in path.components() {
627 Self::step_nofollow(stack, catalog, c)?;
628 }
629 Ok(())
630 }
631
632 /// This assumes that there are no more symlinks in the path stack.
633 async fn walk_pxar_archive(
634 accessor: &Accessor,
635 mut stack: &mut [PathStackEntry],
636 ) -> Result<FileEntry, Error> {
637 if stack[0].pxar.is_none() {
638 stack[0].pxar = Some(accessor.open_root().await?.lookup_self().await?);
639 }
640
641 // Now walk the directory stack:
642 let mut at = 1;
643 while at < stack.len() {
644 if stack[at].pxar.is_some() {
645 at += 1;
646 continue;
647 }
648
649 let parent = stack[at - 1].pxar.as_ref().unwrap();
650 let dir = parent.enter_directory().await?;
651 let name = Path::new(OsStr::from_bytes(&stack[at].catalog.name));
652 stack[at].pxar = Some(
653 dir.lookup(name)
654 .await?
655 .ok_or_else(|| format_err!("no such entry in pxar file: {:?}", name))?,
656 );
657
658 at += 1;
659 }
660
661 Ok(stack.last().unwrap().pxar.clone().unwrap())
662 }
663
664 fn complete_path(&mut self, input: &str) -> Result<Vec<String>, Error> {
665 let mut tmp_stack;
666 let (parent, base, part) = match input.rfind('/') {
667 Some(ind) => {
668 let (base, part) = input.split_at(ind + 1);
669 let path = PathBuf::from(base);
670 if path.is_absolute() {
671 tmp_stack = self.new_path_stack();
672 } else {
673 tmp_stack = self.position.clone();
674 }
675 Self::walk_catalog_nofollow(&mut tmp_stack, &mut self.catalog, &path)?;
676 (&tmp_stack.last().unwrap().catalog, base, part)
677 }
678 None => (&self.position.last().unwrap().catalog, "", input),
679 };
680
681 let entries = self.catalog.read_dir(parent)?;
682
683 let mut out = Vec::new();
684 for entry in entries {
685 let mut name = base.to_string();
686 if entry.name.starts_with(part.as_bytes()) {
687 name.push_str(std::str::from_utf8(&entry.name)?);
688 if entry.is_directory() {
689 name.push('/');
690 }
691 out.push(name);
692 }
693 }
694
695 Ok(out)
696 }
697
698 // Break async recursion here: lookup -> walk_catalog -> step -> lookup
699 fn lookup<'future, 's, 'c, 'a, 'p, 'y>(
700 stack: &'s [PathStackEntry],
701 catalog: &'c mut CatalogReader,
702 accessor: &'a Accessor,
703 path: Option<&'p Path>,
704 follow_symlinks: &'y mut Option<usize>,
705 ) -> Pin<Box<dyn Future<Output = Result<Vec<PathStackEntry>, Error>> + Send + 'future>>
706 where
707 's: 'future,
708 'c: 'future,
709 'a: 'future,
710 'p: 'future,
711 'y: 'future,
712 {
713 Box::pin(async move {
714 Ok(match path {
715 None => stack.to_vec(),
716 Some(path) => {
717 let mut stack = if path.is_absolute() {
718 stack[..1].to_vec()
719 } else {
720 stack.to_vec()
721 };
722 Self::walk_catalog(&mut stack, catalog, accessor, path, follow_symlinks)
723 .await?;
724 stack
725 }
726 })
727 })
728 }
729
730 async fn ls(&mut self, path: Option<&Path>) -> Result<(), Error> {
731 let stack = Self::lookup(
732 &self.position,
733 &mut self.catalog,
734 &self.accessor,
735 path,
736 &mut Some(0),
737 )
738 .await?;
739
740 let last = stack.last().unwrap();
741 if last.catalog.is_directory() {
742 let items = self.catalog.read_dir(&stack.last().unwrap().catalog)?;
743 let mut out = std::io::stdout();
744 // FIXME: columnize
745 for item in items {
746 out.write_all(&item.name)?;
747 out.write_all(b"\n")?;
748 }
749 } else {
750 let mut out = std::io::stdout();
751 out.write_all(&last.catalog.name)?;
752 out.write_all(b"\n")?;
753 }
754 Ok(())
755 }
756
757 async fn stat(&mut self, path: PathBuf) -> Result<(), Error> {
758 let mut stack = Self::lookup(
759 &self.position,
760 &mut self.catalog,
761 &self.accessor,
762 Some(&path),
763 &mut Some(0),
764 )
765 .await?;
766
767 let file = Self::walk_pxar_archive(&self.accessor, &mut stack).await?;
768 std::io::stdout()
769 .write_all(crate::pxar::format_multi_line_entry(file.entry()).as_bytes())?;
770 Ok(())
771 }
772
773 async fn cd(&mut self, path: Option<&Path>) -> Result<(), Error> {
774 match path {
775 Some(path) => {
776 let new_position = Self::lookup(
777 &self.position,
778 &mut self.catalog,
779 &self.accessor,
780 Some(path),
781 &mut None,
782 )
783 .await?;
784 if !new_position.last().unwrap().catalog.is_directory() {
785 bail!("not a directory");
786 }
787 self.position = new_position;
788 }
789 None => self.position.truncate(1),
790 }
791 self.update_prompt();
792 Ok(())
793 }
794
795 /// This stack must have been canonicalized already!
796 fn format_path_stack(stack: &[PathStackEntry]) -> OsString {
797 if stack.len() <= 1 {
798 return OsString::from("/");
799 }
800
801 let mut out = OsString::new();
802 for c in stack.iter().skip(1) {
803 out.push("/");
804 out.push(OsStr::from_bytes(&c.catalog.name));
805 }
806
807 out
808 }
809
810 async fn select(&mut self, path: PathBuf) -> Result<(), Error> {
811 let stack = Self::lookup(
812 &self.position,
813 &mut self.catalog,
814 &self.accessor,
815 Some(&path),
816 &mut Some(0),
817 )
818 .await?;
819
820 let path = Self::format_path_stack(&stack);
821 let entry = MatchEntry::include(MatchPattern::Literal(path.as_bytes().to_vec()));
822 if self.selected.insert(path.clone(), entry).is_some() {
823 println!("path already selected: {:?}", path);
824 } else {
825 println!("added path: {:?}", path);
826 }
827
828 Ok(())
829 }
830
831 async fn deselect(&mut self, path: PathBuf) -> Result<(), Error> {
832 let stack = Self::lookup(
833 &self.position,
834 &mut self.catalog,
835 &self.accessor,
836 Some(&path),
837 &mut Some(0),
838 )
839 .await?;
840
841 let path = Self::format_path_stack(&stack);
842
843 if self.selected.remove(&path).is_some() {
844 println!("removed path from selection: {:?}", path);
845 } else {
846 println!("path not selected: {:?}", path);
847 }
848
849 Ok(())
850 }
851
852 async fn deselect_all(&mut self) -> Result<(), Error> {
853 self.selected.clear();
854 println!("cleared selection");
855 Ok(())
856 }
857
858 async fn list_selected(&mut self, patterns: bool) -> Result<(), Error> {
859 if patterns {
860 self.list_selected_patterns().await
861 } else {
862 self.list_matching_files().await
863 }
864 }
865
866 async fn list_selected_patterns(&self) -> Result<(), Error> {
867 for entry in self.selected.keys() {
868 println!("{:?}", entry);
869 }
870 Ok(())
871 }
872
873 fn build_match_list(&self) -> Vec<MatchEntry> {
874 let mut list = Vec::with_capacity(self.selected.len());
875 for entry in self.selected.values() {
876 list.push(entry.clone());
877 }
878 list
879 }
880
881 async fn list_matching_files(&mut self) -> Result<(), Error> {
882 let matches = self.build_match_list();
883
884 self.catalog.find(
885 &self.position[0].catalog,
886 &mut Vec::new(),
887 &matches,
888 &mut |path: &[u8]| -> Result<(), Error> {
889 let mut out = std::io::stdout();
890 out.write_all(path)?;
891 out.write_all(b"\n")?;
892 Ok(())
893 },
894 )?;
895
896 Ok(())
897 }
898
899 async fn find(&mut self, pattern: String, select: bool) -> Result<(), Error> {
900 let pattern_os = OsString::from(pattern.clone());
901 let pattern_entry =
902 MatchEntry::parse_pattern(pattern, PatternFlag::PATH_NAME, MatchType::Include)?;
903
904 let mut found_some = false;
905 self.catalog.find(
906 &self.position[0].catalog,
907 &mut Vec::new(),
908 &[&pattern_entry],
909 &mut |path: &[u8]| -> Result<(), Error> {
910 found_some = true;
911 let mut out = std::io::stdout();
912 out.write_all(path)?;
913 out.write_all(b"\n")?;
914 Ok(())
915 },
916 )?;
917
918 if found_some && select {
919 self.selected.insert(pattern_os, pattern_entry);
920 }
921
922 Ok(())
923 }
924
925 async fn restore_selected(&mut self, destination: PathBuf) -> Result<(), Error> {
926 if self.selected.is_empty() {
927 bail!("no entries selected");
928 }
929
930 let match_list = self.build_match_list();
931
932 self.restore_with_match_list(destination, &match_list).await
933 }
934
935 async fn restore(
936 &mut self,
937 destination: PathBuf,
938 pattern: Option<String>,
939 ) -> Result<(), Error> {
940 let tmp;
941 let match_list: &[MatchEntry] = match pattern {
942 None => &[],
943 Some(pattern) => {
944 tmp = [MatchEntry::parse_pattern(
945 pattern,
946 PatternFlag::PATH_NAME,
947 MatchType::Include,
948 )?];
949 &tmp
950 }
951 };
952
953 self.restore_with_match_list(destination, match_list).await
954 }
955
956 async fn restore_with_match_list(
957 &mut self,
958 destination: PathBuf,
959 match_list: &[MatchEntry],
960 ) -> Result<(), Error> {
961 create_path(
962 &destination,
963 None,
964 Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
965 )
966 .map_err(|err| format_err!("error creating directory {:?}: {}", destination, err))?;
967
968 let rootdir = Dir::open(
969 &destination,
970 OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
971 Mode::empty(),
972 )
973 .map_err(|err| {
974 format_err!("unable to open target directory {:?}: {}", destination, err,)
975 })?;
976
977 let mut dir_stack = self.new_path_stack();
978 Self::walk_pxar_archive(&self.accessor, &mut dir_stack).await?;
979 let root_meta = dir_stack
980 .last()
981 .unwrap()
982 .pxar
983 .as_ref()
984 .unwrap()
985 .entry()
986 .metadata()
987 .clone();
988
989 let extractor =
990 crate::pxar::extract::Extractor::new(rootdir, root_meta, true, Flags::DEFAULT);
991
992 let mut extractor = ExtractorState::new(
993 &mut self.catalog,
994 dir_stack,
995 extractor,
996 &match_list,
997 &self.accessor,
998 )?;
999
1000 extractor.extract().await
1001 }
1002 }
1003
1004 struct ExtractorState<'a> {
1005 path: Vec<u8>,
1006 path_len: usize,
1007 path_len_stack: Vec<usize>,
1008
1009 dir_stack: Vec<PathStackEntry>,
1010
1011 matches: bool,
1012 matches_stack: Vec<bool>,
1013
1014 read_dir: <Vec<catalog::DirEntry> as IntoIterator>::IntoIter,
1015 read_dir_stack: Vec<<Vec<catalog::DirEntry> as IntoIterator>::IntoIter>,
1016
1017 extractor: crate::pxar::extract::Extractor,
1018
1019 catalog: &'a mut CatalogReader,
1020 match_list: &'a [MatchEntry],
1021 accessor: &'a Accessor,
1022 }
1023
1024 impl<'a> ExtractorState<'a> {
1025 pub fn new(
1026 catalog: &'a mut CatalogReader,
1027 dir_stack: Vec<PathStackEntry>,
1028 extractor: crate::pxar::extract::Extractor,
1029 match_list: &'a [MatchEntry],
1030 accessor: &'a Accessor,
1031 ) -> Result<Self, Error> {
1032 let read_dir = catalog
1033 .read_dir(&dir_stack.last().unwrap().catalog)?
1034 .into_iter();
1035 Ok(Self {
1036 path: Vec::new(),
1037 path_len: 0,
1038 path_len_stack: Vec::new(),
1039
1040 dir_stack,
1041
1042 matches: match_list.is_empty(),
1043 matches_stack: Vec::new(),
1044
1045 read_dir,
1046 read_dir_stack: Vec::new(),
1047
1048 extractor,
1049
1050 catalog,
1051 match_list,
1052 accessor,
1053 })
1054 }
1055
1056 pub async fn extract(&mut self) -> Result<(), Error> {
1057 loop {
1058 let entry = match self.read_dir.next() {
1059 Some(entry) => entry,
1060 None => match self.handle_end_of_directory()? {
1061 ControlFlow::Break(()) => break, // done with root directory
1062 ControlFlow::Continue(()) => continue,
1063 },
1064 };
1065
1066 self.path.truncate(self.path_len);
1067 if !entry.name.starts_with(b"/") {
1068 self.path.reserve(entry.name.len() + 1);
1069 self.path.push(b'/');
1070 }
1071 self.path.extend(&entry.name);
1072
1073 self.extractor.set_path(OsString::from_vec(self.path.clone()));
1074 self.handle_entry(entry).await?;
1075 }
1076
1077 Ok(())
1078 }
1079
1080 fn handle_end_of_directory(&mut self) -> Result<ControlFlow<()>, Error> {
1081 // go up a directory:
1082 self.read_dir = match self.read_dir_stack.pop() {
1083 Some(r) => r,
1084 None => return Ok(ControlFlow::Break(())), // out of root directory
1085 };
1086
1087 self.matches = self
1088 .matches_stack
1089 .pop()
1090 .ok_or_else(|| format_err!("internal iterator error (matches_stack)"))?;
1091
1092 self.dir_stack
1093 .pop()
1094 .ok_or_else(|| format_err!("internal iterator error (dir_stack)"))?;
1095
1096 self.path_len = self
1097 .path_len_stack
1098 .pop()
1099 .ok_or_else(|| format_err!("internal iterator error (path_len_stack)"))?;
1100
1101 self.extractor.leave_directory()?;
1102
1103 Ok(ControlFlow::Continue(()))
1104 }
1105
1106 async fn handle_new_directory(
1107 &mut self,
1108 entry: catalog::DirEntry,
1109 match_result: Option<MatchType>,
1110 ) -> Result<(), Error> {
1111 // enter a new directory:
1112 self.read_dir_stack.push(mem::replace(
1113 &mut self.read_dir,
1114 self.catalog.read_dir(&entry)?.into_iter(),
1115 ));
1116 self.matches_stack.push(self.matches);
1117 self.dir_stack.push(PathStackEntry::new(entry));
1118 self.path_len_stack.push(self.path_len);
1119 self.path_len = self.path.len();
1120
1121 Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
1122 let dir_pxar = self.dir_stack.last().unwrap().pxar.as_ref().unwrap();
1123 let dir_meta = dir_pxar.entry().metadata().clone();
1124 let create = self.matches && match_result != Some(MatchType::Exclude);
1125 self.extractor.enter_directory(dir_pxar.file_name().to_os_string(), dir_meta, create)?;
1126
1127 Ok(())
1128 }
1129
1130 pub async fn handle_entry(&mut self, entry: catalog::DirEntry) -> Result<(), Error> {
1131 let match_result = self.match_list.matches(&self.path, entry.get_file_mode());
1132 let did_match = match match_result {
1133 Some(MatchType::Include) => true,
1134 Some(MatchType::Exclude) => false,
1135 None => self.matches,
1136 };
1137
1138 match (did_match, &entry.attr) {
1139 (_, DirEntryAttribute::Directory { .. }) => {
1140 self.handle_new_directory(entry, match_result).await?;
1141 }
1142 (true, DirEntryAttribute::File { .. }) => {
1143 self.dir_stack.push(PathStackEntry::new(entry));
1144 let file = Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
1145 self.extract_file(file).await?;
1146 self.dir_stack.pop();
1147 }
1148 (true, DirEntryAttribute::Symlink)
1149 | (true, DirEntryAttribute::BlockDevice)
1150 | (true, DirEntryAttribute::CharDevice)
1151 | (true, DirEntryAttribute::Fifo)
1152 | (true, DirEntryAttribute::Socket)
1153 | (true, DirEntryAttribute::Hardlink) => {
1154 let attr = entry.attr.clone();
1155 self.dir_stack.push(PathStackEntry::new(entry));
1156 let file = Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
1157 self.extract_special(file, attr).await?;
1158 self.dir_stack.pop();
1159 }
1160 (false, _) => (), // skip
1161 }
1162
1163 Ok(())
1164 }
1165
1166 fn path(&self) -> &OsStr {
1167 OsStr::from_bytes(&self.path)
1168 }
1169
1170 async fn extract_file(&mut self, entry: FileEntry) -> Result<(), Error> {
1171 match entry.kind() {
1172 pxar::EntryKind::File { size, .. } => {
1173 let file_name = CString::new(entry.file_name().as_bytes())?;
1174 let mut contents = entry.contents().await?;
1175 self.extractor.async_extract_file(
1176 &file_name,
1177 entry.metadata(),
1178 *size,
1179 &mut contents,
1180 )
1181 .await
1182 }
1183 _ => {
1184 bail!(
1185 "catalog file {:?} not a regular file in the archive",
1186 self.path()
1187 );
1188 }
1189 }
1190 }
1191
1192 async fn extract_special(
1193 &mut self,
1194 entry: FileEntry,
1195 catalog_attr: DirEntryAttribute,
1196 ) -> Result<(), Error> {
1197 let file_name = CString::new(entry.file_name().as_bytes())?;
1198 match (catalog_attr, entry.kind()) {
1199 (DirEntryAttribute::Symlink, pxar::EntryKind::Symlink(symlink)) => {
1200 block_in_place(|| self.extractor.extract_symlink(
1201 &file_name,
1202 entry.metadata(),
1203 symlink.as_os_str(),
1204 ))
1205 }
1206 (DirEntryAttribute::Symlink, _) => {
1207 bail!(
1208 "catalog symlink {:?} not a symlink in the archive",
1209 self.path()
1210 );
1211 }
1212
1213 (DirEntryAttribute::Hardlink, pxar::EntryKind::Hardlink(hardlink)) => {
1214 block_in_place(|| self.extractor.extract_hardlink(&file_name, hardlink.as_os_str()))
1215 }
1216 (DirEntryAttribute::Hardlink, _) => {
1217 bail!(
1218 "catalog hardlink {:?} not a hardlink in the archive",
1219 self.path()
1220 );
1221 }
1222
1223 (ref attr, pxar::EntryKind::Device(device)) => {
1224 self.extract_device(attr.clone(), &file_name, device, entry.metadata())
1225 }
1226
1227 (DirEntryAttribute::Fifo, pxar::EntryKind::Fifo) => {
1228 block_in_place(|| self.extractor.extract_special(&file_name, entry.metadata(), 0))
1229 }
1230 (DirEntryAttribute::Fifo, _) => {
1231 bail!("catalog fifo {:?} not a fifo in the archive", self.path());
1232 }
1233
1234 (DirEntryAttribute::Socket, pxar::EntryKind::Socket) => {
1235 block_in_place(|| self.extractor.extract_special(&file_name, entry.metadata(), 0))
1236 }
1237 (DirEntryAttribute::Socket, _) => {
1238 bail!(
1239 "catalog socket {:?} not a socket in the archive",
1240 self.path()
1241 );
1242 }
1243
1244 attr => bail!("unhandled file type {:?} for {:?}", attr, self.path()),
1245 }
1246 }
1247
1248 fn extract_device(
1249 &mut self,
1250 attr: DirEntryAttribute,
1251 file_name: &CStr,
1252 device: &pxar::format::Device,
1253 metadata: &Metadata,
1254 ) -> Result<(), Error> {
1255 match attr {
1256 DirEntryAttribute::BlockDevice => {
1257 if !metadata.stat.is_blockdev() {
1258 bail!(
1259 "catalog block device {:?} is not a block device in the archive",
1260 self.path(),
1261 );
1262 }
1263 }
1264 DirEntryAttribute::CharDevice => {
1265 if !metadata.stat.is_chardev() {
1266 bail!(
1267 "catalog character device {:?} is not a character device in the archive",
1268 self.path(),
1269 );
1270 }
1271 }
1272 _ => {
1273 bail!(
1274 "unexpected file type for {:?} in the catalog, \
1275 which is a device special file in the archive",
1276 self.path(),
1277 );
1278 }
1279 }
1280 block_in_place(|| self.extractor.extract_special(file_name, metadata, device.to_dev_t()))
1281 }
1282 }