]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/backup/catalog_shell.rs
src/config/network.rs: make it compatible with pve
[proxmox-backup.git] / src / backup / catalog_shell.rs
index 645d7d4d86765aae9f5608842bbdd1c351afeb1a..7683ed075710dbd837ffba6b01e9cd39aa8577b6 100644 (file)
@@ -4,10 +4,10 @@ use std::convert::TryFrom;
 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::*, *};
@@ -64,6 +64,10 @@ pub fn catalog_shell_cli() -> CommandLineInterface {
                 .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)
@@ -101,7 +105,7 @@ impl Shell {
         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();
@@ -109,8 +113,7 @@ impl Shell {
                 catalog,
                 selected: Vec::new(),
                 decoder,
-                root: root.clone(),
-                current: root,
+                path,
             });
         });
 
@@ -137,7 +140,9 @@ impl Shell {
                     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()?;
         }
@@ -163,12 +168,14 @@ impl Shell {
             };
 
             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(&current.last().unwrap()) {
+            let entries = match ctx.catalog.read_dir(&current) {
                 Ok(entries) => entries,
                 Err(_) => return Ok(Vec::new()),
             };
@@ -194,7 +201,7 @@ impl Shell {
 /// 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'])?;
@@ -218,17 +225,17 @@ fn pwd_command() -> Result<(), Error> {
 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(())
     })
 }
@@ -247,19 +254,18 @@ fn cd_command(path: Option<String>) -> Result<(), Error> {
 /// 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() {
@@ -308,13 +314,10 @@ fn ls_command(path: Option<String>) -> Result<(), Error> {
 /// 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())?;
@@ -423,10 +426,10 @@ fn stat_command(path: String) -> Result<(), Error> {
 /// 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);
@@ -451,8 +454,11 @@ fn select_command(path: String) -> Result<(), Error> {
 /// 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 {
@@ -466,6 +472,15 @@ fn deselect_command(path: String) -> Result<(), Error> {
     })
 }
 
+#[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: {
@@ -516,7 +531,7 @@ fn list_selected_command(pattern: Option<bool>) -> Result<(), Error> {
             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,
@@ -561,8 +576,8 @@ fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error>
         } 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
@@ -593,13 +608,15 @@ fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error>
 /// 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());
@@ -618,7 +635,7 @@ fn find_command(path: String, pattern: String, select: Option<bool>) -> Result<(
         // 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,
@@ -647,11 +664,8 @@ struct Context {
     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 {
@@ -679,90 +693,164 @@ 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(&current, &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(&current, &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"),
@@ -770,4 +858,27 @@ impl Context {
         }
         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) })
+    }
 }