]>
Commit | Line | Data |
---|---|---|
6934c6fe | 1 | use std::cell::RefCell; |
25cdd0e0 | 2 | use std::collections::HashMap; |
03f779c6 | 3 | use std::convert::TryFrom; |
6934c6fe | 4 | use std::ffi::{CString, OsStr}; |
f14c96ea CE |
5 | use std::io::Write; |
6 | use std::os::unix::ffi::OsStrExt; | |
32d192a9 | 7 | use std::path::{Component, Path, PathBuf}; |
f14c96ea | 8 | |
03f779c6 | 9 | use chrono::{Utc, offset::TimeZone}; |
f7d4e4b5 | 10 | use anyhow::{bail, format_err, Error}; |
03f779c6 | 11 | use nix::sys::stat::{Mode, SFlag}; |
f14c96ea | 12 | |
501f4fa2 DM |
13 | use proxmox::api::{cli::*, *}; |
14 | use proxmox::sys::linux::tty; | |
15 | ||
6934c6fe | 16 | use super::catalog::{CatalogReader, DirEntry}; |
f14c96ea | 17 | use crate::pxar::*; |
6934c6fe | 18 | use crate::tools; |
f14c96ea | 19 | |
f14c96ea | 20 | |
951cf17e | 21 | const PROMPT_PREFIX: &str = "pxar:"; |
6934c6fe | 22 | const PROMPT: &str = ">"; |
951cf17e CE |
23 | |
24 | /// Interactive shell for interacton with the catalog. | |
f14c96ea | 25 | pub 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 | 33 | pub 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 |
98 | impl 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(¤t) { |
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. | |
200 | fn 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 | |
223 | fn 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. | |
253 | fn 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. | |
313 | fn 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. | |
425 | fn 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. | |
453 | fn 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. | |
475 | fn 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. | |
495 | fn 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 | 522 | fn 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. | |
564 | fn 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. | |
607 | fn 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 |
653 | std::thread_local! { |
654 | static CONTEXT: RefCell<Option<Context>> = RefCell::new(None); | |
655 | } | |
656 | ||
657 | /// Holds the context needed for access to catalog and decoder | |
658 | struct 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 | ||
669 | impl 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)] | |
713 | struct CatalogPathStack { | |
714 | stack: Vec<DirEntry>, | |
715 | root: DirEntry, | |
716 | } | |
717 | ||
718 | impl 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(¤t, &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 | } |