diff --git a/src/backup.rs b/src/backup.rs index 7a5068f0..6a18f39b 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -181,3 +181,9 @@ pub use backup_info::*; mod datastore; pub use datastore::*; + +mod readline; +pub use readline::*; + +mod catalog_shell; +pub use catalog_shell::*; diff --git a/src/backup/catalog_shell.rs b/src/backup/catalog_shell.rs new file mode 100644 index 00000000..9a7e5248 --- /dev/null +++ b/src/backup/catalog_shell.rs @@ -0,0 +1,573 @@ +use std::collections::HashSet; +use std::ffi::{CStr, CString, OsStr}; +use std::io::Write; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +use failure::*; +use libc; + +use crate::backup::*; +use crate::client::*; +use crate::pxar::*; + +use super::catalog::{CatalogReader, DirEntry}; +use super::readline::{Readline, Context}; + +/// State of the shell instance +pub struct Shell { + /// Readline context + rl: Readline, + /// List of paths selected for a restore + selected: HashSet>, + /// Decoder instance for the current pxar archive + decoder: Decoder, fn(&Path) -> Result<(), Error>>, + /// Root directory for the give archive as stored in the catalog + root: Vec, +} + +/// All supported commands of the shell +enum Command<'a> { + /// List the content of the current dir or in path, if provided + List(&'a [u8]), + /// Stat of the provided path + Stat(&'a [u8]), + /// Select the given entry for a restore + Select(&'a [u8]), + /// Remove the entry from the list of entries to restore + Deselect(&'a [u8]), + /// Restore an archive to the provided target, can be limited to files + /// matching the provided match pattern + Restore(&'a [u8], &'a [u8]), + /// Restore the selected entries to the provided target + RestoreSelected(&'a [u8]), + /// List the entries currently selected for restore + ListSelected, + /// Change the current working directory + ChangeDir(&'a [u8]), + /// Print the working directory + PrintWorkingDir, + /// Terminate the shell loop, returns from the shell + Quit, + /// Empty line from readline + Empty, +} + +const PROMPT_PREFIX: &str = "pxar:"; +const PROMPT_POST: &str = " > "; + +impl Shell { + pub fn new( + mut catalog: CatalogReader, + archive_name: &str, + decoder: Decoder, fn(&Path) -> Result<(), Error>> + ) -> Result { + let catalog_root = catalog.root()?; + // The root for the given archive as stored in the catalog + let archive_root = catalog.lookup(&catalog_root, archive_name.as_bytes())?; + let root = vec![archive_root]; + Ok(Self { + rl: Readline::new( + Self::generate_prompt(b"/"), + root.clone(), + Box::new(complete), + catalog, + ), + selected: HashSet::new(), + decoder, + root, + }) + } + + fn generate_prompt(path: &[u8]) -> CString { + let mut buffer = Vec::new(); + buffer.extend_from_slice(PROMPT_PREFIX.as_bytes()); + buffer.extend_from_slice(path); + buffer.extend_from_slice(PROMPT_POST.as_bytes()); + unsafe { CString::from_vec_unchecked(buffer) } + } + + /// Start the interactive shell loop + pub fn shell(mut self) -> Result<(), Error> { + while let Some(line) = self.rl.readline() { + let res = match self.parse_command(&line) { + Ok(Command::List(path)) => self.list(path).and_then(|list| { + Self::print_list(&list).map_err(|err| format_err!("{}", err))?; + Ok(()) + }), + Ok(Command::ChangeDir(path)) => self.change_dir(path), + Ok(Command::Restore(target, pattern)) => self.restore(target, pattern), + Ok(Command::Select(path)) => self.select(path), + Ok(Command::Deselect(path)) => self.deselect(path), + Ok(Command::RestoreSelected(target)) => self.restore_selected(target), + Ok(Command::Stat(path)) => self.stat(&path).and_then(|(item, attr, size)| { + Self::print_stat(&item, &attr, size)?; + Ok(()) + }), + Ok(Command::ListSelected) => { + self.list_selected().map_err(|err| format_err!("{}", err)) + } + Ok(Command::PrintWorkingDir) => self.pwd().and_then(|pwd| { + Self::print_pwd(&pwd).map_err(|err| format_err!("{}", err))?; + Ok(()) + }), + Ok(Command::Quit) => break, + Ok(Command::Empty) => continue, + Err(err) => Err(err), + }; + if let Err(err) = res { + println!("error: {}", err); + } + } + Ok(()) + } + + /// Command parser mapping the line returned by readline to a command. + fn parse_command<'a>(&self, line: &'a [u8]) -> Result, Error> { + // readline already handles tabs, so here we only split on spaces + let args: Vec<&[u8]> = line + .split(|b| *b == b' ') + .filter(|word| !word.is_empty()) + .collect(); + + if args.is_empty() { + return Ok(Command::Empty); + } + + match args[0] { + b"quit" => Ok(Command::Quit), + b"exit" => Ok(Command::Quit), + b"ls" => match args.len() { + 1 => Ok(Command::List(&[])), + 2 => Ok(Command::List(args[1])), + _ => bail!("To many parameters!"), + }, + b"pwd" => Ok(Command::PrintWorkingDir), + b"restore" => match args.len() { + 1 => bail!("no target provided"), + 2 => Ok(Command::Restore(args[1], &[])), + 4 => if args[2] == b"-p" { + Ok(Command::Restore(args[1], args[3])) + } else { + bail!("invalid parameter") + } + _ => bail!("to many parameters"), + }, + b"cd" => match args.len() { + 1 => Ok(Command::ChangeDir(&[])), + 2 => Ok(Command::ChangeDir(args[1])), + _ => bail!("to many parameters"), + }, + b"stat" => match args.len() { + 1 => bail!("no path provided"), + 2 => Ok(Command::Stat(args[1])), + _ => bail!("to many parameters"), + }, + b"select" => match args.len() { + 1 => bail!("no path provided"), + 2 => Ok(Command::Select(args[1])), + _ => bail!("to many parameters"), + }, + b"deselect" => match args.len() { + 1 => bail!("no path provided"), + 2 => Ok(Command::Deselect(args[1])), + _ => bail!("to many parameters"), + }, + b"selected" => match args.len() { + 1 => Ok(Command::ListSelected), + _ => bail!("to many parameters"), + }, + b"restore-selected" => match args.len() { + 1 => bail!("no path provided"), + 2 => Ok(Command::RestoreSelected(args[1])), + _ => bail!("to many parameters"), + }, + _ => bail!("command not known"), + } + } + + /// Get a mut ref to the context in order to be able to access the + /// catalog and the directory stack for the current working directory. + fn context(&mut self) -> &mut Context { + self.rl.context() + } + + /// Change the current working directory to the new directory + fn change_dir(&mut self, path: &[u8]) -> Result<(), Error> { + let mut path = self.canonical_path(path)?; + if !path + .last() + .ok_or_else(|| format_err!("invalid path component"))? + .is_directory() + { + // Change to the parent dir of the file instead + path.pop(); + eprintln!("not a directory, fallback to parent directory"); + } + self.context().current = path; + // Update the directory displayed in the prompt + let prompt = Self::generate_prompt(self.pwd()?.as_slice()); + self.rl.update_prompt(prompt); + Ok(()) + } + + /// List the content of a directory. + /// + /// Executed on files it returns the DirEntry of the file as single element + /// in the list. + fn list(&mut self, path: &[u8]) -> Result, Error> { + let parent = if !path.is_empty() { + self.canonical_path(path)? + .last() + .ok_or_else(|| format_err!("invalid path component"))? + .clone() + } else { + self.context().current.last().unwrap().clone() + }; + + let list = if parent.is_directory() { + self.context().catalog.read_dir(&parent)? + } else { + vec![parent] + }; + Ok(list) + } + + /// Return the current working directory as string + fn pwd(&mut self) -> Result, Error> { + Self::to_path(&self.context().current.clone()) + } + + /// Generate an absolute path from a directory stack. + fn to_path(dir_stack: &[DirEntry]) -> Result, Error> { + let mut path = vec![b'/']; + // Skip the archive root, '/' is displayed for it + for item in dir_stack.iter().skip(1) { + path.extend_from_slice(&item.name); + if item.is_directory() { + path.push(b'/'); + } + } + Ok(path) + } + + /// Resolve the indirect path components and return an absolute path. + /// + /// This will actually navigate the filesystem tree to check that the + /// path is vaild and exists. + /// This does not include following symbolic links. + /// If None is given as path, only the root directory is returned. + fn canonical_path(&mut self, path: &[u8]) -> Result, Error> { + if path == b"/" { + return Ok(self.root.clone()); + } + + let mut path_slice = if path.is_empty() { + // Fallback to root if no path was provided + return Ok(self.root.clone()); + } else { + path + }; + + let mut dir_stack = if path_slice.starts_with(&[b'/']) { + // Absolute path, reduce view of slice and start from root + path_slice = &path_slice[1..]; + self.root.clone() + } else { + // Relative path, start from current working directory + self.context().current.clone() + }; + let should_end_dir = if path_slice.ends_with(&[b'/']) { + path_slice = &path_slice[0..path_slice.len() - 1]; + true + } else { + false + }; + for name in path_slice.split(|b| *b == b'/') { + match name { + b"." => continue, + b".." => { + // Never pop archive root from stack + if dir_stack.len() > 1 { + dir_stack.pop(); + } + } + _ => { + let entry = self.context().catalog.lookup(dir_stack.last().unwrap(), name)?; + dir_stack.push(entry); + } + } + } + if should_end_dir && !dir_stack.last() + .ok_or_else(|| format_err!("invalid path component"))? + .is_directory() + { + bail!("entry is not a directory"); + } + + Ok(dir_stack) + } + + /// Read the metadata for a given directory entry. + /// + /// This is expensive because the data has to be read from the pxar `Decoder`, + /// which means reading over the network. + fn stat(&mut self, path: &[u8]) -> Result<(DirectoryEntry, PxarAttributes, u64), Error> { + // First check if the file exists in the catalog, therefore avoiding + // expensive calls to the decoder just to find out that there could be no + // such entry. This is done by calling canonical_path(), which returns + // the full path if it exists, error otherwise. + let path = self.canonical_path(path)?; + self.lookup(&path) + } + + /// Look up the entry given by a canonical absolute `path` in the archive. + /// + /// This will actively navigate the archive by calling the corresponding decoder + /// functionalities and is therefore very expensive. + fn lookup( + &mut self, + absolute_path: &[DirEntry], + ) -> Result<(DirectoryEntry, PxarAttributes, u64), Error> { + let mut current = self.decoder.root()?; + let (_, _, mut attr, mut size) = self.decoder.attributes(0)?; + // Ignore the archive root, don't need it. + for item in absolute_path.iter().skip(1) { + match self.decoder.lookup(¤t, &OsStr::from_bytes(&item.name))? { + Some((item, item_attr, item_size)) => { + current = item; + attr = item_attr; + size = item_size; + } + // This should not happen if catalog an archive are consistent. + None => bail!("no such file or directory in archive"), + } + } + Ok((current, attr, size)) + } + + /// Select an entry for restore. + /// + /// This will return an error if the entry is already present in the list or + /// if an invalid path was provided. + fn select(&mut self, path: &[u8]) -> Result<(), Error> { + // Calling canonical_path() makes sure the provided path is valid and + // actually contained within the catalog and therefore also the archive. + let path = self.canonical_path(path)?; + if self.selected.insert(Self::to_path(&path)?) { + Ok(()) + } else { + bail!("entry already selected for restore") + } + } + + /// Deselect an entry for restore. + /// + /// This will return an error if the entry was not found in the list of entries + /// selected for restore. + fn deselect(&mut self, path: &[u8]) -> Result<(), Error> { + if self.selected.remove(path) { + Ok(()) + } else { + bail!("entry not selected for restore") + } + } + + /// Restore the selected entries to the given target path. + /// + /// Target must not exist on the clients filesystem. + fn restore_selected(&mut self, target: &[u8]) -> Result<(), Error> { + let mut list = Vec::new(); + for path in &self.selected { + let pattern = MatchPattern::from_line(path)? + .ok_or_else(|| format_err!("encountered invalid match pattern"))?; + list.push(pattern); + } + + // Entry point for the restore is always root here as the provided match + // patterns are relative to root as well. + let start_dir = self.decoder.root()?; + let target: &OsStr = OsStrExt::from_bytes(target); + self.decoder.restore(&start_dir, &Path::new(target), &list)?; + Ok(()) + } + + /// List entries currently selected for restore. + fn list_selected(&self) -> Result<(), std::io::Error> { + let mut out = std::io::stdout(); + for entry in &self.selected { + out.write_all(entry)?; + out.write_all(&[b'\n'])?; + } + out.flush()?; + Ok(()) + } + + /// Restore the sub-archive given by the current working directory to target. + /// + /// By further providing a pattern, the restore can be limited to a narrower + /// subset of this sub-archive. + /// If pattern is an empty slice, the full dir is restored. + fn restore(&mut self, target: &[u8], pattern: &[u8]) -> Result<(), Error> { + let match_pattern = match pattern { + b"" | b"/" | b"." => Vec::new(), + _ => vec![MatchPattern::from_line(pattern)?.unwrap()], + }; + // Entry point for the restore. + let start_dir = if pattern.starts_with(&[b'/']) { + self.decoder.root()? + } else { + // Get the directory corresponding to the working directory from the + // archive. + let cwd = self.context().current.clone(); + let (dir, _, _) = self.lookup(&cwd)?; + dir + }; + + let target: &OsStr = OsStrExt::from_bytes(target); + self.decoder.restore(&start_dir, &Path::new(target), &match_pattern)?; + Ok(()) + } + + fn print_list(list: &Vec) -> Result<(), std::io::Error> { + let max = list + .iter() + .max_by(|x, y| x.name.len().cmp(&y.name.len())); + let max = match max { + Some(dir_entry) => dir_entry.name.len() + 1, + None => 0, + }; + let (_rows, mut cols) = Self::get_terminal_size(); + cols /= max; + let mut out = std::io::stdout(); + + for (index, item) in list.iter().enumerate() { + out.write_all(&item.name)?; + // Fill with whitespaces + out.write_all(&vec![b' '; max - item.name.len()])?; + if index % cols == (cols - 1) { + out.write_all(&[b'\n'])?; + } + } + // If the last line is not complete, add the newline + if list.len() % cols != cols - 1 { + out.write_all(&[b'\n'])?; + } + out.flush()?; + Ok(()) + } + + fn print_pwd(pwd: &[u8]) -> Result<(), std::io::Error> { + let mut out = std::io::stdout(); + out.write_all(pwd)?; + out.write_all(&[b'\n'])?; + out.flush()?; + Ok(()) + } + + fn print_stat(item: &DirectoryEntry, _attr: &PxarAttributes, size: u64) -> Result<(), std::io::Error> { + let mut out = std::io::stdout(); + out.write_all("File: ".as_bytes())?; + out.write_all(&item.filename.as_bytes())?; + out.write_all(&[b'\n'])?; + out.write_all(format!("Size: {}\n", size).as_bytes())?; + let mode = match item.entry.mode as u32 & libc::S_IFMT { + libc::S_IFDIR => "directory".as_bytes(), + libc::S_IFREG => "regular file".as_bytes(), + libc::S_IFLNK => "symbolic link".as_bytes(), + libc::S_IFBLK => "block special file".as_bytes(), + libc::S_IFCHR => "character special file".as_bytes(), + _ => "unknown".as_bytes(), + }; + out.write_all("Type: ".as_bytes())?; + out.write_all(&mode)?; + out.write_all(&[b'\n'])?; + out.write_all(format!("Uid: {}\n", item.entry.uid).as_bytes())?; + out.write_all(format!("Gid: {}\n", item.entry.gid).as_bytes())?; + out.flush()?; + Ok(()) + } + + /// Get the current size of the terminal + /// + /// uses tty_ioctl, see man tty_ioctl(2) + fn get_terminal_size() -> (usize, usize) { + + const TIOCGWINSZ: libc::c_ulong = 0x00005413; + + #[repr(C)] + struct WinSize { + ws_row: libc::c_ushort, + ws_col: libc::c_ushort, + _ws_xpixel: libc::c_ushort, // unused + _ws_ypixel: libc::c_ushort, // unused + } + + let mut winsize = WinSize { + ws_row: 0, + ws_col: 0, + _ws_xpixel: 0, + _ws_ypixel: 0, + }; + unsafe { libc::ioctl(libc::STDOUT_FILENO, TIOCGWINSZ, &mut winsize) }; + (winsize.ws_row as usize, winsize.ws_col as usize) + } +} + +/// Filename completion callback for the shell +// TODO: impl command completion. For now only filename completion. +fn complete( + ctx: &mut Context, + text: &CStr, + _start: usize, + _end: usize +) -> Vec { + let slices: Vec<_> = text + .to_bytes() + .split(|b| *b == b'/') + .collect(); + let to_complete = match slices.last() { + Some(last) => last, + None => return Vec::new(), + }; + let mut current = ctx.current.clone(); + let (prefix, entries) = { + let mut prefix = Vec::new(); + if slices.len() > 1 { + for component in &slices[..slices.len() - 1] { + if component == b"." { + continue; + } else if component == b".." { + // Never leave the current archive in the catalog + if current.len() > 1 { current.pop(); } + } else { + match ctx.catalog.lookup(current.last().unwrap(), component) { + Err(_) => return Vec::new(), + Ok(dir) => current.push(dir), + } + } + prefix.extend_from_slice(component); + prefix.push(b'/'); + } + } + let entries = match ctx.catalog.read_dir(¤t.last().unwrap()) { + Ok(entries) => entries, + Err(_) => return Vec::new(), + }; + (prefix, entries) + }; + // Create a list of completion strings which outlives this function + let mut list = Vec::new(); + for entry in &entries { + if entry.name.starts_with(to_complete) { + let mut name_buf = prefix.clone(); + name_buf.extend_from_slice(&entry.name); + if entry.is_directory() { + name_buf.push(b'/'); + } + let name = unsafe { CString::from_vec_unchecked(name_buf) }; + list.push(name); + } + } + list +}