]>
Commit | Line | Data |
---|---|---|
6934c6fe | 1 | use std::cell::RefCell; |
25cdd0e0 | 2 | use std::collections::HashMap; |
6934c6fe | 3 | use std::ffi::{CString, OsStr}; |
f14c96ea CE |
4 | use std::io::Write; |
5 | use std::os::unix::ffi::OsStrExt; | |
6 | use std::path::Path; | |
7 | ||
8 | use failure::*; | |
f14c96ea | 9 | |
6934c6fe | 10 | use super::catalog::{CatalogReader, DirEntry}; |
f14c96ea | 11 | use crate::pxar::*; |
6934c6fe | 12 | use crate::tools; |
f14c96ea | 13 | |
6934c6fe | 14 | use proxmox::api::{cli::*, *}; |
f14c96ea | 15 | |
951cf17e | 16 | const PROMPT_PREFIX: &str = "pxar:"; |
6934c6fe | 17 | const PROMPT: &str = ">"; |
951cf17e CE |
18 | |
19 | /// Interactive shell for interacton with the catalog. | |
f14c96ea | 20 | pub struct Shell { |
6934c6fe CE |
21 | /// Readline instance handling input and callbacks |
22 | rl: rustyline::Editor<CliHelper>, | |
23 | prompt: String, | |
951cf17e CE |
24 | } |
25 | ||
ecbaa38f DM |
26 | /// This list defines all the shell commands and their properties |
27 | /// using the api schema | |
55c3cb69 | 28 | pub fn catalog_shell_cli() -> CommandLineInterface { |
ecbaa38f DM |
29 | |
30 | let map = CliCommandMap::new() | |
48ef3c33 | 31 | .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND)) |
ecbaa38f DM |
32 | .insert( |
33 | "cd", | |
34 | CliCommand::new(&API_METHOD_CD_COMMAND) | |
35 | .arg_param(&["path"]) | |
36 | .completion_cb("path", Shell::complete_path) | |
ecbaa38f DM |
37 | ) |
38 | .insert( | |
39 | "ls", | |
40 | CliCommand::new(&API_METHOD_LS_COMMAND) | |
41 | .arg_param(&["path"]) | |
42 | .completion_cb("path", Shell::complete_path) | |
48ef3c33 | 43 | ) |
ecbaa38f DM |
44 | .insert( |
45 | "stat", | |
46 | CliCommand::new(&API_METHOD_STAT_COMMAND) | |
47 | .arg_param(&["path"]) | |
48 | .completion_cb("path", Shell::complete_path) | |
48ef3c33 | 49 | ) |
ecbaa38f DM |
50 | .insert( |
51 | "select", | |
52 | CliCommand::new(&API_METHOD_SELECT_COMMAND) | |
53 | .arg_param(&["path"]) | |
54 | .completion_cb("path", Shell::complete_path) | |
ecbaa38f DM |
55 | ) |
56 | .insert( | |
57 | "deselect", | |
58 | CliCommand::new(&API_METHOD_DESELECT_COMMAND) | |
59 | .arg_param(&["path"]) | |
60 | .completion_cb("path", Shell::complete_path) | |
ecbaa38f DM |
61 | ) |
62 | .insert( | |
63 | "restore-selected", | |
64 | CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND) | |
65 | .arg_param(&["target"]) | |
66 | .completion_cb("target", tools::complete_file_name) | |
ecbaa38f DM |
67 | ) |
68 | .insert( | |
69 | "list-selected", | |
48ef3c33 | 70 | CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND), |
ecbaa38f DM |
71 | ) |
72 | .insert( | |
73 | "restore", | |
74 | CliCommand::new(&API_METHOD_RESTORE_COMMAND) | |
75 | .arg_param(&["target"]) | |
76 | .completion_cb("target", tools::complete_file_name) | |
ecbaa38f | 77 | ) |
25cdd0e0 CE |
78 | .insert( |
79 | "find", | |
80 | CliCommand::new(&API_METHOD_FIND_COMMAND) | |
81 | .arg_param(&["path", "pattern"]) | |
82 | .completion_cb("path", Shell::complete_path) | |
83 | ) | |
ecbaa38f DM |
84 | .insert_help(); |
85 | ||
86 | CommandLineInterface::Nested(map) | |
87 | } | |
88 | ||
951cf17e CE |
89 | impl Shell { |
90 | /// Create a new shell for the given catalog and pxar archive. | |
91 | pub fn new( | |
6934c6fe | 92 | mut catalog: CatalogReader<std::fs::File>, |
951cf17e CE |
93 | archive_name: &str, |
94 | decoder: Decoder, | |
95 | ) -> Result<Self, Error> { | |
6934c6fe CE |
96 | let catalog_root = catalog.root()?; |
97 | // The root for the given archive as stored in the catalog | |
98 | let archive_root = catalog.lookup(&catalog_root, archive_name.as_bytes())?; | |
99 | let root = vec![archive_root]; | |
100 | ||
101 | CONTEXT.with(|handle| { | |
102 | let mut ctx = handle.borrow_mut(); | |
103 | *ctx = Some(Context { | |
104 | catalog, | |
25cdd0e0 | 105 | selected: Vec::new(), |
6934c6fe CE |
106 | decoder, |
107 | root: root.clone(), | |
108 | current: root, | |
109 | }); | |
110 | }); | |
111 | ||
55c3cb69 | 112 | let cli_helper = CliHelper::new(catalog_shell_cli()); |
6934c6fe CE |
113 | let mut rl = rustyline::Editor::<CliHelper>::new(); |
114 | rl.set_helper(Some(cli_helper)); | |
115 | ||
116 | Context::with(|ctx| { | |
117 | Ok(Self { | |
118 | rl, | |
119 | prompt: ctx.generate_prompt()?, | |
120 | }) | |
951cf17e CE |
121 | }) |
122 | } | |
123 | ||
124 | /// Start the interactive shell loop | |
125 | pub fn shell(mut self) -> Result<(), Error> { | |
6934c6fe CE |
126 | while let Ok(line) = self.rl.readline(&self.prompt) { |
127 | let helper = self.rl.helper().unwrap(); | |
128 | let args = match shellword_split(&line) { | |
129 | Ok(args) => args, | |
951cf17e | 130 | Err(err) => { |
6934c6fe | 131 | println!("Error: {}", err); |
951cf17e CE |
132 | continue; |
133 | } | |
134 | }; | |
6934c6fe CE |
135 | let _ = handle_command(helper.cmd_def(), "", args); |
136 | self.rl.add_history_entry(line); | |
137 | self.update_prompt()?; | |
951cf17e CE |
138 | } |
139 | Ok(()) | |
140 | } | |
951cf17e | 141 | |
6934c6fe CE |
142 | /// Update the prompt to the new working directory |
143 | fn update_prompt(&mut self) -> Result<(), Error> { | |
144 | Context::with(|ctx| { | |
145 | self.prompt = ctx.generate_prompt()?; | |
146 | Ok(()) | |
147 | }) | |
951cf17e CE |
148 | } |
149 | ||
6934c6fe CE |
150 | /// Completions for paths by lookup in the catalog |
151 | fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> { | |
152 | Context::with(|ctx| { | |
153 | let (base, to_complete) = match complete_me.rfind('/') { | |
154 | // Split at ind + 1 so the slash remains on base, ok also if | |
155 | // ends in slash as split_at accepts up to length as index. | |
156 | Some(ind) => complete_me.split_at(ind + 1), | |
157 | None => ("", complete_me), | |
158 | }; | |
951cf17e | 159 | |
6934c6fe CE |
160 | let current = if base.is_empty() { |
161 | ctx.current.clone() | |
162 | } else { | |
163 | ctx.canonical_path(base)? | |
164 | }; | |
951cf17e | 165 | |
6934c6fe CE |
166 | let entries = match ctx.catalog.read_dir(¤t.last().unwrap()) { |
167 | Ok(entries) => entries, | |
168 | Err(_) => return Ok(Vec::new()), | |
169 | }; | |
170 | ||
171 | let mut list = Vec::new(); | |
172 | for entry in &entries { | |
173 | let mut name = String::from(base); | |
174 | if entry.name.starts_with(to_complete.as_bytes()) { | |
175 | name.push_str(std::str::from_utf8(&entry.name)?); | |
176 | if entry.is_directory() { | |
177 | name.push('/'); | |
951cf17e | 178 | } |
6934c6fe | 179 | list.push(name); |
951cf17e | 180 | } |
951cf17e | 181 | } |
6934c6fe CE |
182 | Ok(list) |
183 | }) | |
184 | .unwrap_or_default() | |
951cf17e CE |
185 | } |
186 | } | |
187 | ||
6934c6fe CE |
188 | #[api(input: { properties: {} })] |
189 | /// List the current working directory. | |
190 | fn pwd_command() -> Result<(), Error> { | |
191 | Context::with(|ctx| { | |
192 | let path = Context::generate_cstring(&ctx.current)?; | |
193 | let mut out = std::io::stdout(); | |
194 | out.write_all(&path.as_bytes())?; | |
195 | out.write_all(&[b'\n'])?; | |
196 | out.flush()?; | |
197 | Ok(()) | |
198 | }) | |
951cf17e CE |
199 | } |
200 | ||
6934c6fe CE |
201 | #[api( |
202 | input: { | |
203 | properties: { | |
204 | path: { | |
205 | type: String, | |
206 | optional: true, | |
207 | description: "target path." | |
208 | } | |
209 | } | |
951cf17e | 210 | } |
6934c6fe CE |
211 | )] |
212 | /// Change the current working directory to the new directory | |
213 | fn cd_command(path: Option<String>) -> Result<(), Error> { | |
214 | Context::with(|ctx| { | |
215 | let path = path.unwrap_or_default(); | |
216 | let mut path = ctx.canonical_path(&path)?; | |
217 | if !path | |
218 | .last() | |
219 | .ok_or_else(|| format_err!("invalid path component"))? | |
220 | .is_directory() | |
221 | { | |
222 | // Change to the parent dir of the file instead | |
223 | path.pop(); | |
224 | eprintln!("not a directory, fallback to parent directory"); | |
225 | } | |
226 | ctx.current = path; | |
227 | Ok(()) | |
228 | }) | |
951cf17e CE |
229 | } |
230 | ||
6934c6fe CE |
231 | #[api( |
232 | input: { | |
233 | properties: { | |
234 | path: { | |
235 | type: String, | |
236 | optional: true, | |
237 | description: "target path." | |
238 | } | |
951cf17e CE |
239 | } |
240 | } | |
6934c6fe CE |
241 | )] |
242 | /// List the content of working directory or given path. | |
243 | fn ls_command(path: Option<String>) -> Result<(), Error> { | |
244 | Context::with(|ctx| { | |
245 | let parent = if let Some(path) = path { | |
246 | ctx.canonical_path(&path)? | |
247 | .last() | |
248 | .ok_or_else(|| format_err!("invalid path component"))? | |
249 | .clone() | |
250 | } else { | |
251 | ctx.current.last().unwrap().clone() | |
252 | }; | |
951cf17e | 253 | |
6934c6fe CE |
254 | let list = if parent.is_directory() { |
255 | ctx.catalog.read_dir(&parent)? | |
951cf17e | 256 | } else { |
6934c6fe CE |
257 | vec![parent] |
258 | }; | |
259 | ||
260 | if list.is_empty() { | |
261 | return Ok(()); | |
951cf17e | 262 | } |
6934c6fe CE |
263 | let max = list.iter().max_by(|x, y| x.name.len().cmp(&y.name.len())); |
264 | let max = match max { | |
265 | Some(dir_entry) => dir_entry.name.len() + 1, | |
266 | None => 0, | |
267 | }; | |
951cf17e | 268 | |
6934c6fe CE |
269 | let (_rows, mut cols) = Context::get_terminal_size(); |
270 | cols /= max; | |
951cf17e | 271 | |
6934c6fe CE |
272 | let mut out = std::io::stdout(); |
273 | for (index, item) in list.iter().enumerate() { | |
274 | out.write_all(&item.name)?; | |
275 | // Fill with whitespaces | |
276 | out.write_all(&vec![b' '; max - item.name.len()])?; | |
277 | if index % cols == (cols - 1) { | |
278 | out.write_all(&[b'\n'])?; | |
951cf17e | 279 | } |
951cf17e | 280 | } |
6934c6fe CE |
281 | // If the last line is not complete, add the newline |
282 | if list.len() % cols != cols - 1 { | |
283 | out.write_all(&[b'\n'])?; | |
951cf17e | 284 | } |
6934c6fe CE |
285 | out.flush()?; |
286 | Ok(()) | |
287 | }) | |
288 | } | |
289 | ||
290 | #[api( | |
291 | input: { | |
292 | properties: { | |
293 | path: { | |
294 | type: String, | |
295 | description: "target path." | |
296 | } | |
951cf17e | 297 | } |
951cf17e | 298 | } |
6934c6fe CE |
299 | )] |
300 | /// Read the metadata for a given directory entry. | |
301 | /// | |
302 | /// This is expensive because the data has to be read from the pxar `Decoder`, | |
303 | /// which means reading over the network. | |
304 | fn stat_command(path: String) -> Result<(), Error> { | |
305 | Context::with(|ctx| { | |
306 | // First check if the file exists in the catalog, therefore avoiding | |
307 | // expensive calls to the decoder just to find out that there maybe is | |
308 | // no such entry. | |
309 | // This is done by calling canonical_path(), which returns the full path | |
310 | // if it exists, error otherwise. | |
311 | let path = ctx.canonical_path(&path)?; | |
90fc97af | 312 | let item = ctx.lookup(&path)?; |
6934c6fe CE |
313 | let mut out = std::io::stdout(); |
314 | out.write_all(b"File: ")?; | |
315 | out.write_all(item.filename.as_bytes())?; | |
316 | out.write_all(&[b'\n'])?; | |
90fc97af | 317 | out.write_all(format!("Size: {}\n", item.size).as_bytes())?; |
6934c6fe CE |
318 | out.write_all(b"Type: ")?; |
319 | match item.entry.mode as u32 & libc::S_IFMT { | |
320 | libc::S_IFDIR => out.write_all(b"directory\n")?, | |
321 | libc::S_IFREG => out.write_all(b"regular file\n")?, | |
322 | libc::S_IFLNK => out.write_all(b"symbolic link\n")?, | |
323 | libc::S_IFBLK => out.write_all(b"block special file\n")?, | |
324 | libc::S_IFCHR => out.write_all(b"character special file\n")?, | |
325 | _ => out.write_all(b"unknown\n")?, | |
326 | }; | |
327 | out.write_all(format!("Uid: {}\n", item.entry.uid).as_bytes())?; | |
328 | out.write_all(format!("Gid: {}\n", item.entry.gid).as_bytes())?; | |
329 | out.flush()?; | |
330 | Ok(()) | |
331 | }) | |
951cf17e CE |
332 | } |
333 | ||
6934c6fe CE |
334 | #[api( |
335 | input: { | |
336 | properties: { | |
337 | path: { | |
338 | type: String, | |
339 | description: "target path." | |
340 | } | |
341 | } | |
342 | } | |
343 | )] | |
344 | /// Select an entry for restore. | |
345 | /// | |
346 | /// This will return an error if the entry is already present in the list or | |
347 | /// if an invalid path was provided. | |
348 | fn select_command(path: String) -> Result<(), Error> { | |
349 | Context::with(|ctx| { | |
350 | // Calling canonical_path() makes sure the provided path is valid and | |
351 | // actually contained within the catalog and therefore also the archive. | |
352 | let path = ctx.canonical_path(&path)?; | |
25cdd0e0 CE |
353 | let pattern = MatchPattern::from_line(Context::generate_cstring(&path)?.as_bytes())? |
354 | .ok_or_else(|| format_err!("encountered invalid match pattern"))?; | |
355 | if ctx.selected.iter().find(|p| **p == pattern).is_none() { | |
356 | ctx.selected.push(pattern); | |
6934c6fe | 357 | } |
25cdd0e0 | 358 | Ok(()) |
6934c6fe | 359 | }) |
f14c96ea CE |
360 | } |
361 | ||
6934c6fe CE |
362 | #[api( |
363 | input: { | |
364 | properties: { | |
365 | path: { | |
366 | type: String, | |
367 | description: "path to entry to remove from list." | |
368 | } | |
369 | } | |
f14c96ea | 370 | } |
6934c6fe CE |
371 | )] |
372 | /// Deselect an entry for restore. | |
373 | /// | |
374 | /// This will return an error if the entry was not found in the list of entries | |
375 | /// selected for restore. | |
376 | fn deselect_command(path: String) -> Result<(), Error> { | |
377 | Context::with(|ctx| { | |
378 | let path = ctx.canonical_path(&path)?; | |
25cdd0e0 CE |
379 | let mut pattern = MatchPattern::from_line(Context::generate_cstring(&path)?.as_bytes())? |
380 | .ok_or_else(|| format_err!("encountered invalid match pattern"))?; | |
381 | if let Some(last) = ctx.selected.last() { | |
382 | if last == &pattern { | |
383 | ctx.selected.pop(); | |
384 | return Ok(()); | |
385 | } | |
6934c6fe | 386 | } |
25cdd0e0 CE |
387 | pattern.invert(); |
388 | ctx.selected.push(pattern); | |
389 | Ok(()) | |
6934c6fe CE |
390 | }) |
391 | } | |
f14c96ea | 392 | |
6934c6fe CE |
393 | #[api( |
394 | input: { | |
395 | properties: { | |
396 | target: { | |
397 | type: String, | |
398 | description: "target path for restore on local filesystem." | |
399 | } | |
400 | } | |
f14c96ea | 401 | } |
6934c6fe CE |
402 | )] |
403 | /// Restore the selected entries to the given target path. | |
404 | /// | |
405 | /// Target must not exist on the clients filesystem. | |
406 | fn restore_selected_command(target: String) -> Result<(), Error> { | |
407 | Context::with(|ctx| { | |
25cdd0e0 | 408 | if ctx.selected.is_empty() { |
6934c6fe CE |
409 | bail!("no entries selected for restore"); |
410 | } | |
f14c96ea | 411 | |
6934c6fe CE |
412 | // Entry point for the restore is always root here as the provided match |
413 | // patterns are relative to root as well. | |
414 | let start_dir = ctx.decoder.root()?; | |
415 | ctx.decoder | |
25cdd0e0 | 416 | .restore(&start_dir, &Path::new(&target), &ctx.selected)?; |
6934c6fe CE |
417 | Ok(()) |
418 | }) | |
419 | } | |
f14c96ea | 420 | |
6934c6fe CE |
421 | #[api( input: { properties: {} })] |
422 | /// List entries currently selected for restore. | |
423 | fn list_selected_command() -> Result<(), Error> { | |
424 | Context::with(|ctx| { | |
425 | let mut out = std::io::stdout(); | |
25cdd0e0 | 426 | out.write_all(&MatchPattern::to_bytes(ctx.selected.as_slice()))?; |
6934c6fe | 427 | out.flush()?; |
f14c96ea | 428 | Ok(()) |
6934c6fe CE |
429 | }) |
430 | } | |
f14c96ea | 431 | |
6934c6fe CE |
432 | #[api( |
433 | input: { | |
434 | properties: { | |
435 | target: { | |
436 | type: String, | |
437 | description: "target path for restore on local filesystem." | |
438 | }, | |
439 | pattern: { | |
440 | type: String, | |
441 | optional: true, | |
442 | description: "match pattern to limit files for restore." | |
443 | } | |
444 | } | |
445 | } | |
446 | )] | |
447 | /// Restore the sub-archive given by the current working directory to target. | |
448 | /// | |
449 | /// By further providing a pattern, the restore can be limited to a narrower | |
450 | /// subset of this sub-archive. | |
451 | /// If pattern is not present or empty, the full archive is restored to target. | |
452 | fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> { | |
453 | Context::with(|ctx| { | |
454 | let pattern = pattern.unwrap_or_default(); | |
455 | let match_pattern = match pattern.as_str() { | |
456 | "" | "/" | "." => Vec::new(), | |
457 | _ => vec![MatchPattern::from_line(pattern.as_bytes())?.unwrap()], | |
f14c96ea | 458 | }; |
6934c6fe CE |
459 | // Decoder entry point for the restore. |
460 | let start_dir = if pattern.starts_with("/") { | |
461 | ctx.decoder.root()? | |
f14c96ea | 462 | } else { |
6934c6fe CE |
463 | // Get the directory corresponding to the working directory from the |
464 | // archive. | |
465 | let cwd = ctx.current.clone(); | |
90fc97af | 466 | ctx.lookup(&cwd)? |
f14c96ea | 467 | }; |
f14c96ea | 468 | |
6934c6fe CE |
469 | ctx.decoder |
470 | .restore(&start_dir, &Path::new(&target), &match_pattern)?; | |
471 | Ok(()) | |
472 | }) | |
473 | } | |
474 | ||
25cdd0e0 CE |
475 | #[api( |
476 | input: { | |
477 | properties: { | |
478 | path: { | |
479 | type: String, | |
480 | description: "Path to node from where to start the search." | |
481 | }, | |
482 | pattern: { | |
483 | type: String, | |
484 | description: "Match pattern for matching files in the catalog." | |
485 | }, | |
486 | select: { | |
487 | type: bool, | |
488 | optional: true, | |
489 | description: "Add matching filenames to list for restore." | |
490 | } | |
491 | } | |
492 | } | |
493 | )] | |
494 | /// Find entries in the catalog matching the given match pattern. | |
495 | fn find_command(path: String, pattern: String, select: Option<bool>) -> Result<(), Error> { | |
496 | Context::with(|ctx| { | |
38d9a698 | 497 | let path = ctx.canonical_path(&path)?; |
25cdd0e0 CE |
498 | if !path.last().unwrap().is_directory() { |
499 | bail!("path should be a directory, not a file!"); | |
500 | } | |
501 | let select = select.unwrap_or(false); | |
502 | ||
503 | let cpath = Context::generate_cstring(&path).unwrap(); | |
504 | let pattern = if pattern.starts_with("!") { | |
505 | let mut buffer = vec![b'!']; | |
506 | buffer.extend_from_slice(cpath.as_bytes()); | |
507 | buffer.extend_from_slice(pattern[1..pattern.len()].as_bytes()); | |
508 | buffer | |
509 | } else { | |
510 | let mut buffer = cpath.as_bytes().to_vec(); | |
511 | buffer.extend_from_slice(pattern.as_bytes()); | |
512 | buffer | |
513 | }; | |
514 | ||
515 | let pattern = MatchPattern::from_line(&pattern)? | |
516 | .ok_or_else(|| format_err!("invalid match pattern"))?; | |
517 | let slice = vec![pattern.as_slice()]; | |
518 | ||
38d9a698 CE |
519 | // The match pattern all contain the prefix of the entry path in order to |
520 | // store them if selected, so the entry point for find is always the root | |
521 | // directory. | |
522 | let mut dir_stack = ctx.root.clone(); | |
25cdd0e0 | 523 | ctx.catalog.find( |
38d9a698 | 524 | &mut dir_stack, |
25cdd0e0 CE |
525 | &slice, |
526 | &Box::new(|path: &[DirEntry]| println!("{:?}", Context::generate_cstring(path).unwrap())) | |
527 | )?; | |
528 | ||
529 | // Insert if matches should be selected. | |
530 | // Avoid duplicate entries of the same match pattern. | |
531 | if select && ctx.selected.iter().find(|p| **p == pattern).is_none() { | |
532 | ctx.selected.push(pattern); | |
533 | } | |
534 | ||
535 | Ok(()) | |
536 | }) | |
537 | } | |
538 | ||
6934c6fe CE |
539 | std::thread_local! { |
540 | static CONTEXT: RefCell<Option<Context>> = RefCell::new(None); | |
541 | } | |
542 | ||
543 | /// Holds the context needed for access to catalog and decoder | |
544 | struct Context { | |
545 | /// Calalog reader instance to navigate | |
546 | catalog: CatalogReader<std::fs::File>, | |
547 | /// List of selected paths for restore | |
25cdd0e0 | 548 | selected: Vec<MatchPattern>, |
6934c6fe CE |
549 | /// Decoder instance for the current pxar archive |
550 | decoder: Decoder, | |
551 | /// Root directory for the give archive as stored in the catalog | |
552 | root: Vec<DirEntry>, | |
553 | /// Stack of directories up to the current working directory | |
554 | /// used for navigation and path completion. | |
555 | current: Vec<DirEntry>, | |
556 | } | |
557 | ||
558 | impl Context { | |
559 | /// Execute `call` within a context providing a mut ref to `Context` instance. | |
560 | fn with<T, F>(call: F) -> Result<T, Error> | |
561 | where | |
562 | F: FnOnce(&mut Context) -> Result<T, Error>, | |
563 | { | |
564 | CONTEXT.with(|cell| { | |
565 | let mut ctx = cell.borrow_mut(); | |
566 | call(&mut ctx.as_mut().unwrap()) | |
567 | }) | |
f14c96ea CE |
568 | } |
569 | ||
6934c6fe CE |
570 | /// Generate CString from provided stack of `DirEntry`s. |
571 | fn generate_cstring(dir_stack: &[DirEntry]) -> Result<CString, Error> { | |
f14c96ea | 572 | let mut path = vec![b'/']; |
6934c6fe CE |
573 | // Skip the archive root, the '/' is displayed for it instead |
574 | for component in dir_stack.iter().skip(1) { | |
575 | path.extend_from_slice(&component.name); | |
576 | if component.is_directory() { | |
f14c96ea CE |
577 | path.push(b'/'); |
578 | } | |
579 | } | |
6934c6fe | 580 | Ok(unsafe { CString::from_vec_unchecked(path) }) |
f14c96ea CE |
581 | } |
582 | ||
583 | /// Resolve the indirect path components and return an absolute path. | |
584 | /// | |
585 | /// This will actually navigate the filesystem tree to check that the | |
586 | /// path is vaild and exists. | |
587 | /// This does not include following symbolic links. | |
588 | /// If None is given as path, only the root directory is returned. | |
6934c6fe CE |
589 | fn canonical_path(&mut self, path: &str) -> Result<Vec<DirEntry>, Error> { |
590 | if path == "/" { | |
f14c96ea CE |
591 | return Ok(self.root.clone()); |
592 | } | |
593 | ||
594 | let mut path_slice = if path.is_empty() { | |
595 | // Fallback to root if no path was provided | |
596 | return Ok(self.root.clone()); | |
597 | } else { | |
598 | path | |
599 | }; | |
600 | ||
6934c6fe | 601 | let mut dir_stack = if path_slice.starts_with("/") { |
f14c96ea CE |
602 | // Absolute path, reduce view of slice and start from root |
603 | path_slice = &path_slice[1..]; | |
604 | self.root.clone() | |
605 | } else { | |
606 | // Relative path, start from current working directory | |
6934c6fe | 607 | self.current.clone() |
f14c96ea | 608 | }; |
6934c6fe | 609 | let should_end_dir = if path_slice.ends_with("/") { |
f14c96ea CE |
610 | path_slice = &path_slice[0..path_slice.len() - 1]; |
611 | true | |
612 | } else { | |
613 | false | |
614 | }; | |
6934c6fe | 615 | for name in path_slice.split('/') { |
f14c96ea | 616 | match name { |
6934c6fe CE |
617 | "." => continue, |
618 | ".." => { | |
f14c96ea CE |
619 | // Never pop archive root from stack |
620 | if dir_stack.len() > 1 { | |
621 | dir_stack.pop(); | |
622 | } | |
623 | } | |
624 | _ => { | |
6934c6fe | 625 | let entry = self.catalog.lookup(dir_stack.last().unwrap(), name.as_bytes())?; |
f14c96ea CE |
626 | dir_stack.push(entry); |
627 | } | |
628 | } | |
629 | } | |
951cf17e CE |
630 | if should_end_dir |
631 | && !dir_stack | |
632 | .last() | |
633 | .ok_or_else(|| format_err!("invalid path component"))? | |
634 | .is_directory() | |
f14c96ea CE |
635 | { |
636 | bail!("entry is not a directory"); | |
637 | } | |
638 | ||
639 | Ok(dir_stack) | |
640 | } | |
641 | ||
6934c6fe CE |
642 | /// Generate the CString to display by readline based on |
643 | /// PROMPT_PREFIX, PROMPT and the current working directory. | |
644 | fn generate_prompt(&self) -> Result<String, Error> { | |
645 | let prompt = format!( | |
646 | "{}{} {} ", | |
647 | PROMPT_PREFIX, | |
648 | Self::generate_cstring(&self.current)?.to_string_lossy(), | |
649 | PROMPT, | |
650 | ); | |
651 | Ok(prompt) | |
f14c96ea CE |
652 | } |
653 | ||
654 | /// Get the current size of the terminal | |
314bb358 | 655 | /// # Safety |
f14c96ea | 656 | /// |
314bb358 | 657 | /// uses unsafe call to tty_ioctl, see man tty_ioctl(2) |
f14c96ea | 658 | fn get_terminal_size() -> (usize, usize) { |
314bb358 | 659 | const TIOCGWINSZ: libc::c_ulong = 0x5413; |
f14c96ea CE |
660 | |
661 | #[repr(C)] | |
662 | struct WinSize { | |
663 | ws_row: libc::c_ushort, | |
664 | ws_col: libc::c_ushort, | |
665 | _ws_xpixel: libc::c_ushort, // unused | |
666 | _ws_ypixel: libc::c_ushort, // unused | |
667 | } | |
668 | ||
669 | let mut winsize = WinSize { | |
670 | ws_row: 0, | |
671 | ws_col: 0, | |
672 | _ws_xpixel: 0, | |
673 | _ws_ypixel: 0, | |
674 | }; | |
675 | unsafe { libc::ioctl(libc::STDOUT_FILENO, TIOCGWINSZ, &mut winsize) }; | |
676 | (winsize.ws_row as usize, winsize.ws_col as usize) | |
677 | } | |
f14c96ea | 678 | |
6934c6fe CE |
679 | /// Look up the entry given by a canonical absolute `path` in the archive. |
680 | /// | |
681 | /// This will actively navigate the archive by calling the corresponding | |
682 | /// decoder functionalities and is therefore very expensive. | |
90fc97af | 683 | fn lookup(&mut self, absolute_path: &[DirEntry]) -> Result<DirectoryEntry, Error> { |
6934c6fe | 684 | let mut current = self.decoder.root()?; |
6934c6fe CE |
685 | // Ignore the archive root, don't need it. |
686 | for item in absolute_path.iter().skip(1) { | |
687 | match self | |
688 | .decoder | |
689 | .lookup(¤t, &OsStr::from_bytes(&item.name))? | |
690 | { | |
90fc97af | 691 | Some(item) => current = item, |
6934c6fe CE |
692 | // This should not happen if catalog an archive are consistent. |
693 | None => bail!("no such file or directory in archive - inconsistent catalog"), | |
f14c96ea | 694 | } |
f14c96ea | 695 | } |
90fc97af | 696 | Ok(current) |
f14c96ea | 697 | } |
f14c96ea | 698 | } |