use std::ffi::{CString, OsStr};
use std::io::Write;
use std::os::unix::ffi::OsStrExt;
-use std::path::Path;
+use std::path::{Component, Path, PathBuf};
use chrono::{Utc, offset::TimeZone};
-use failure::*;
+use anyhow::{bail, format_err, Error};
use nix::sys::stat::{Mode, SFlag};
use proxmox::api::{cli::*, *};
.arg_param(&["path"])
.completion_cb("path", Shell::complete_path)
)
+ .insert(
+ "clear-selected",
+ CliCommand::new(&API_METHOD_CLEAR_SELECTED_COMMAND)
+ )
.insert(
"restore-selected",
CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND)
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];
+ let path = CatalogPathStack::new(archive_root);
CONTEXT.with(|handle| {
let mut ctx = handle.borrow_mut();
catalog,
selected: Vec::new(),
decoder,
- root: root.clone(),
- current: root,
+ path,
});
});
continue;
}
};
- let _ = handle_command(helper.cmd_def(), "", args, None);
+
+ let rpcenv = CliEnvironment::new();
+ let _ = handle_command(helper.cmd_def(), "", args, rpcenv, None);
self.rl.add_history_entry(line);
self.update_prompt()?;
}
};
let current = if base.is_empty() {
- ctx.current.clone()
+ ctx.path.last().clone()
} else {
- ctx.canonical_path(base)?
+ let mut local = ctx.path.clone();
+ local.traverse(&PathBuf::from(base), &mut ctx.decoder, &mut ctx.catalog, false)?;
+ local.last().clone()
};
- let entries = match ctx.catalog.read_dir(¤t.last().unwrap()) {
+ let entries = match ctx.catalog.read_dir(¤t) {
Ok(entries) => entries,
Err(_) => return Ok(Vec::new()),
};
/// List the current working directory.
fn pwd_command() -> Result<(), Error> {
Context::with(|ctx| {
- let path = Context::generate_cstring(&ctx.current)?;
+ let path = ctx.path.generate_cstring()?;
let mut out = std::io::stdout();
out.write_all(&path.as_bytes())?;
out.write_all(&[b'\n'])?;
fn cd_command(path: Option<String>) -> Result<(), Error> {
Context::with(|ctx| {
let path = path.unwrap_or_default();
- let mut path = ctx.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();
+ if path.is_empty() {
+ ctx.path.clear();
+ return Ok(());
+ }
+ let mut local = ctx.path.clone();
+ local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, true)?;
+ if !local.last().is_directory() {
+ local.pop();
eprintln!("not a directory, fallback to parent directory");
}
- ctx.current = path;
+ ctx.path = local;
Ok(())
})
}
/// List the content of working directory or given path.
fn ls_command(path: Option<String>) -> Result<(), Error> {
Context::with(|ctx| {
- let parent = if let Some(path) = path {
- ctx.canonical_path(&path)?
- .last()
- .ok_or_else(|| format_err!("invalid path component"))?
- .clone()
+ let parent = if let Some(ref path) = path {
+ let mut local = ctx.path.clone();
+ local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, false)?;
+ local.last().clone()
} else {
- ctx.current.last().unwrap().clone()
+ ctx.path.last().clone()
};
let list = if parent.is_directory() {
ctx.catalog.read_dir(&parent)?
} else {
- vec![parent]
+ vec![parent.clone()]
};
if list.is_empty() {
/// which means reading over the network.
fn stat_command(path: String) -> Result<(), Error> {
Context::with(|ctx| {
- // First check if the file exists in the catalog, therefore avoiding
- // expensive calls to the decoder just to find out that there maybe is
- // no such entry.
- // This is done by calling canonical_path(), which returns the full path
- // if it exists, error otherwise.
- let path = ctx.canonical_path(&path)?;
- let item = ctx.lookup(&path)?;
+ let mut local = ctx.path.clone();
+ local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, false)?;
+ let canonical = local.canonical(&mut ctx.decoder, &mut ctx.catalog, false)?;
+ let item = canonical.lookup(&mut ctx.decoder)?;
let mut out = std::io::stdout();
out.write_all(b" File:\t")?;
out.write_all(item.filename.as_bytes())?;
/// if an invalid path was provided.
fn select_command(path: String) -> Result<(), Error> {
Context::with(|ctx| {
- // Calling canonical_path() makes sure the provided path is valid and
- // actually contained within the catalog and therefore also the archive.
- let path = ctx.canonical_path(&path)?;
- let pattern = MatchPattern::from_line(Context::generate_cstring(&path)?.as_bytes())?
+ let mut local = ctx.path.clone();
+ local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, false)?;
+ let canonical = local.canonical(&mut ctx.decoder, &mut ctx.catalog, false)?;
+ let pattern = MatchPattern::from_line(canonical.generate_cstring()?.as_bytes())?
.ok_or_else(|| format_err!("encountered invalid match pattern"))?;
if ctx.selected.iter().find(|p| **p == pattern).is_none() {
ctx.selected.push(pattern);
/// selected for restore.
fn deselect_command(path: String) -> Result<(), Error> {
Context::with(|ctx| {
- let path = ctx.canonical_path(&path)?;
- let mut pattern = MatchPattern::from_line(Context::generate_cstring(&path)?.as_bytes())?
+ let mut local = ctx.path.clone();
+ local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, false)?;
+ let canonical = local.canonical(&mut ctx.decoder, &mut ctx.catalog, false)?;
+ println!("{:?}", canonical.generate_cstring()?);
+ let mut pattern = MatchPattern::from_line(canonical.generate_cstring()?.as_bytes())?
.ok_or_else(|| format_err!("encountered invalid match pattern"))?;
if let Some(last) = ctx.selected.last() {
if last == &pattern {
})
}
+#[api( input: { properties: { } })]
+/// Clear the list of files selected for restore.
+fn clear_selected_command() -> Result<(), Error> {
+ Context::with(|ctx| {
+ ctx.selected.clear();
+ Ok(())
+ })
+}
+
#[api(
input: {
properties: {
for pattern in &ctx.selected {
slices.push(pattern.as_slice());
}
- let mut dir_stack = ctx.root.clone();
+ let mut dir_stack = vec![ctx.path.root()];
ctx.catalog.find(
&mut dir_stack,
&slices,
} else {
// Get the directory corresponding to the working directory from the
// archive.
- let cwd = ctx.current.clone();
- ctx.lookup(&cwd)?
+ let cwd = ctx.path.clone();
+ cwd.lookup(&mut ctx.decoder)?
};
ctx.decoder
/// Find entries in the catalog matching the given match pattern.
fn find_command(path: String, pattern: String, select: Option<bool>) -> Result<(), Error> {
Context::with(|ctx| {
- let path = ctx.canonical_path(&path)?;
- if !path.last().unwrap().is_directory() {
+ let mut local = ctx.path.clone();
+ local.traverse(&PathBuf::from(path), &mut ctx.decoder, &mut ctx.catalog, false)?;
+ let canonical = local.canonical(&mut ctx.decoder, &mut ctx.catalog, false)?;
+ if !local.last().is_directory() {
bail!("path should be a directory, not a file!");
}
let select = select.unwrap_or(false);
- let cpath = Context::generate_cstring(&path).unwrap();
+ let cpath = canonical.generate_cstring().unwrap();
let pattern = if pattern.starts_with("!") {
let mut buffer = vec![b'!'];
buffer.extend_from_slice(cpath.as_bytes());
// The match pattern all contain the prefix of the entry path in order to
// store them if selected, so the entry point for find is always the root
// directory.
- let mut dir_stack = ctx.root.clone();
+ let mut dir_stack = vec![ctx.path.root()];
ctx.catalog.find(
&mut dir_stack,
&slice,
selected: Vec<MatchPattern>,
/// Decoder instance for the current pxar archive
decoder: Decoder,
- /// Root directory for the give archive as stored in the catalog
- root: Vec<DirEntry>,
- /// Stack of directories up to the current working directory
- /// used for navigation and path completion.
- current: Vec<DirEntry>,
+ /// Handle catalog stuff
+ path: CatalogPathStack,
}
impl Context {
Ok(unsafe { CString::from_vec_unchecked(path) })
}
- /// Resolve the indirect path components and return an absolute path.
+ /// Generate the CString to display by readline based on
+ /// PROMPT_PREFIX, PROMPT and the current working directory.
+ fn generate_prompt(&self) -> Result<String, Error> {
+ let prompt = format!(
+ "{}{} {} ",
+ PROMPT_PREFIX,
+ self.path.generate_cstring()?.to_string_lossy(),
+ PROMPT,
+ );
+ Ok(prompt)
+ }
+}
+
+/// A valid path in the catalog starting from root.
+///
+/// Symlinks are stored by pushing the symlink entry and the target entry onto
+/// the stack. Allows to resolve all symlink in order to generate a canonical
+/// path needed for reading from the archive.
+#[derive(Clone)]
+struct CatalogPathStack {
+ stack: Vec<DirEntry>,
+ root: DirEntry,
+}
+
+impl CatalogPathStack {
+ /// Create a new stack with given root entry.
+ fn new(root: DirEntry) -> Self {
+ Self {
+ stack: Vec::new(),
+ root,
+ }
+ }
+
+ /// Get a clone of the root directories entry.
+ fn root(&self) -> DirEntry {
+ self.root.clone()
+ }
+
+ /// Remove all entries from the stack.
///
- /// 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: &str) -> Result<Vec<DirEntry>, Error> {
- if path == "/" {
- 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
- };
+ /// This equals to being at the root directory.
+ fn clear(&mut self) {
+ self.stack.clear();
+ }
- let mut dir_stack = if path_slice.starts_with("/") {
- // 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.current.clone()
- };
- let should_end_dir = if path_slice.ends_with("/") {
- path_slice = &path_slice[0..path_slice.len() - 1];
- true
+ /// Get a reference to the last entry on the stack.
+ fn last(&self) -> &DirEntry {
+ self.stack.last().unwrap_or(&self.root)
+ }
+
+ /// Check if the last entry is a symlink.
+ fn last_is_symlink(&self) -> bool {
+ self.last().is_symlink()
+ }
+
+ /// Check if the last entry is a directory.
+ fn last_is_directory(&self) -> bool {
+ self.last().is_directory()
+ }
+
+ /// Remove a component, if it was a symlink target,
+ /// this removes also the symlink entry.
+ fn pop(&mut self) -> Option<DirEntry> {
+ let entry = self.stack.pop()?;
+ if self.last_is_symlink() {
+ self.stack.pop()
} else {
- false
- };
- for name in path_slice.split('/') {
- match name {
- "" => continue, // Multiple successive slashes are valid and treated as one.
- "." => continue,
- ".." => {
- // Never pop archive root from stack
- if dir_stack.len() > 1 {
- dir_stack.pop();
+ Some(entry)
+ }
+ }
+
+ /// Add a component to the stack.
+ fn push(&mut self, entry: DirEntry) {
+ self.stack.push(entry)
+ }
+
+ /// Check if pushing the given entry onto the CatalogPathStack would create a
+ /// loop by checking if the same entry is already present.
+ fn creates_loop(&self, entry: &DirEntry) -> bool {
+ self.stack.iter().any(|comp| comp.eq(entry))
+ }
+
+ /// Starting from this path, traverse the catalog by the provided `path`.
+ fn traverse(
+ &mut self,
+ path: &PathBuf,
+ mut decoder: &mut Decoder,
+ mut catalog: &mut CatalogReader<std::fs::File>,
+ follow_final: bool,
+ ) -> Result<(), Error> {
+ for component in path.components() {
+ match component {
+ Component::RootDir => self.clear(),
+ Component::CurDir => continue,
+ Component::ParentDir => { self.pop(); }
+ Component::Normal(comp) => {
+ let entry = catalog.lookup(self.last(), comp.as_bytes())?;
+ if self.creates_loop(&entry) {
+ bail!("loop detected, will not follow");
+ }
+ self.push(entry);
+ if self.last_is_symlink() && follow_final {
+ let mut canonical = self.canonical(&mut decoder, &mut catalog, follow_final)?;
+ let target = canonical.pop().unwrap();
+ self.push(target);
}
}
- _ => {
- let entry = self.catalog.lookup(dir_stack.last().unwrap(), name.as_bytes())?;
- dir_stack.push(entry);
- }
+ Component::Prefix(_) => bail!("encountered prefix component. Non unix systems not supported."),
}
}
- if should_end_dir
- && !dir_stack
- .last()
- .ok_or_else(|| format_err!("invalid path component"))?
- .is_directory()
- {
+ if path.as_os_str().as_bytes().ends_with(b"/") && !self.last_is_directory() {
bail!("entry is not a directory");
}
-
- Ok(dir_stack)
+ Ok(())
}
- /// Generate the CString to display by readline based on
- /// PROMPT_PREFIX, PROMPT and the current working directory.
- fn generate_prompt(&self) -> Result<String, Error> {
- let prompt = format!(
- "{}{} {} ",
- PROMPT_PREFIX,
- Self::generate_cstring(&self.current)?.to_string_lossy(),
- PROMPT,
- );
- Ok(prompt)
+ /// Create a canonical version of this path with symlinks resolved.
+ ///
+ /// If resolve final is true, follow also an eventual symlink of the last
+ /// path component.
+ fn canonical(
+ &self,
+ mut decoder: &mut Decoder,
+ mut catalog: &mut CatalogReader<std::fs::File>,
+ resolve_final: bool,
+ ) -> Result<Self, Error> {
+ let mut canonical = CatalogPathStack::new(self.root.clone());
+ let mut iter = self.stack.iter().enumerate();
+ while let Some((index, component)) = iter.next() {
+ if component.is_directory() {
+ canonical.push(component.clone());
+ } else if component.is_symlink() {
+ canonical.push(component.clone());
+ if index != self.stack.len() - 1 || resolve_final {
+ // Get the symlink target by traversing the canonical path
+ // in the archive up to the symlink.
+ let archive_entry = canonical.lookup(&mut decoder)?;
+ canonical.pop();
+ // Resolving target means also ignoring the target in the iterator, so get it.
+ iter.next();
+ let target = archive_entry.target
+ .ok_or_else(|| format_err!("expected entry with symlink target."))?;
+ canonical.traverse(&target, &mut decoder, &mut catalog, resolve_final)?;
+ }
+ } else if index != self.stack.len() - 1 {
+ bail!("intermitten node is not symlink nor directory");
+ } else {
+ canonical.push(component.clone());
+ }
+ }
+ Ok(canonical)
}
- /// 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, Error> {
- let mut current = self.decoder.root()?;
- // 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))?
- {
+ /// Lookup this path in the archive using the provided decoder.
+ fn lookup(&self, decoder: &mut Decoder) -> Result<DirectoryEntry, Error> {
+ let mut current = decoder.root()?;
+ for component in self.stack.iter() {
+ match decoder.lookup(¤t, &OsStr::from_bytes(&component.name))? {
Some(item) => current = item,
// This should not happen if catalog an archive are consistent.
None => bail!("no such file or directory in archive - inconsistent catalog"),
}
Ok(current)
}
+
+ /// Generate a CString from this.
+ fn generate_cstring(&self) -> Result<CString, Error> {
+ let mut path = vec![b'/'];
+ let mut iter = self.stack.iter().enumerate();
+ while let Some((index, component)) = iter.next() {
+ if component.is_symlink() && index != self.stack.len() - 1 {
+ let (_, next) = iter.next()
+ .ok_or_else(|| format_err!("unresolved symlink encountered"))?;
+ // Display the name of the link, not the target
+ path.extend_from_slice(&component.name);
+ if next.is_directory() {
+ path.push(b'/');
+ }
+ } else {
+ path.extend_from_slice(&component.name);
+ if component.is_directory() {
+ path.push(b'/');
+ }
+ }
+ }
+ Ok(unsafe { CString::from_vec_unchecked(path) })
+ }
}