]> git.proxmox.com Git - proxmox-backup.git/blame - src/backup/catalog_shell.rs
switch from failure to anyhow
[proxmox-backup.git] / src / backup / catalog_shell.rs
CommitLineData
6934c6fe 1use std::cell::RefCell;
25cdd0e0 2use std::collections::HashMap;
03f779c6 3use std::convert::TryFrom;
6934c6fe 4use std::ffi::{CString, OsStr};
f14c96ea
CE
5use std::io::Write;
6use std::os::unix::ffi::OsStrExt;
32d192a9 7use std::path::{Component, Path, PathBuf};
f14c96ea 8
03f779c6 9use chrono::{Utc, offset::TimeZone};
f7d4e4b5 10use anyhow::{bail, format_err, Error};
03f779c6 11use nix::sys::stat::{Mode, SFlag};
f14c96ea 12
501f4fa2
DM
13use proxmox::api::{cli::*, *};
14use proxmox::sys::linux::tty;
15
6934c6fe 16use super::catalog::{CatalogReader, DirEntry};
f14c96ea 17use crate::pxar::*;
6934c6fe 18use crate::tools;
f14c96ea 19
f14c96ea 20
951cf17e 21const PROMPT_PREFIX: &str = "pxar:";
6934c6fe 22const PROMPT: &str = ">";
951cf17e
CE
23
24/// Interactive shell for interacton with the catalog.
f14c96ea 25pub struct Shell {
6934c6fe
CE
26 /// Readline instance handling input and callbacks
27 rl: rustyline::Editor<CliHelper>,
28 prompt: String,
951cf17e
CE
29}
30
ecbaa38f
DM
31/// This list defines all the shell commands and their properties
32/// using the api schema
55c3cb69 33pub fn catalog_shell_cli() -> CommandLineInterface {
ecbaa38f
DM
34
35 let map = CliCommandMap::new()
48ef3c33 36 .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND))
ecbaa38f
DM
37 .insert(
38 "cd",
39 CliCommand::new(&API_METHOD_CD_COMMAND)
40 .arg_param(&["path"])
41 .completion_cb("path", Shell::complete_path)
ecbaa38f
DM
42 )
43 .insert(
44 "ls",
45 CliCommand::new(&API_METHOD_LS_COMMAND)
46 .arg_param(&["path"])
47 .completion_cb("path", Shell::complete_path)
48ef3c33 48 )
ecbaa38f
DM
49 .insert(
50 "stat",
51 CliCommand::new(&API_METHOD_STAT_COMMAND)
52 .arg_param(&["path"])
53 .completion_cb("path", Shell::complete_path)
48ef3c33 54 )
ecbaa38f
DM
55 .insert(
56 "select",
57 CliCommand::new(&API_METHOD_SELECT_COMMAND)
58 .arg_param(&["path"])
59 .completion_cb("path", Shell::complete_path)
ecbaa38f
DM
60 )
61 .insert(
62 "deselect",
63 CliCommand::new(&API_METHOD_DESELECT_COMMAND)
64 .arg_param(&["path"])
65 .completion_cb("path", Shell::complete_path)
ecbaa38f 66 )
35ddf0b4
CE
67 .insert(
68 "clear-selected",
69 CliCommand::new(&API_METHOD_CLEAR_SELECTED_COMMAND)
70 )
ecbaa38f
DM
71 .insert(
72 "restore-selected",
73 CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND)
74 .arg_param(&["target"])
75 .completion_cb("target", tools::complete_file_name)
ecbaa38f
DM
76 )
77 .insert(
78 "list-selected",
48ef3c33 79 CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND),
ecbaa38f
DM
80 )
81 .insert(
82 "restore",
83 CliCommand::new(&API_METHOD_RESTORE_COMMAND)
84 .arg_param(&["target"])
85 .completion_cb("target", tools::complete_file_name)
ecbaa38f 86 )
25cdd0e0
CE
87 .insert(
88 "find",
89 CliCommand::new(&API_METHOD_FIND_COMMAND)
90 .arg_param(&["path", "pattern"])
91 .completion_cb("path", Shell::complete_path)
92 )
ecbaa38f
DM
93 .insert_help();
94
95 CommandLineInterface::Nested(map)
96}
97
951cf17e
CE
98impl Shell {
99 /// Create a new shell for the given catalog and pxar archive.
100 pub fn new(
6934c6fe 101 mut catalog: CatalogReader<std::fs::File>,
951cf17e
CE
102 archive_name: &str,
103 decoder: Decoder,
104 ) -> Result<Self, Error> {
6934c6fe
CE
105 let catalog_root = catalog.root()?;
106 // The root for the given archive as stored in the catalog
107 let archive_root = catalog.lookup(&catalog_root, archive_name.as_bytes())?;
32d192a9 108 let path = CatalogPathStack::new(archive_root);
6934c6fe
CE
109
110 CONTEXT.with(|handle| {
111 let mut ctx = handle.borrow_mut();
112 *ctx = Some(Context {
113 catalog,
25cdd0e0 114 selected: Vec::new(),
6934c6fe 115 decoder,
32d192a9 116 path,
6934c6fe
CE
117 });
118 });
119
55c3cb69 120 let cli_helper = CliHelper::new(catalog_shell_cli());
6934c6fe
CE
121 let mut rl = rustyline::Editor::<CliHelper>::new();
122 rl.set_helper(Some(cli_helper));
123
124 Context::with(|ctx| {
125 Ok(Self {
126 rl,
127 prompt: ctx.generate_prompt()?,
128 })
951cf17e
CE
129 })
130 }
131
132 /// Start the interactive shell loop
133 pub fn shell(mut self) -> Result<(), Error> {
6934c6fe
CE
134 while let Ok(line) = self.rl.readline(&self.prompt) {
135 let helper = self.rl.helper().unwrap();
136 let args = match shellword_split(&line) {
137 Ok(args) => args,
951cf17e 138 Err(err) => {
6934c6fe 139 println!("Error: {}", err);
951cf17e
CE
140 continue;
141 }
142 };
d08bc483 143 let _ = handle_command(helper.cmd_def(), "", args, None);
6934c6fe
CE
144 self.rl.add_history_entry(line);
145 self.update_prompt()?;
951cf17e
CE
146 }
147 Ok(())
148 }
951cf17e 149
6934c6fe
CE
150 /// Update the prompt to the new working directory
151 fn update_prompt(&mut self) -> Result<(), Error> {
152 Context::with(|ctx| {
153 self.prompt = ctx.generate_prompt()?;
154 Ok(())
155 })
951cf17e
CE
156 }
157
6934c6fe
CE
158 /// Completions for paths by lookup in the catalog
159 fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
160 Context::with(|ctx| {
161 let (base, to_complete) = match complete_me.rfind('/') {
162 // Split at ind + 1 so the slash remains on base, ok also if
163 // ends in slash as split_at accepts up to length as index.
164 Some(ind) => complete_me.split_at(ind + 1),
165 None => ("", complete_me),
166 };
951cf17e 167
6934c6fe 168 let current = if base.is_empty() {
32d192a9 169 ctx.path.last().clone()
6934c6fe 170 } else {
32d192a9
CE
171 let mut local = ctx.path.clone();
172 local.traverse(&PathBuf::from(base), &mut ctx.decoder, &mut ctx.catalog, false)?;
173 local.last().clone()
6934c6fe 174 };
951cf17e 175
32d192a9 176 let entries = match ctx.catalog.read_dir(&current) {
6934c6fe
CE
177 Ok(entries) => entries,
178 Err(_) => return Ok(Vec::new()),
179 };
180
181 let mut list = Vec::new();
182 for entry in &entries {
183 let mut name = String::from(base);
184 if entry.name.starts_with(to_complete.as_bytes()) {
185 name.push_str(std::str::from_utf8(&entry.name)?);
186 if entry.is_directory() {
187 name.push('/');
951cf17e 188 }
6934c6fe 189 list.push(name);
951cf17e 190 }
951cf17e 191 }
6934c6fe
CE
192 Ok(list)
193 })
194 .unwrap_or_default()
951cf17e
CE
195 }
196}
197
6934c6fe
CE
198#[api(input: { properties: {} })]
199/// List the current working directory.
200fn pwd_command() -> Result<(), Error> {
201 Context::with(|ctx| {
32d192a9 202 let path = ctx.path.generate_cstring()?;
6934c6fe
CE
203 let mut out = std::io::stdout();
204 out.write_all(&path.as_bytes())?;
205 out.write_all(&[b'\n'])?;
206 out.flush()?;
207 Ok(())
208 })
951cf17e
CE
209}
210
6934c6fe
CE
211#[api(
212 input: {
213 properties: {
214 path: {
215 type: String,
216 optional: true,
217 description: "target path."
218 }
219 }
951cf17e 220 }
6934c6fe
CE
221)]
222/// Change the current working directory to the new directory
223fn cd_command(path: Option<String>) -> Result<(), Error> {
224 Context::with(|ctx| {
225 let path = path.unwrap_or_default();
32d192a9
CE
226 if path.is_empty() {
227 ctx.path.clear();
228 return Ok(());
229 }
230 let mut local = ctx.path.clone();
231 local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, true)?;
232 if !local.last().is_directory() {
233 local.pop();
6934c6fe
CE
234 eprintln!("not a directory, fallback to parent directory");
235 }
32d192a9 236 ctx.path = local;
6934c6fe
CE
237 Ok(())
238 })
951cf17e
CE
239}
240
6934c6fe
CE
241#[api(
242 input: {
243 properties: {
244 path: {
245 type: String,
246 optional: true,
247 description: "target path."
248 }
951cf17e
CE
249 }
250 }
6934c6fe
CE
251)]
252/// List the content of working directory or given path.
253fn ls_command(path: Option<String>) -> Result<(), Error> {
254 Context::with(|ctx| {
32d192a9
CE
255 let parent = if let Some(ref path) = path {
256 let mut local = ctx.path.clone();
257 local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, false)?;
258 local.last().clone()
6934c6fe 259 } else {
32d192a9 260 ctx.path.last().clone()
6934c6fe 261 };
951cf17e 262
6934c6fe
CE
263 let list = if parent.is_directory() {
264 ctx.catalog.read_dir(&parent)?
951cf17e 265 } else {
32d192a9 266 vec![parent.clone()]
6934c6fe
CE
267 };
268
269 if list.is_empty() {
270 return Ok(());
951cf17e 271 }
6934c6fe
CE
272 let max = list.iter().max_by(|x, y| x.name.len().cmp(&y.name.len()));
273 let max = match max {
274 Some(dir_entry) => dir_entry.name.len() + 1,
275 None => 0,
276 };
951cf17e 277
501f4fa2 278 let (_rows, mut cols) = tty::stdout_terminal_size();
6934c6fe 279 cols /= max;
951cf17e 280
6934c6fe
CE
281 let mut out = std::io::stdout();
282 for (index, item) in list.iter().enumerate() {
283 out.write_all(&item.name)?;
284 // Fill with whitespaces
285 out.write_all(&vec![b' '; max - item.name.len()])?;
286 if index % cols == (cols - 1) {
287 out.write_all(&[b'\n'])?;
951cf17e 288 }
951cf17e 289 }
6934c6fe
CE
290 // If the last line is not complete, add the newline
291 if list.len() % cols != cols - 1 {
292 out.write_all(&[b'\n'])?;
951cf17e 293 }
6934c6fe
CE
294 out.flush()?;
295 Ok(())
296 })
297}
298
299#[api(
300 input: {
301 properties: {
302 path: {
303 type: String,
304 description: "target path."
305 }
951cf17e 306 }
951cf17e 307 }
6934c6fe
CE
308)]
309/// Read the metadata for a given directory entry.
310///
311/// This is expensive because the data has to be read from the pxar `Decoder`,
312/// which means reading over the network.
313fn stat_command(path: String) -> Result<(), Error> {
314 Context::with(|ctx| {
32d192a9
CE
315 let mut local = ctx.path.clone();
316 local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, false)?;
317 let canonical = local.canonical(&mut ctx.decoder, &mut ctx.catalog, false)?;
318 let item = canonical.lookup(&mut ctx.decoder)?;
6934c6fe 319 let mut out = std::io::stdout();
03f779c6 320 out.write_all(b" File:\t")?;
6934c6fe 321 out.write_all(item.filename.as_bytes())?;
03f779c6
CE
322 out.write_all(b"\n")?;
323 out.write_all(format!(" Size:\t{}\t\t", item.size).as_bytes())?;
324 out.write_all(b"Type:\t")?;
325
326 let mut mode_out = vec![b'-'; 10];
327 match SFlag::from_bits_truncate(item.entry.mode as u32) {
328 SFlag::S_IFDIR => {
329 mode_out[0] = b'd';
330 out.write_all(b"directory\n")?;
331 }
332 SFlag::S_IFREG => {
333 mode_out[0] = b'-';
334 out.write_all(b"regular file\n")?;
335 }
336 SFlag::S_IFLNK => {
337 mode_out[0] = b'l';
338 out.write_all(b"symbolic link\n")?;
339 }
340 SFlag::S_IFBLK => {
341 mode_out[0] = b'b';
342 out.write_all(b"block special file\n")?;
343 }
344 SFlag::S_IFCHR => {
345 mode_out[0] = b'c';
346 out.write_all(b"character special file\n")?;
347 }
6934c6fe
CE
348 _ => out.write_all(b"unknown\n")?,
349 };
03f779c6
CE
350
351 let mode = Mode::from_bits_truncate(item.entry.mode as u32);
352 if mode.contains(Mode::S_IRUSR) {
353 mode_out[1] = b'r';
354 }
355 if mode.contains(Mode::S_IWUSR) {
356 mode_out[2] = b'w';
357 }
358 match (mode.contains(Mode::S_IXUSR), mode.contains(Mode::S_ISUID)) {
359 (false, false) => mode_out[3] = b'-',
360 (true, false) => mode_out[3] = b'x',
361 (false, true) => mode_out[3] = b'S',
362 (true, true) => mode_out[3] = b's',
363 }
364
365 if mode.contains(Mode::S_IRGRP) {
366 mode_out[4] = b'r';
367 }
368 if mode.contains(Mode::S_IWGRP) {
369 mode_out[5] = b'w';
370 }
371 match (mode.contains(Mode::S_IXGRP), mode.contains(Mode::S_ISGID)) {
372 (false, false) => mode_out[6] = b'-',
373 (true, false) => mode_out[6] = b'x',
374 (false, true) => mode_out[6] = b'S',
375 (true, true) => mode_out[6] = b's',
376 }
377
378 if mode.contains(Mode::S_IROTH) {
379 mode_out[7] = b'r';
380 }
381 if mode.contains(Mode::S_IWOTH) {
382 mode_out[8] = b'w';
383 }
384 match (mode.contains(Mode::S_IXOTH), mode.contains(Mode::S_ISVTX)) {
385 (false, false) => mode_out[9] = b'-',
386 (true, false) => mode_out[9] = b'x',
387 (false, true) => mode_out[9] = b'T',
388 (true, true) => mode_out[9] = b't',
389 }
390
391 if !item.xattr.xattrs.is_empty() {
392 mode_out.push(b'+');
393 }
394
395 out.write_all(b"Access:\t")?;
396 out.write_all(&mode_out)?;
397 out.write_all(b"\t")?;
398 out.write_all(format!(" Uid:\t{}\t", item.entry.uid).as_bytes())?;
399 out.write_all(format!("Gid:\t{}\n", item.entry.gid).as_bytes())?;
400
401 let time = i64::try_from(item.entry.mtime)?;
402 let sec = time / 1_000_000_000;
403 let nsec = u32::try_from(time % 1_000_000_000)?;
404 let dt = Utc.timestamp(sec, nsec);
405 out.write_all(format!("Modify:\t{}\n", dt.to_rfc2822()).as_bytes())?;
6934c6fe
CE
406 out.flush()?;
407 Ok(())
408 })
951cf17e
CE
409}
410
6934c6fe
CE
411#[api(
412 input: {
413 properties: {
414 path: {
415 type: String,
416 description: "target path."
417 }
418 }
419 }
420)]
421/// Select an entry for restore.
422///
423/// This will return an error if the entry is already present in the list or
424/// if an invalid path was provided.
425fn select_command(path: String) -> Result<(), Error> {
426 Context::with(|ctx| {
32d192a9
CE
427 let mut local = ctx.path.clone();
428 local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, false)?;
429 let canonical = local.canonical(&mut ctx.decoder, &mut ctx.catalog, false)?;
430 let pattern = MatchPattern::from_line(canonical.generate_cstring()?.as_bytes())?
25cdd0e0
CE
431 .ok_or_else(|| format_err!("encountered invalid match pattern"))?;
432 if ctx.selected.iter().find(|p| **p == pattern).is_none() {
433 ctx.selected.push(pattern);
6934c6fe 434 }
25cdd0e0 435 Ok(())
6934c6fe 436 })
f14c96ea
CE
437}
438
6934c6fe
CE
439#[api(
440 input: {
441 properties: {
442 path: {
443 type: String,
444 description: "path to entry to remove from list."
445 }
446 }
f14c96ea 447 }
6934c6fe
CE
448)]
449/// Deselect an entry for restore.
450///
451/// This will return an error if the entry was not found in the list of entries
452/// selected for restore.
453fn deselect_command(path: String) -> Result<(), Error> {
454 Context::with(|ctx| {
32d192a9
CE
455 let mut local = ctx.path.clone();
456 local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, false)?;
457 let canonical = local.canonical(&mut ctx.decoder, &mut ctx.catalog, false)?;
458 println!("{:?}", canonical.generate_cstring()?);
459 let mut pattern = MatchPattern::from_line(canonical.generate_cstring()?.as_bytes())?
25cdd0e0
CE
460 .ok_or_else(|| format_err!("encountered invalid match pattern"))?;
461 if let Some(last) = ctx.selected.last() {
462 if last == &pattern {
463 ctx.selected.pop();
464 return Ok(());
465 }
6934c6fe 466 }
25cdd0e0
CE
467 pattern.invert();
468 ctx.selected.push(pattern);
469 Ok(())
6934c6fe
CE
470 })
471}
f14c96ea 472
35ddf0b4
CE
473#[api( input: { properties: { } })]
474/// Clear the list of files selected for restore.
475fn clear_selected_command() -> Result<(), Error> {
476 Context::with(|ctx| {
477 ctx.selected.clear();
478 Ok(())
479 })
480}
481
6934c6fe
CE
482#[api(
483 input: {
484 properties: {
485 target: {
486 type: String,
487 description: "target path for restore on local filesystem."
488 }
489 }
f14c96ea 490 }
6934c6fe
CE
491)]
492/// Restore the selected entries to the given target path.
493///
494/// Target must not exist on the clients filesystem.
495fn restore_selected_command(target: String) -> Result<(), Error> {
496 Context::with(|ctx| {
25cdd0e0 497 if ctx.selected.is_empty() {
6934c6fe
CE
498 bail!("no entries selected for restore");
499 }
f14c96ea 500
6934c6fe
CE
501 // Entry point for the restore is always root here as the provided match
502 // patterns are relative to root as well.
503 let start_dir = ctx.decoder.root()?;
504 ctx.decoder
25cdd0e0 505 .restore(&start_dir, &Path::new(&target), &ctx.selected)?;
6934c6fe
CE
506 Ok(())
507 })
508}
f14c96ea 509
8e464141
CE
510#[api(
511 input: {
512 properties: {
513 pattern: {
514 type: Boolean,
515 description: "List match patterns instead of the matching files.",
516 optional: true,
517 }
518 }
519 }
520)]
6934c6fe 521/// List entries currently selected for restore.
8e464141 522fn list_selected_command(pattern: Option<bool>) -> Result<(), Error> {
6934c6fe
CE
523 Context::with(|ctx| {
524 let mut out = std::io::stdout();
8e464141
CE
525 if let Some(true) = pattern {
526 out.write_all(&MatchPattern::to_bytes(ctx.selected.as_slice()))?;
527 } else {
528 let mut slices = Vec::with_capacity(ctx.selected.len());
529 for pattern in &ctx.selected {
530 slices.push(pattern.as_slice());
531 }
32d192a9 532 let mut dir_stack = vec![ctx.path.root()];
8e464141
CE
533 ctx.catalog.find(
534 &mut dir_stack,
535 &slices,
536 &Box::new(|path: &[DirEntry]| println!("{:?}", Context::generate_cstring(path).unwrap()))
537 )?;
538 }
6934c6fe 539 out.flush()?;
f14c96ea 540 Ok(())
6934c6fe
CE
541 })
542}
f14c96ea 543
6934c6fe
CE
544#[api(
545 input: {
546 properties: {
547 target: {
548 type: String,
549 description: "target path for restore on local filesystem."
550 },
551 pattern: {
552 type: String,
553 optional: true,
554 description: "match pattern to limit files for restore."
555 }
556 }
557 }
558)]
559/// Restore the sub-archive given by the current working directory to target.
560///
561/// By further providing a pattern, the restore can be limited to a narrower
562/// subset of this sub-archive.
563/// If pattern is not present or empty, the full archive is restored to target.
564fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> {
565 Context::with(|ctx| {
566 let pattern = pattern.unwrap_or_default();
567 let match_pattern = match pattern.as_str() {
568 "" | "/" | "." => Vec::new(),
569 _ => vec![MatchPattern::from_line(pattern.as_bytes())?.unwrap()],
f14c96ea 570 };
6934c6fe
CE
571 // Decoder entry point for the restore.
572 let start_dir = if pattern.starts_with("/") {
573 ctx.decoder.root()?
f14c96ea 574 } else {
6934c6fe
CE
575 // Get the directory corresponding to the working directory from the
576 // archive.
32d192a9
CE
577 let cwd = ctx.path.clone();
578 cwd.lookup(&mut ctx.decoder)?
f14c96ea 579 };
f14c96ea 580
6934c6fe
CE
581 ctx.decoder
582 .restore(&start_dir, &Path::new(&target), &match_pattern)?;
583 Ok(())
584 })
585}
586
25cdd0e0
CE
587#[api(
588 input: {
589 properties: {
590 path: {
591 type: String,
592 description: "Path to node from where to start the search."
593 },
594 pattern: {
595 type: String,
596 description: "Match pattern for matching files in the catalog."
597 },
598 select: {
599 type: bool,
600 optional: true,
601 description: "Add matching filenames to list for restore."
602 }
603 }
604 }
605)]
606/// Find entries in the catalog matching the given match pattern.
607fn find_command(path: String, pattern: String, select: Option<bool>) -> Result<(), Error> {
608 Context::with(|ctx| {
32d192a9
CE
609 let mut local = ctx.path.clone();
610 local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, false)?;
611 let canonical = local.canonical(&mut ctx.decoder, &mut ctx.catalog, false)?;
612 if !local.last().is_directory() {
25cdd0e0
CE
613 bail!("path should be a directory, not a file!");
614 }
615 let select = select.unwrap_or(false);
616
32d192a9 617 let cpath = canonical.generate_cstring().unwrap();
25cdd0e0
CE
618 let pattern = if pattern.starts_with("!") {
619 let mut buffer = vec![b'!'];
620 buffer.extend_from_slice(cpath.as_bytes());
621 buffer.extend_from_slice(pattern[1..pattern.len()].as_bytes());
622 buffer
623 } else {
624 let mut buffer = cpath.as_bytes().to_vec();
625 buffer.extend_from_slice(pattern.as_bytes());
626 buffer
627 };
628
629 let pattern = MatchPattern::from_line(&pattern)?
630 .ok_or_else(|| format_err!("invalid match pattern"))?;
631 let slice = vec![pattern.as_slice()];
632
38d9a698
CE
633 // The match pattern all contain the prefix of the entry path in order to
634 // store them if selected, so the entry point for find is always the root
635 // directory.
32d192a9 636 let mut dir_stack = vec![ctx.path.root()];
25cdd0e0 637 ctx.catalog.find(
38d9a698 638 &mut dir_stack,
25cdd0e0
CE
639 &slice,
640 &Box::new(|path: &[DirEntry]| println!("{:?}", Context::generate_cstring(path).unwrap()))
641 )?;
642
643 // Insert if matches should be selected.
644 // Avoid duplicate entries of the same match pattern.
645 if select && ctx.selected.iter().find(|p| **p == pattern).is_none() {
646 ctx.selected.push(pattern);
647 }
648
649 Ok(())
650 })
651}
652
6934c6fe
CE
653std::thread_local! {
654 static CONTEXT: RefCell<Option<Context>> = RefCell::new(None);
655}
656
657/// Holds the context needed for access to catalog and decoder
658struct Context {
659 /// Calalog reader instance to navigate
660 catalog: CatalogReader<std::fs::File>,
661 /// List of selected paths for restore
25cdd0e0 662 selected: Vec<MatchPattern>,
6934c6fe
CE
663 /// Decoder instance for the current pxar archive
664 decoder: Decoder,
32d192a9
CE
665 /// Handle catalog stuff
666 path: CatalogPathStack,
6934c6fe
CE
667}
668
669impl Context {
670 /// Execute `call` within a context providing a mut ref to `Context` instance.
671 fn with<T, F>(call: F) -> Result<T, Error>
672 where
673 F: FnOnce(&mut Context) -> Result<T, Error>,
674 {
675 CONTEXT.with(|cell| {
676 let mut ctx = cell.borrow_mut();
677 call(&mut ctx.as_mut().unwrap())
678 })
f14c96ea
CE
679 }
680
6934c6fe
CE
681 /// Generate CString from provided stack of `DirEntry`s.
682 fn generate_cstring(dir_stack: &[DirEntry]) -> Result<CString, Error> {
f14c96ea 683 let mut path = vec![b'/'];
6934c6fe
CE
684 // Skip the archive root, the '/' is displayed for it instead
685 for component in dir_stack.iter().skip(1) {
686 path.extend_from_slice(&component.name);
687 if component.is_directory() {
f14c96ea
CE
688 path.push(b'/');
689 }
690 }
6934c6fe 691 Ok(unsafe { CString::from_vec_unchecked(path) })
f14c96ea
CE
692 }
693
6934c6fe
CE
694 /// Generate the CString to display by readline based on
695 /// PROMPT_PREFIX, PROMPT and the current working directory.
696 fn generate_prompt(&self) -> Result<String, Error> {
697 let prompt = format!(
698 "{}{} {} ",
699 PROMPT_PREFIX,
32d192a9 700 self.path.generate_cstring()?.to_string_lossy(),
6934c6fe
CE
701 PROMPT,
702 );
703 Ok(prompt)
f14c96ea 704 }
f14c96ea 705}
fee5528e
CE
706
707/// A valid path in the catalog starting from root.
708///
709/// Symlinks are stored by pushing the symlink entry and the target entry onto
710/// the stack. Allows to resolve all symlink in order to generate a canonical
711/// path needed for reading from the archive.
712#[derive(Clone)]
713struct CatalogPathStack {
714 stack: Vec<DirEntry>,
715 root: DirEntry,
716}
717
718impl CatalogPathStack {
719 /// Create a new stack with given root entry.
720 fn new(root: DirEntry) -> Self {
721 Self {
722 stack: Vec::new(),
723 root,
724 }
725 }
726
727 /// Get a clone of the root directories entry.
728 fn root(&self) -> DirEntry {
729 self.root.clone()
730 }
731
732 /// Remove all entries from the stack.
733 ///
734 /// This equals to being at the root directory.
735 fn clear(&mut self) {
736 self.stack.clear();
737 }
738
739 /// Get a reference to the last entry on the stack.
740 fn last(&self) -> &DirEntry {
741 self.stack.last().unwrap_or(&self.root)
742 }
743
744 /// Check if the last entry is a symlink.
745 fn last_is_symlink(&self) -> bool {
746 self.last().is_symlink()
747 }
748
749 /// Check if the last entry is a directory.
750 fn last_is_directory(&self) -> bool {
751 self.last().is_directory()
752 }
753
754 /// Remove a component, if it was a symlink target,
755 /// this removes also the symlink entry.
756 fn pop(&mut self) -> Option<DirEntry> {
757 let entry = self.stack.pop()?;
758 if self.last_is_symlink() {
759 self.stack.pop()
760 } else {
761 Some(entry)
762 }
763 }
764
765 /// Add a component to the stack.
766 fn push(&mut self, entry: DirEntry) {
767 self.stack.push(entry)
768 }
769
770 /// Check if pushing the given entry onto the CatalogPathStack would create a
771 /// loop by checking if the same entry is already present.
772 fn creates_loop(&self, entry: &DirEntry) -> bool {
773 self.stack.iter().any(|comp| comp.eq(entry))
774 }
775
776 /// Starting from this path, traverse the catalog by the provided `path`.
777 fn traverse(
778 &mut self,
779 path: &PathBuf,
780 mut decoder: &mut Decoder,
781 mut catalog: &mut CatalogReader<std::fs::File>,
782 follow_final: bool,
783 ) -> Result<(), Error> {
784 for component in path.components() {
785 match component {
786 Component::RootDir => self.clear(),
787 Component::CurDir => continue,
788 Component::ParentDir => { self.pop(); }
789 Component::Normal(comp) => {
790 let entry = catalog.lookup(self.last(), comp.as_bytes())?;
791 if self.creates_loop(&entry) {
792 bail!("loop detected, will not follow");
793 }
794 self.push(entry);
795 if self.last_is_symlink() && follow_final {
796 let mut canonical = self.canonical(&mut decoder, &mut catalog, follow_final)?;
797 let target = canonical.pop().unwrap();
798 self.push(target);
799 }
800 }
801 Component::Prefix(_) => bail!("encountered prefix component. Non unix systems not supported."),
802 }
803 }
804 if path.as_os_str().as_bytes().ends_with(b"/") && !self.last_is_directory() {
805 bail!("entry is not a directory");
806 }
807 Ok(())
808 }
809
810 /// Create a canonical version of this path with symlinks resolved.
811 ///
812 /// If resolve final is true, follow also an eventual symlink of the last
813 /// path component.
814 fn canonical(
815 &self,
816 mut decoder: &mut Decoder,
817 mut catalog: &mut CatalogReader<std::fs::File>,
818 resolve_final: bool,
819 ) -> Result<Self, Error> {
820 let mut canonical = CatalogPathStack::new(self.root.clone());
821 let mut iter = self.stack.iter().enumerate();
822 while let Some((index, component)) = iter.next() {
823 if component.is_directory() {
824 canonical.push(component.clone());
825 } else if component.is_symlink() {
826 canonical.push(component.clone());
827 if index != self.stack.len() - 1 || resolve_final {
828 // Get the symlink target by traversing the canonical path
829 // in the archive up to the symlink.
830 let archive_entry = canonical.lookup(&mut decoder)?;
831 canonical.pop();
832 // Resolving target means also ignoring the target in the iterator, so get it.
833 iter.next();
834 let target = archive_entry.target
835 .ok_or_else(|| format_err!("expected entry with symlink target."))?;
836 canonical.traverse(&target, &mut decoder, &mut catalog, resolve_final)?;
837 }
838 } else if index != self.stack.len() - 1 {
839 bail!("intermitten node is not symlink nor directory");
840 } else {
841 canonical.push(component.clone());
842 }
843 }
844 Ok(canonical)
845 }
846
847 /// Lookup this path in the archive using the provided decoder.
848 fn lookup(&self, decoder: &mut Decoder) -> Result<DirectoryEntry, Error> {
849 let mut current = decoder.root()?;
850 for component in self.stack.iter() {
851 match decoder.lookup(&current, &OsStr::from_bytes(&component.name))? {
852 Some(item) => current = item,
853 // This should not happen if catalog an archive are consistent.
854 None => bail!("no such file or directory in archive - inconsistent catalog"),
855 }
856 }
857 Ok(current)
858 }
859
860 /// Generate a CString from this.
861 fn generate_cstring(&self) -> Result<CString, Error> {
862 let mut path = vec![b'/'];
863 let mut iter = self.stack.iter().enumerate();
864 while let Some((index, component)) = iter.next() {
865 if component.is_symlink() && index != self.stack.len() - 1 {
866 let (_, next) = iter.next()
867 .ok_or_else(|| format_err!("unresolved symlink encountered"))?;
868 // Display the name of the link, not the target
869 path.extend_from_slice(&component.name);
870 if next.is_directory() {
871 path.push(b'/');
872 }
873 } else {
874 path.extend_from_slice(&component.name);
875 if component.is_directory() {
876 path.push(b'/');
877 }
878 }
879 }
880 Ok(unsafe { CString::from_vec_unchecked(path) })
881 }
882}