1 use std
::cell
::RefCell
;
2 use std
::collections
::HashMap
;
3 use std
::ffi
::{CString, OsStr}
;
5 use std
::os
::unix
::ffi
::OsStrExt
;
10 use super::catalog
::{CatalogReader, DirEntry}
;
14 use proxmox
::api
::{cli::*, *}
;
16 const PROMPT_PREFIX
: &str = "pxar:";
17 const PROMPT
: &str = ">";
19 /// Interactive shell for interacton with the catalog.
21 /// Readline instance handling input and callbacks
22 rl
: rustyline
::Editor
<CliHelper
>,
26 /// This list defines all the shell commands and their properties
27 /// using the api schema
28 pub fn catalog_shell_cli() -> CommandLineInterface
{
30 let map
= CliCommandMap
::new()
31 .insert("pwd", CliCommand
::new(&API_METHOD_PWD_COMMAND
))
34 CliCommand
::new(&API_METHOD_CD_COMMAND
)
36 .completion_cb("path", Shell
::complete_path
)
40 CliCommand
::new(&API_METHOD_LS_COMMAND
)
42 .completion_cb("path", Shell
::complete_path
)
46 CliCommand
::new(&API_METHOD_STAT_COMMAND
)
48 .completion_cb("path", Shell
::complete_path
)
52 CliCommand
::new(&API_METHOD_SELECT_COMMAND
)
54 .completion_cb("path", Shell
::complete_path
)
58 CliCommand
::new(&API_METHOD_DESELECT_COMMAND
)
60 .completion_cb("path", Shell
::complete_path
)
64 CliCommand
::new(&API_METHOD_RESTORE_SELECTED_COMMAND
)
65 .arg_param(&["target"])
66 .completion_cb("target", tools
::complete_file_name
)
70 CliCommand
::new(&API_METHOD_LIST_SELECTED_COMMAND
),
74 CliCommand
::new(&API_METHOD_RESTORE_COMMAND
)
75 .arg_param(&["target"])
76 .completion_cb("target", tools
::complete_file_name
)
80 CliCommand
::new(&API_METHOD_FIND_COMMAND
)
81 .arg_param(&["path", "pattern"])
82 .completion_cb("path", Shell
::complete_path
)
86 CommandLineInterface
::Nested(map
)
90 /// Create a new shell for the given catalog and pxar archive.
92 mut catalog
: CatalogReader
<std
::fs
::File
>,
95 ) -> Result
<Self, Error
> {
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
];
101 CONTEXT
.with(|handle
| {
102 let mut ctx
= handle
.borrow_mut();
103 *ctx
= Some(Context
{
105 selected
: Vec
::new(),
112 let cli_helper
= CliHelper
::new(catalog_shell_cli());
113 let mut rl
= rustyline
::Editor
::<CliHelper
>::new();
114 rl
.set_helper(Some(cli_helper
));
116 Context
::with(|ctx
| {
119 prompt
: ctx
.generate_prompt()?
,
124 /// Start the interactive shell loop
125 pub fn shell(mut self) -> Result
<(), Error
> {
126 while let Ok(line
) = self.rl
.readline(&self.prompt
) {
127 let helper
= self.rl
.helper().unwrap();
128 let args
= match shellword_split(&line
) {
131 println
!("Error: {}", err
);
135 let _
= handle_command(helper
.cmd_def(), "", args
);
136 self.rl
.add_history_entry(line
);
137 self.update_prompt()?
;
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()?
;
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
),
160 let current
= if base
.is_empty() {
163 ctx
.canonical_path(base
)?
166 let entries
= match ctx
.catalog
.read_dir(¤t
.last().unwrap()) {
167 Ok(entries
) => entries
,
168 Err(_
) => return Ok(Vec
::new()),
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() {
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'
])?
;
207 description
: "target path."
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
)?
;
219 .ok_or_else(|| format_err
!("invalid path component"))?
222 // Change to the parent dir of the file instead
224 eprintln
!("not a directory, fallback to parent directory");
237 description
: "target path."
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
)?
248 .ok_or_else(|| format_err
!("invalid path component"))?
251 ctx
.current
.last().unwrap().clone()
254 let list
= if parent
.is_directory() {
255 ctx
.catalog
.read_dir(&parent
)?
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,
269 let (_rows
, mut cols
) = Context
::get_terminal_size();
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'
])?
;
281 // If the last line is not complete, add the newline
282 if list
.len() % cols
!= cols
- 1 {
283 out
.write_all(&[b'
\n'
])?
;
295 description
: "target path."
300 /// Read the metadata for a given directory entry.
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
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
)?
;
312 let item
= ctx
.lookup(&path
)?
;
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'
])?
;
317 out
.write_all(format
!("Size: {}\n", item
.size
).as_bytes())?
;
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")?
,
327 out
.write_all(format
!("Uid: {}\n", item
.entry
.uid
).as_bytes())?
;
328 out
.write_all(format
!("Gid: {}\n", item
.entry
.gid
).as_bytes())?
;
339 description
: "target path."
344 /// Select an entry for restore.
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
)?
;
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
);
367 description
: "path to entry to remove from list."
372 /// Deselect an entry for restore.
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
)?
;
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
{
388 ctx
.selected
.push(pattern
);
398 description
: "target path for restore on local filesystem."
403 /// Restore the selected entries to the given target path.
405 /// Target must not exist on the clients filesystem.
406 fn restore_selected_command(target
: String
) -> Result
<(), Error
> {
407 Context
::with(|ctx
| {
408 if ctx
.selected
.is_empty() {
409 bail
!("no entries selected for restore");
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()?
;
416 .restore(&start_dir
, &Path
::new(&target
), &ctx
.selected
)?
;
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();
426 out
.write_all(&MatchPattern
::to_bytes(ctx
.selected
.as_slice()))?
;
437 description
: "target path for restore on local filesystem."
442 description
: "match pattern to limit files for restore."
447 /// Restore the sub-archive given by the current working directory to target.
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()],
459 // Decoder entry point for the restore.
460 let start_dir
= if pattern
.starts_with("/") {
463 // Get the directory corresponding to the working directory from the
465 let cwd
= ctx
.current
.clone();
470 .restore(&start_dir
, &Path
::new(&target
), &match_pattern
)?
;
480 description
: "Path to node from where to start the search."
484 description
: "Match pattern for matching files in the catalog."
489 description
: "Add matching filenames to list for restore."
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
| {
497 let path
= ctx
.canonical_path(&path
)?
;
498 if !path
.last().unwrap().is_directory() {
499 bail
!("path should be a directory, not a file!");
501 let select
= select
.unwrap_or(false);
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());
510 let mut buffer
= cpath
.as_bytes().to_vec();
511 buffer
.extend_from_slice(pattern
.as_bytes());
515 let pattern
= MatchPattern
::from_line(&pattern
)?
516 .ok_or_else(|| format_err
!("invalid match pattern"))?
;
517 let slice
= vec
![pattern
.as_slice()];
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
522 let mut dir_stack
= ctx
.root
.clone();
526 &Box
::new(|path
: &[DirEntry
]| println
!("{:?}", Context
::generate_cstring(path
).unwrap()))
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
);
540 static CONTEXT
: RefCell
<Option
<Context
>> = RefCell
::new(None
);
543 /// Holds the context needed for access to catalog and decoder
545 /// Calalog reader instance to navigate
546 catalog
: CatalogReader
<std
::fs
::File
>,
547 /// List of selected paths for restore
548 selected
: Vec
<MatchPattern
>,
549 /// Decoder instance for the current pxar archive
551 /// Root directory for the give archive as stored in the catalog
553 /// Stack of directories up to the current working directory
554 /// used for navigation and path completion.
555 current
: Vec
<DirEntry
>,
559 /// Execute `call` within a context providing a mut ref to `Context` instance.
560 fn with
<T
, F
>(call
: F
) -> Result
<T
, Error
>
562 F
: FnOnce(&mut Context
) -> Result
<T
, Error
>,
564 CONTEXT
.with(|cell
| {
565 let mut ctx
= cell
.borrow_mut();
566 call(&mut ctx
.as_mut().unwrap())
570 /// Generate CString from provided stack of `DirEntry`s.
571 fn generate_cstring(dir_stack
: &[DirEntry
]) -> Result
<CString
, Error
> {
572 let mut path
= vec
![b'
/'
];
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() {
580 Ok(unsafe { CString::from_vec_unchecked(path) }
)
583 /// Resolve the indirect path components and return an absolute path.
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.
589 fn canonical_path(&mut self, path
: &str) -> Result
<Vec
<DirEntry
>, Error
> {
591 return Ok(self.root
.clone());
594 let mut path_slice
= if path
.is_empty() {
595 // Fallback to root if no path was provided
596 return Ok(self.root
.clone());
601 let mut dir_stack
= if path_slice
.starts_with("/") {
602 // Absolute path, reduce view of slice and start from root
603 path_slice
= &path_slice
[1..];
606 // Relative path, start from current working directory
609 let should_end_dir
= if path_slice
.ends_with("/") {
610 path_slice
= &path_slice
[0..path_slice
.len() - 1];
615 for name
in path_slice
.split('
/'
) {
619 // Never pop archive root from stack
620 if dir_stack
.len() > 1 {
625 let entry
= self.catalog
.lookup(dir_stack
.last().unwrap(), name
.as_bytes())?
;
626 dir_stack
.push(entry
);
633 .ok_or_else(|| format_err
!("invalid path component"))?
636 bail
!("entry is not a directory");
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
!(
648 Self::generate_cstring(&self.current
)?
.to_string_lossy(),
654 /// Get the current size of the terminal
657 /// uses unsafe call to tty_ioctl, see man tty_ioctl(2)
658 fn get_terminal_size() -> (usize, usize) {
659 const TIOCGWINSZ
: libc
::c_ulong
= 0x5413;
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
669 let mut winsize
= WinSize
{
675 unsafe { libc::ioctl(libc::STDOUT_FILENO, TIOCGWINSZ, &mut winsize) }
;
676 (winsize
.ws_row
as usize, winsize
.ws_col
as usize)
679 /// Look up the entry given by a canonical absolute `path` in the archive.
681 /// This will actively navigate the archive by calling the corresponding
682 /// decoder functionalities and is therefore very expensive.
683 fn lookup(&mut self, absolute_path
: &[DirEntry
]) -> Result
<DirectoryEntry
, Error
> {
684 let mut current
= self.decoder
.root()?
;
685 // Ignore the archive root, don't need it.
686 for item
in absolute_path
.iter().skip(1) {
689 .lookup(¤t
, &OsStr
::from_bytes(&item
.name
))?
691 Some(item
) => current
= item
,
692 // This should not happen if catalog an archive are consistent.
693 None
=> bail
!("no such file or directory in archive - inconsistent catalog"),