1 use std
::cell
::RefCell
;
2 use std
::collections
::{HashMap, HashSet}
;
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 CommandLineInterface
::Nested(map
)
84 /// Create a new shell for the given catalog and pxar archive.
86 mut catalog
: CatalogReader
<std
::fs
::File
>,
89 ) -> Result
<Self, Error
> {
90 let catalog_root
= catalog
.root()?
;
91 // The root for the given archive as stored in the catalog
92 let archive_root
= catalog
.lookup(&catalog_root
, archive_name
.as_bytes())?
;
93 let root
= vec
![archive_root
];
95 CONTEXT
.with(|handle
| {
96 let mut ctx
= handle
.borrow_mut();
99 selected
: HashSet
::new(),
106 let cli_helper
= CliHelper
::new(catalog_shell_cli());
107 let mut rl
= rustyline
::Editor
::<CliHelper
>::new();
108 rl
.set_helper(Some(cli_helper
));
110 Context
::with(|ctx
| {
113 prompt
: ctx
.generate_prompt()?
,
118 /// Start the interactive shell loop
119 pub fn shell(mut self) -> Result
<(), Error
> {
120 while let Ok(line
) = self.rl
.readline(&self.prompt
) {
121 let helper
= self.rl
.helper().unwrap();
122 let args
= match shellword_split(&line
) {
125 println
!("Error: {}", err
);
129 let _
= handle_command(helper
.cmd_def(), "", args
);
130 self.rl
.add_history_entry(line
);
131 self.update_prompt()?
;
136 /// Update the prompt to the new working directory
137 fn update_prompt(&mut self) -> Result
<(), Error
> {
138 Context
::with(|ctx
| {
139 self.prompt
= ctx
.generate_prompt()?
;
144 /// Completions for paths by lookup in the catalog
145 fn complete_path(complete_me
: &str, _map
: &HashMap
<String
, String
>) -> Vec
<String
> {
146 Context
::with(|ctx
| {
147 let (base
, to_complete
) = match complete_me
.rfind('
/'
) {
148 // Split at ind + 1 so the slash remains on base, ok also if
149 // ends in slash as split_at accepts up to length as index.
150 Some(ind
) => complete_me
.split_at(ind
+ 1),
151 None
=> ("", complete_me
),
154 let current
= if base
.is_empty() {
157 ctx
.canonical_path(base
)?
160 let entries
= match ctx
.catalog
.read_dir(¤t
.last().unwrap()) {
161 Ok(entries
) => entries
,
162 Err(_
) => return Ok(Vec
::new()),
165 let mut list
= Vec
::new();
166 for entry
in &entries
{
167 let mut name
= String
::from(base
);
168 if entry
.name
.starts_with(to_complete
.as_bytes()) {
169 name
.push_str(std
::str::from_utf8(&entry
.name
)?
);
170 if entry
.is_directory() {
182 #[api(input: { properties: {} })]
183 /// List the current working directory.
184 fn pwd_command() -> Result
<(), Error
> {
185 Context
::with(|ctx
| {
186 let path
= Context
::generate_cstring(&ctx
.current
)?
;
187 let mut out
= std
::io
::stdout();
188 out
.write_all(&path
.as_bytes())?
;
189 out
.write_all(&[b'
\n'
])?
;
201 description
: "target path."
206 /// Change the current working directory to the new directory
207 fn cd_command(path
: Option
<String
>) -> Result
<(), Error
> {
208 Context
::with(|ctx
| {
209 let path
= path
.unwrap_or_default();
210 let mut path
= ctx
.canonical_path(&path
)?
;
213 .ok_or_else(|| format_err
!("invalid path component"))?
216 // Change to the parent dir of the file instead
218 eprintln
!("not a directory, fallback to parent directory");
231 description
: "target path."
236 /// List the content of working directory or given path.
237 fn ls_command(path
: Option
<String
>) -> Result
<(), Error
> {
238 Context
::with(|ctx
| {
239 let parent
= if let Some(path
) = path
{
240 ctx
.canonical_path(&path
)?
242 .ok_or_else(|| format_err
!("invalid path component"))?
245 ctx
.current
.last().unwrap().clone()
248 let list
= if parent
.is_directory() {
249 ctx
.catalog
.read_dir(&parent
)?
257 let max
= list
.iter().max_by(|x
, y
| x
.name
.len().cmp(&y
.name
.len()));
258 let max
= match max
{
259 Some(dir_entry
) => dir_entry
.name
.len() + 1,
263 let (_rows
, mut cols
) = Context
::get_terminal_size();
266 let mut out
= std
::io
::stdout();
267 for (index
, item
) in list
.iter().enumerate() {
268 out
.write_all(&item
.name
)?
;
269 // Fill with whitespaces
270 out
.write_all(&vec
![b' '
; max
- item
.name
.len()])?
;
271 if index
% cols
== (cols
- 1) {
272 out
.write_all(&[b'
\n'
])?
;
275 // If the last line is not complete, add the newline
276 if list
.len() % cols
!= cols
- 1 {
277 out
.write_all(&[b'
\n'
])?
;
289 description
: "target path."
294 /// Read the metadata for a given directory entry.
296 /// This is expensive because the data has to be read from the pxar `Decoder`,
297 /// which means reading over the network.
298 fn stat_command(path
: String
) -> Result
<(), Error
> {
299 Context
::with(|ctx
| {
300 // First check if the file exists in the catalog, therefore avoiding
301 // expensive calls to the decoder just to find out that there maybe is
303 // This is done by calling canonical_path(), which returns the full path
304 // if it exists, error otherwise.
305 let path
= ctx
.canonical_path(&path
)?
;
306 let (item
, _attr
, size
) = ctx
.lookup(&path
)?
;
307 let mut out
= std
::io
::stdout();
308 out
.write_all(b
"File: ")?
;
309 out
.write_all(item
.filename
.as_bytes())?
;
310 out
.write_all(&[b'
\n'
])?
;
311 out
.write_all(format
!("Size: {}\n", size
).as_bytes())?
;
312 out
.write_all(b
"Type: ")?
;
313 match item
.entry
.mode
as u32 & libc
::S_IFMT
{
314 libc
::S_IFDIR
=> out
.write_all(b
"directory\n")?
,
315 libc
::S_IFREG
=> out
.write_all(b
"regular file\n")?
,
316 libc
::S_IFLNK
=> out
.write_all(b
"symbolic link\n")?
,
317 libc
::S_IFBLK
=> out
.write_all(b
"block special file\n")?
,
318 libc
::S_IFCHR
=> out
.write_all(b
"character special file\n")?
,
319 _
=> out
.write_all(b
"unknown\n")?
,
321 out
.write_all(format
!("Uid: {}\n", item
.entry
.uid
).as_bytes())?
;
322 out
.write_all(format
!("Gid: {}\n", item
.entry
.gid
).as_bytes())?
;
333 description
: "target path."
338 /// Select an entry for restore.
340 /// This will return an error if the entry is already present in the list or
341 /// if an invalid path was provided.
342 fn select_command(path
: String
) -> Result
<(), Error
> {
343 Context
::with(|ctx
| {
344 // Calling canonical_path() makes sure the provided path is valid and
345 // actually contained within the catalog and therefore also the archive.
346 let path
= ctx
.canonical_path(&path
)?
;
349 .insert(Context
::generate_cstring(&path
)?
.into_bytes())
353 bail
!("entry already selected for restore")
363 description
: "path to entry to remove from list."
368 /// Deselect an entry for restore.
370 /// This will return an error if the entry was not found in the list of entries
371 /// selected for restore.
372 fn deselect_command(path
: String
) -> Result
<(), Error
> {
373 Context
::with(|ctx
| {
374 let path
= ctx
.canonical_path(&path
)?
;
375 if ctx
.selected
.remove(&Context
::generate_cstring(&path
)?
.into_bytes()) {
378 bail
!("entry not selected for restore")
388 description
: "target path for restore on local filesystem."
393 /// Restore the selected entries to the given target path.
395 /// Target must not exist on the clients filesystem.
396 fn restore_selected_command(target
: String
) -> Result
<(), Error
> {
397 Context
::with(|ctx
| {
398 let mut list
= Vec
::new();
399 for path
in &ctx
.selected
{
400 let pattern
= MatchPattern
::from_line(path
)?
401 .ok_or_else(|| format_err
!("encountered invalid match pattern"))?
;
405 bail
!("no entries selected for restore");
408 // Entry point for the restore is always root here as the provided match
409 // patterns are relative to root as well.
410 let start_dir
= ctx
.decoder
.root()?
;
412 .restore(&start_dir
, &Path
::new(&target
), &list
)?
;
417 #[api( input: { properties: {} })]
418 /// List entries currently selected for restore.
419 fn list_selected_command() -> Result
<(), Error
> {
420 Context
::with(|ctx
| {
421 let mut out
= std
::io
::stdout();
422 for entry
in &ctx
.selected
{
423 out
.write_all(entry
)?
;
424 out
.write_all(&[b'
\n'
])?
;
436 description
: "target path for restore on local filesystem."
441 description
: "match pattern to limit files for restore."
446 /// Restore the sub-archive given by the current working directory to target.
448 /// By further providing a pattern, the restore can be limited to a narrower
449 /// subset of this sub-archive.
450 /// If pattern is not present or empty, the full archive is restored to target.
451 fn restore_command(target
: String
, pattern
: Option
<String
>) -> Result
<(), Error
> {
452 Context
::with(|ctx
| {
453 let pattern
= pattern
.unwrap_or_default();
454 let match_pattern
= match pattern
.as_str() {
455 "" | "/" | "." => Vec
::new(),
456 _
=> vec
![MatchPattern
::from_line(pattern
.as_bytes())?
.unwrap()],
458 // Decoder entry point for the restore.
459 let start_dir
= if pattern
.starts_with("/") {
462 // Get the directory corresponding to the working directory from the
464 let cwd
= ctx
.current
.clone();
465 let (dir
, _
, _
) = ctx
.lookup(&cwd
)?
;
470 .restore(&start_dir
, &Path
::new(&target
), &match_pattern
)?
;
476 static CONTEXT
: RefCell
<Option
<Context
>> = RefCell
::new(None
);
479 /// Holds the context needed for access to catalog and decoder
481 /// Calalog reader instance to navigate
482 catalog
: CatalogReader
<std
::fs
::File
>,
483 /// List of selected paths for restore
484 selected
: HashSet
<Vec
<u8>>,
485 /// Decoder instance for the current pxar archive
487 /// Root directory for the give archive as stored in the catalog
489 /// Stack of directories up to the current working directory
490 /// used for navigation and path completion.
491 current
: Vec
<DirEntry
>,
495 /// Execute `call` within a context providing a mut ref to `Context` instance.
496 fn with
<T
, F
>(call
: F
) -> Result
<T
, Error
>
498 F
: FnOnce(&mut Context
) -> Result
<T
, Error
>,
500 CONTEXT
.with(|cell
| {
501 let mut ctx
= cell
.borrow_mut();
502 call(&mut ctx
.as_mut().unwrap())
506 /// Generate CString from provided stack of `DirEntry`s.
507 fn generate_cstring(dir_stack
: &[DirEntry
]) -> Result
<CString
, Error
> {
508 let mut path
= vec
![b'
/'
];
509 // Skip the archive root, the '/' is displayed for it instead
510 for component
in dir_stack
.iter().skip(1) {
511 path
.extend_from_slice(&component
.name
);
512 if component
.is_directory() {
516 Ok(unsafe { CString::from_vec_unchecked(path) }
)
519 /// Resolve the indirect path components and return an absolute path.
521 /// This will actually navigate the filesystem tree to check that the
522 /// path is vaild and exists.
523 /// This does not include following symbolic links.
524 /// If None is given as path, only the root directory is returned.
525 fn canonical_path(&mut self, path
: &str) -> Result
<Vec
<DirEntry
>, Error
> {
527 return Ok(self.root
.clone());
530 let mut path_slice
= if path
.is_empty() {
531 // Fallback to root if no path was provided
532 return Ok(self.root
.clone());
537 let mut dir_stack
= if path_slice
.starts_with("/") {
538 // Absolute path, reduce view of slice and start from root
539 path_slice
= &path_slice
[1..];
542 // Relative path, start from current working directory
545 let should_end_dir
= if path_slice
.ends_with("/") {
546 path_slice
= &path_slice
[0..path_slice
.len() - 1];
551 for name
in path_slice
.split('
/'
) {
555 // Never pop archive root from stack
556 if dir_stack
.len() > 1 {
561 let entry
= self.catalog
.lookup(dir_stack
.last().unwrap(), name
.as_bytes())?
;
562 dir_stack
.push(entry
);
569 .ok_or_else(|| format_err
!("invalid path component"))?
572 bail
!("entry is not a directory");
578 /// Generate the CString to display by readline based on
579 /// PROMPT_PREFIX, PROMPT and the current working directory.
580 fn generate_prompt(&self) -> Result
<String
, Error
> {
581 let prompt
= format
!(
584 Self::generate_cstring(&self.current
)?
.to_string_lossy(),
590 /// Get the current size of the terminal
593 /// uses unsafe call to tty_ioctl, see man tty_ioctl(2)
594 fn get_terminal_size() -> (usize, usize) {
595 const TIOCGWINSZ
: libc
::c_ulong
= 0x5413;
599 ws_row
: libc
::c_ushort
,
600 ws_col
: libc
::c_ushort
,
601 _ws_xpixel
: libc
::c_ushort
, // unused
602 _ws_ypixel
: libc
::c_ushort
, // unused
605 let mut winsize
= WinSize
{
611 unsafe { libc::ioctl(libc::STDOUT_FILENO, TIOCGWINSZ, &mut winsize) }
;
612 (winsize
.ws_row
as usize, winsize
.ws_col
as usize)
615 /// Look up the entry given by a canonical absolute `path` in the archive.
617 /// This will actively navigate the archive by calling the corresponding
618 /// decoder functionalities and is therefore very expensive.
621 absolute_path
: &[DirEntry
],
622 ) -> Result
<(DirectoryEntry
, PxarAttributes
, u64), Error
> {
623 let mut current
= self.decoder
.root()?
;
624 let (_
, _
, mut attr
, mut size
) = self.decoder
.attributes(0)?
;
625 // Ignore the archive root, don't need it.
626 for item
in absolute_path
.iter().skip(1) {
629 .lookup(¤t
, &OsStr
::from_bytes(&item
.name
))?
631 Some((item
, item_attr
, item_size
)) => {
636 // This should not happen if catalog an archive are consistent.
637 None
=> bail
!("no such file or directory in archive - inconsistent catalog"),
640 Ok((current
, attr
, size
))