d9b8e2c795
Errors while applying metadata will not be considered fatal by default using `pxar extract` unless `--strict` was passed in which case it'll bail out immediately. It'll still return an error exit status if something had failed along the way. Note that most other errors will still cause it to bail out (eg. errors creating files, or I/O errors while writing the contents). Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
1286 lines
39 KiB
Rust
1286 lines
39 KiB
Rust
use std::collections::HashMap;
|
|
use std::ffi::{CStr, CString, OsStr, OsString};
|
|
use std::future::Future;
|
|
use std::io::Write;
|
|
use std::mem;
|
|
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
|
use std::path::{Path, PathBuf};
|
|
use std::pin::Pin;
|
|
|
|
use anyhow::{bail, format_err, Error};
|
|
use nix::dir::Dir;
|
|
use nix::fcntl::OFlag;
|
|
use nix::sys::stat::Mode;
|
|
|
|
use pathpatterns::{MatchEntry, MatchList, MatchPattern, MatchType, PatternFlag};
|
|
use proxmox::api::api;
|
|
use proxmox::api::cli::{self, CliCommand, CliCommandMap, CliHelper, CommandLineInterface};
|
|
use proxmox::tools::fs::{create_path, CreateOptions};
|
|
use pxar::{EntryKind, Metadata};
|
|
|
|
use crate::backup::catalog::{self, DirEntryAttribute};
|
|
use crate::pxar::Flags;
|
|
use crate::pxar::fuse::{Accessor, FileEntry};
|
|
use crate::tools::runtime::block_in_place;
|
|
|
|
type CatalogReader = crate::backup::CatalogReader<std::fs::File>;
|
|
|
|
const MAX_SYMLINK_COUNT: usize = 40;
|
|
|
|
static mut SHELL: Option<usize> = None;
|
|
|
|
/// This list defines all the shell commands and their properties
|
|
/// using the api schema
|
|
pub fn catalog_shell_cli() -> CommandLineInterface {
|
|
CommandLineInterface::Nested(
|
|
CliCommandMap::new()
|
|
.insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND))
|
|
.insert(
|
|
"cd",
|
|
CliCommand::new(&API_METHOD_CD_COMMAND)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_path),
|
|
)
|
|
.insert(
|
|
"ls",
|
|
CliCommand::new(&API_METHOD_LS_COMMAND)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_path),
|
|
)
|
|
.insert(
|
|
"stat",
|
|
CliCommand::new(&API_METHOD_STAT_COMMAND)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_path),
|
|
)
|
|
.insert(
|
|
"select",
|
|
CliCommand::new(&API_METHOD_SELECT_COMMAND)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_path),
|
|
)
|
|
.insert(
|
|
"deselect",
|
|
CliCommand::new(&API_METHOD_DESELECT_COMMAND)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_path),
|
|
)
|
|
.insert(
|
|
"clear-selected",
|
|
CliCommand::new(&API_METHOD_CLEAR_SELECTED_COMMAND),
|
|
)
|
|
.insert(
|
|
"list-selected",
|
|
CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND),
|
|
)
|
|
.insert(
|
|
"restore-selected",
|
|
CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND)
|
|
.arg_param(&["target"])
|
|
.completion_cb("target", crate::tools::complete_file_name),
|
|
)
|
|
.insert(
|
|
"restore",
|
|
CliCommand::new(&API_METHOD_RESTORE_COMMAND)
|
|
.arg_param(&["target"])
|
|
.completion_cb("target", crate::tools::complete_file_name),
|
|
)
|
|
.insert(
|
|
"find",
|
|
CliCommand::new(&API_METHOD_FIND_COMMAND).arg_param(&["pattern"]),
|
|
)
|
|
.insert(
|
|
"exit",
|
|
CliCommand::new(&API_METHOD_EXIT),
|
|
)
|
|
.insert_help(),
|
|
)
|
|
}
|
|
|
|
fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
|
|
let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
|
|
match shell.complete_path(complete_me) {
|
|
Ok(list) => list,
|
|
Err(err) => {
|
|
eprintln!("error during completion: {}", err);
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
// just an empty wrapper so that it is displayed in help/docs, we check
|
|
// in the readloop for 'exit' again break
|
|
#[api(input: { properties: {} })]
|
|
/// Exit the shell
|
|
async fn exit() -> Result<(), Error> {
|
|
Ok(())
|
|
}
|
|
|
|
#[api(input: { properties: {} })]
|
|
/// List the current working directory.
|
|
async fn pwd_command() -> Result<(), Error> {
|
|
Shell::with(move |shell| shell.pwd()).await
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
type: String,
|
|
optional: true,
|
|
description: "target path."
|
|
}
|
|
}
|
|
}
|
|
)]
|
|
/// Change the current working directory to the new directory
|
|
async fn cd_command(path: Option<String>) -> Result<(), Error> {
|
|
let path = path.as_ref().map(Path::new);
|
|
Shell::with(move |shell| shell.cd(path)).await
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
type: String,
|
|
optional: true,
|
|
description: "target path."
|
|
}
|
|
}
|
|
}
|
|
)]
|
|
/// List the content of working directory or given path.
|
|
async fn ls_command(path: Option<String>) -> Result<(), Error> {
|
|
let path = path.as_ref().map(Path::new);
|
|
Shell::with(move |shell| shell.ls(path)).await
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
type: String,
|
|
description: "target path."
|
|
}
|
|
}
|
|
}
|
|
)]
|
|
/// Read the metadata for a given directory entry.
|
|
///
|
|
/// This is expensive because the data has to be read from the pxar archive, which means reading
|
|
/// over the network.
|
|
async fn stat_command(path: String) -> Result<(), Error> {
|
|
Shell::with(move |shell| shell.stat(PathBuf::from(path))).await
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
type: String,
|
|
description: "target path."
|
|
}
|
|
}
|
|
}
|
|
)]
|
|
/// 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.
|
|
async fn select_command(path: String) -> Result<(), Error> {
|
|
Shell::with(move |shell| shell.select(PathBuf::from(path))).await
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
type: String,
|
|
description: "path to entry to remove from list."
|
|
}
|
|
}
|
|
}
|
|
)]
|
|
/// Deselect an entry for restore.
|
|
///
|
|
/// This will return an error if the entry was not found in the list of entries
|
|
/// selected for restore.
|
|
async fn deselect_command(path: String) -> Result<(), Error> {
|
|
Shell::with(move |shell| shell.deselect(PathBuf::from(path))).await
|
|
}
|
|
|
|
#[api( input: { properties: { } })]
|
|
/// Clear the list of files selected for restore.
|
|
async fn clear_selected_command() -> Result<(), Error> {
|
|
Shell::with(move |shell| shell.deselect_all()).await
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
patterns: {
|
|
type: Boolean,
|
|
description: "List match patterns instead of the matching files.",
|
|
optional: true,
|
|
default: false,
|
|
}
|
|
}
|
|
}
|
|
)]
|
|
/// List entries currently selected for restore.
|
|
async fn list_selected_command(patterns: bool) -> Result<(), Error> {
|
|
Shell::with(move |shell| shell.list_selected(patterns)).await
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
pattern: {
|
|
type: String,
|
|
description: "Match pattern for matching files in the catalog."
|
|
},
|
|
select: {
|
|
type: bool,
|
|
optional: true,
|
|
default: false,
|
|
description: "Add matching filenames to list for restore."
|
|
}
|
|
}
|
|
}
|
|
)]
|
|
/// Find entries in the catalog matching the given match pattern.
|
|
async fn find_command(pattern: String, select: bool) -> Result<(), Error> {
|
|
Shell::with(move |shell| shell.find(pattern, select)).await
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
target: {
|
|
type: String,
|
|
description: "target path for restore on local filesystem."
|
|
}
|
|
}
|
|
}
|
|
)]
|
|
/// Restore the selected entries to the given target path.
|
|
///
|
|
/// Target must not exist on the clients filesystem.
|
|
async fn restore_selected_command(target: String) -> Result<(), Error> {
|
|
Shell::with(move |shell| shell.restore_selected(PathBuf::from(target))).await
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
target: {
|
|
type: String,
|
|
description: "target path for restore on local filesystem."
|
|
},
|
|
pattern: {
|
|
type: String,
|
|
optional: true,
|
|
description: "match pattern to limit files for restore."
|
|
}
|
|
}
|
|
}
|
|
)]
|
|
/// 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 not present or empty, the full archive is restored to target.
|
|
async fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> {
|
|
Shell::with(move |shell| shell.restore(PathBuf::from(target), pattern)).await
|
|
}
|
|
|
|
/// TODO: Should we use this to fix `step()`? Make path resolution behave more like described in
|
|
/// the path_resolution(7) man page.
|
|
///
|
|
/// The `Path` type's component iterator does not tell us anything about trailing slashes or
|
|
/// trailing `Component::CurDir` entries. Since we only support regular paths we'll roll our own
|
|
/// here:
|
|
enum PathComponent<'a> {
|
|
Root,
|
|
CurDir,
|
|
ParentDir,
|
|
Normal(&'a OsStr),
|
|
TrailingSlash,
|
|
}
|
|
|
|
struct PathComponentIter<'a> {
|
|
path: &'a [u8],
|
|
state: u8, // 0=beginning, 1=ongoing, 2=trailing, 3=finished (fused)
|
|
}
|
|
|
|
impl std::iter::FusedIterator for PathComponentIter<'_> {}
|
|
|
|
impl<'a> Iterator for PathComponentIter<'a> {
|
|
type Item = PathComponent<'a>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
if self.path.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
if self.state == 0 {
|
|
self.state = 1;
|
|
if self.path[0] == b'/' {
|
|
// absolute path
|
|
self.path = &self.path[1..];
|
|
return Some(PathComponent::Root);
|
|
}
|
|
}
|
|
|
|
// skip slashes
|
|
let had_slashes = self.path[0] == b'/';
|
|
while self.path.get(0).copied() == Some(b'/') {
|
|
self.path = &self.path[1..];
|
|
}
|
|
|
|
Some(match self.path {
|
|
[] if had_slashes => PathComponent::TrailingSlash,
|
|
[] => return None,
|
|
[b'.'] | [b'.', b'/', ..] => {
|
|
self.path = &self.path[1..];
|
|
PathComponent::CurDir
|
|
}
|
|
[b'.', b'.'] | [b'.', b'.', b'/', ..] => {
|
|
self.path = &self.path[2..];
|
|
PathComponent::ParentDir
|
|
}
|
|
_ => {
|
|
let end = self
|
|
.path
|
|
.iter()
|
|
.position(|&b| b == b'/')
|
|
.unwrap_or(self.path.len());
|
|
let (out, rest) = self.path.split_at(end);
|
|
self.path = rest;
|
|
PathComponent::Normal(OsStr::from_bytes(out))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct Shell {
|
|
/// Readline instance handling input and callbacks
|
|
rl: rustyline::Editor<CliHelper>,
|
|
|
|
/// Interactive prompt.
|
|
prompt: String,
|
|
|
|
/// Calalog reader instance to navigate
|
|
catalog: CatalogReader,
|
|
|
|
/// List of selected paths for restore
|
|
selected: HashMap<OsString, MatchEntry>,
|
|
|
|
/// pxar accessor instance for the current pxar archive
|
|
accessor: Accessor,
|
|
|
|
/// The current position in the archive.
|
|
position: Vec<PathStackEntry>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct PathStackEntry {
|
|
/// This is always available. We mainly navigate through the catalog.
|
|
catalog: catalog::DirEntry,
|
|
|
|
/// Whenever we need something from the actual archive we fill this out. This is cached along
|
|
/// the entire path.
|
|
pxar: Option<FileEntry>,
|
|
}
|
|
|
|
impl PathStackEntry {
|
|
fn new(dir_entry: catalog::DirEntry) -> Self {
|
|
Self {
|
|
pxar: None,
|
|
catalog: dir_entry,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Shell {
|
|
/// Create a new shell for the given catalog and pxar archive.
|
|
pub async fn new(
|
|
mut catalog: CatalogReader,
|
|
archive_name: &str,
|
|
archive: Accessor,
|
|
) -> Result<Self, Error> {
|
|
let cli_helper = CliHelper::new(catalog_shell_cli());
|
|
let mut rl = rustyline::Editor::<CliHelper>::new();
|
|
rl.set_helper(Some(cli_helper));
|
|
|
|
let catalog_root = catalog.root()?;
|
|
let archive_root = catalog
|
|
.lookup(&catalog_root, archive_name.as_bytes())?
|
|
.ok_or_else(|| format_err!("archive not found in catalog"))?;
|
|
let position = vec![PathStackEntry::new(archive_root)];
|
|
|
|
let mut this = Self {
|
|
rl,
|
|
prompt: String::new(),
|
|
catalog,
|
|
selected: HashMap::new(),
|
|
accessor: archive,
|
|
position,
|
|
};
|
|
this.update_prompt();
|
|
Ok(this)
|
|
}
|
|
|
|
async fn with<'a, Fut, R, F>(call: F) -> Result<R, Error>
|
|
where
|
|
F: FnOnce(&'a mut Shell) -> Fut,
|
|
Fut: Future<Output = Result<R, Error>>,
|
|
F: 'a,
|
|
Fut: 'a,
|
|
R: 'static,
|
|
{
|
|
let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
|
|
let result = call(&mut *shell).await;
|
|
result
|
|
}
|
|
|
|
pub async fn shell(mut self) -> Result<(), Error> {
|
|
let this = &mut self;
|
|
unsafe {
|
|
SHELL = Some(this as *mut Shell as usize);
|
|
}
|
|
while let Ok(line) = this.rl.readline(&this.prompt) {
|
|
if line == "exit" {
|
|
break;
|
|
}
|
|
let helper = this.rl.helper().unwrap();
|
|
let args = match cli::shellword_split(&line) {
|
|
Ok(args) => args,
|
|
Err(err) => {
|
|
println!("Error: {}", err);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let _ =
|
|
cli::handle_command_future(helper.cmd_def(), "", args, cli::CliEnvironment::new())
|
|
.await;
|
|
this.rl.add_history_entry(line);
|
|
this.update_prompt();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn update_prompt(&mut self) {
|
|
self.prompt = "pxar:".to_string();
|
|
if self.position.len() <= 1 {
|
|
self.prompt.push('/');
|
|
} else {
|
|
for p in self.position.iter().skip(1) {
|
|
if !p.catalog.name.starts_with(b"/") {
|
|
self.prompt.push('/');
|
|
}
|
|
match std::str::from_utf8(&p.catalog.name) {
|
|
Ok(entry) => self.prompt.push_str(entry),
|
|
Err(_) => self.prompt.push_str("<non-utf8-dir>"),
|
|
}
|
|
}
|
|
}
|
|
self.prompt.push_str(" > ");
|
|
}
|
|
|
|
async fn pwd(&mut self) -> Result<(), Error> {
|
|
let stack = Self::lookup(
|
|
&self.position,
|
|
&mut self.catalog,
|
|
&self.accessor,
|
|
None,
|
|
&mut Some(0),
|
|
)
|
|
.await?;
|
|
let path = Self::format_path_stack(&stack);
|
|
println!("{:?}", path);
|
|
Ok(())
|
|
}
|
|
|
|
fn new_path_stack(&self) -> Vec<PathStackEntry> {
|
|
self.position[..1].to_vec()
|
|
}
|
|
|
|
async fn resolve_symlink(
|
|
stack: &mut Vec<PathStackEntry>,
|
|
catalog: &mut CatalogReader,
|
|
accessor: &Accessor,
|
|
follow_symlinks: &mut Option<usize>,
|
|
) -> Result<(), Error> {
|
|
if let Some(ref mut symlink_count) = follow_symlinks {
|
|
*symlink_count += 1;
|
|
if *symlink_count > MAX_SYMLINK_COUNT {
|
|
bail!("too many levels of symbolic links");
|
|
}
|
|
|
|
let file = Self::walk_pxar_archive(accessor, &mut stack[..]).await?;
|
|
|
|
let path = match file.entry().kind() {
|
|
EntryKind::Symlink(symlink) => Path::new(symlink.as_os_str()),
|
|
_ => bail!("symlink in the catalog was not a symlink in the archive"),
|
|
};
|
|
|
|
let new_stack =
|
|
Self::lookup(&stack, &mut *catalog, accessor, Some(path), follow_symlinks).await?;
|
|
|
|
*stack = new_stack;
|
|
|
|
Ok(())
|
|
} else {
|
|
bail!("target is a symlink");
|
|
}
|
|
}
|
|
|
|
/// Walk a path and add it to the path stack.
|
|
///
|
|
/// If the symlink count is used, symlinks will be followed, until we hit the cap and error
|
|
/// out.
|
|
async fn step(
|
|
stack: &mut Vec<PathStackEntry>,
|
|
catalog: &mut CatalogReader,
|
|
accessor: &Accessor,
|
|
component: std::path::Component<'_>,
|
|
follow_symlinks: &mut Option<usize>,
|
|
) -> Result<(), Error> {
|
|
use std::path::Component;
|
|
match component {
|
|
Component::Prefix(_) => bail!("invalid path component (prefix)"),
|
|
Component::RootDir => stack.truncate(1),
|
|
Component::CurDir => {
|
|
if stack.last().unwrap().catalog.is_symlink() {
|
|
Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?;
|
|
}
|
|
}
|
|
Component::ParentDir => drop(stack.pop()),
|
|
Component::Normal(entry) => {
|
|
if stack.last().unwrap().catalog.is_symlink() {
|
|
Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?;
|
|
}
|
|
match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? {
|
|
Some(dir) => stack.push(PathStackEntry::new(dir)),
|
|
None => bail!("no such file or directory: {:?}", entry),
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn step_nofollow(
|
|
stack: &mut Vec<PathStackEntry>,
|
|
catalog: &mut CatalogReader,
|
|
component: std::path::Component<'_>,
|
|
) -> Result<(), Error> {
|
|
use std::path::Component;
|
|
match component {
|
|
Component::Prefix(_) => bail!("invalid path component (prefix)"),
|
|
Component::RootDir => stack.truncate(1),
|
|
Component::CurDir => {
|
|
if stack.last().unwrap().catalog.is_symlink() {
|
|
bail!("target is a symlink");
|
|
}
|
|
}
|
|
Component::ParentDir => drop(stack.pop()),
|
|
Component::Normal(entry) => {
|
|
if stack.last().unwrap().catalog.is_symlink() {
|
|
bail!("target is a symlink");
|
|
} else {
|
|
match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? {
|
|
Some(dir) => stack.push(PathStackEntry::new(dir)),
|
|
None => bail!("no such file or directory: {:?}", entry),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The pxar accessor is required to resolve symbolic links
|
|
async fn walk_catalog(
|
|
stack: &mut Vec<PathStackEntry>,
|
|
catalog: &mut CatalogReader,
|
|
accessor: &Accessor,
|
|
path: &Path,
|
|
follow_symlinks: &mut Option<usize>,
|
|
) -> Result<(), Error> {
|
|
for c in path.components() {
|
|
Self::step(stack, catalog, accessor, c, follow_symlinks).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Non-async version cannot follow symlinks.
|
|
fn walk_catalog_nofollow(
|
|
stack: &mut Vec<PathStackEntry>,
|
|
catalog: &mut CatalogReader,
|
|
path: &Path,
|
|
) -> Result<(), Error> {
|
|
for c in path.components() {
|
|
Self::step_nofollow(stack, catalog, c)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// This assumes that there are no more symlinks in the path stack.
|
|
async fn walk_pxar_archive(
|
|
accessor: &Accessor,
|
|
mut stack: &mut [PathStackEntry],
|
|
) -> Result<FileEntry, Error> {
|
|
if stack[0].pxar.is_none() {
|
|
stack[0].pxar = Some(accessor.open_root().await?.lookup_self().await?);
|
|
}
|
|
|
|
// Now walk the directory stack:
|
|
let mut at = 1;
|
|
while at < stack.len() {
|
|
if stack[at].pxar.is_some() {
|
|
at += 1;
|
|
continue;
|
|
}
|
|
|
|
let parent = stack[at - 1].pxar.as_ref().unwrap();
|
|
let dir = parent.enter_directory().await?;
|
|
let name = Path::new(OsStr::from_bytes(&stack[at].catalog.name));
|
|
stack[at].pxar = Some(
|
|
dir.lookup(name)
|
|
.await?
|
|
.ok_or_else(|| format_err!("no such entry in pxar file: {:?}", name))?,
|
|
);
|
|
|
|
at += 1;
|
|
}
|
|
|
|
Ok(stack.last().unwrap().pxar.clone().unwrap())
|
|
}
|
|
|
|
fn complete_path(&mut self, input: &str) -> Result<Vec<String>, Error> {
|
|
let mut tmp_stack;
|
|
let (parent, base, part) = match input.rfind('/') {
|
|
Some(ind) => {
|
|
let (base, part) = input.split_at(ind + 1);
|
|
let path = PathBuf::from(base);
|
|
if path.is_absolute() {
|
|
tmp_stack = self.new_path_stack();
|
|
} else {
|
|
tmp_stack = self.position.clone();
|
|
}
|
|
Self::walk_catalog_nofollow(&mut tmp_stack, &mut self.catalog, &path)?;
|
|
(&tmp_stack.last().unwrap().catalog, base, part)
|
|
}
|
|
None => (&self.position.last().unwrap().catalog, "", input),
|
|
};
|
|
|
|
let entries = self.catalog.read_dir(parent)?;
|
|
|
|
let mut out = Vec::new();
|
|
for entry in entries {
|
|
let mut name = base.to_string();
|
|
if entry.name.starts_with(part.as_bytes()) {
|
|
name.push_str(std::str::from_utf8(&entry.name)?);
|
|
if entry.is_directory() {
|
|
name.push('/');
|
|
}
|
|
out.push(name);
|
|
}
|
|
}
|
|
|
|
Ok(out)
|
|
}
|
|
|
|
// Break async recursion here: lookup -> walk_catalog -> step -> lookup
|
|
fn lookup<'future, 's, 'c, 'a, 'p, 'y>(
|
|
stack: &'s [PathStackEntry],
|
|
catalog: &'c mut CatalogReader,
|
|
accessor: &'a Accessor,
|
|
path: Option<&'p Path>,
|
|
follow_symlinks: &'y mut Option<usize>,
|
|
) -> Pin<Box<dyn Future<Output = Result<Vec<PathStackEntry>, Error>> + Send + 'future>>
|
|
where
|
|
's: 'future,
|
|
'c: 'future,
|
|
'a: 'future,
|
|
'p: 'future,
|
|
'y: 'future,
|
|
{
|
|
Box::pin(async move {
|
|
Ok(match path {
|
|
None => stack.to_vec(),
|
|
Some(path) => {
|
|
let mut stack = if path.is_absolute() {
|
|
stack[..1].to_vec()
|
|
} else {
|
|
stack.to_vec()
|
|
};
|
|
Self::walk_catalog(&mut stack, catalog, accessor, path, follow_symlinks)
|
|
.await?;
|
|
stack
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
async fn ls(&mut self, path: Option<&Path>) -> Result<(), Error> {
|
|
let stack = Self::lookup(
|
|
&self.position,
|
|
&mut self.catalog,
|
|
&self.accessor,
|
|
path,
|
|
&mut Some(0),
|
|
)
|
|
.await?;
|
|
|
|
let last = stack.last().unwrap();
|
|
if last.catalog.is_directory() {
|
|
let items = self.catalog.read_dir(&stack.last().unwrap().catalog)?;
|
|
let mut out = std::io::stdout();
|
|
// FIXME: columnize
|
|
for item in items {
|
|
out.write_all(&item.name)?;
|
|
out.write_all(b"\n")?;
|
|
}
|
|
} else {
|
|
let mut out = std::io::stdout();
|
|
out.write_all(&last.catalog.name)?;
|
|
out.write_all(b"\n")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn stat(&mut self, path: PathBuf) -> Result<(), Error> {
|
|
let mut stack = Self::lookup(
|
|
&self.position,
|
|
&mut self.catalog,
|
|
&self.accessor,
|
|
Some(&path),
|
|
&mut Some(0),
|
|
)
|
|
.await?;
|
|
|
|
let file = Self::walk_pxar_archive(&self.accessor, &mut stack).await?;
|
|
std::io::stdout()
|
|
.write_all(crate::pxar::format_multi_line_entry(file.entry()).as_bytes())?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn cd(&mut self, path: Option<&Path>) -> Result<(), Error> {
|
|
match path {
|
|
Some(path) => {
|
|
let new_position = Self::lookup(
|
|
&self.position,
|
|
&mut self.catalog,
|
|
&self.accessor,
|
|
Some(path),
|
|
&mut None,
|
|
)
|
|
.await?;
|
|
if !new_position.last().unwrap().catalog.is_directory() {
|
|
bail!("not a directory");
|
|
}
|
|
self.position = new_position;
|
|
}
|
|
None => self.position.truncate(1),
|
|
}
|
|
self.update_prompt();
|
|
Ok(())
|
|
}
|
|
|
|
/// This stack must have been canonicalized already!
|
|
fn format_path_stack(stack: &[PathStackEntry]) -> OsString {
|
|
if stack.len() <= 1 {
|
|
return OsString::from("/");
|
|
}
|
|
|
|
let mut out = OsString::new();
|
|
for c in stack.iter().skip(1) {
|
|
out.push("/");
|
|
out.push(OsStr::from_bytes(&c.catalog.name));
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
async fn select(&mut self, path: PathBuf) -> Result<(), Error> {
|
|
let stack = Self::lookup(
|
|
&self.position,
|
|
&mut self.catalog,
|
|
&self.accessor,
|
|
Some(&path),
|
|
&mut Some(0),
|
|
)
|
|
.await?;
|
|
|
|
let path = Self::format_path_stack(&stack);
|
|
let entry = MatchEntry::include(MatchPattern::Literal(path.as_bytes().to_vec()));
|
|
if self.selected.insert(path.clone(), entry).is_some() {
|
|
println!("path already selected: {:?}", path);
|
|
} else {
|
|
println!("added path: {:?}", path);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn deselect(&mut self, path: PathBuf) -> Result<(), Error> {
|
|
let stack = Self::lookup(
|
|
&self.position,
|
|
&mut self.catalog,
|
|
&self.accessor,
|
|
Some(&path),
|
|
&mut Some(0),
|
|
)
|
|
.await?;
|
|
|
|
let path = Self::format_path_stack(&stack);
|
|
|
|
if self.selected.remove(&path).is_some() {
|
|
println!("removed path from selection: {:?}", path);
|
|
} else {
|
|
println!("path not selected: {:?}", path);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn deselect_all(&mut self) -> Result<(), Error> {
|
|
self.selected.clear();
|
|
println!("cleared selection");
|
|
Ok(())
|
|
}
|
|
|
|
async fn list_selected(&mut self, patterns: bool) -> Result<(), Error> {
|
|
if patterns {
|
|
self.list_selected_patterns().await
|
|
} else {
|
|
self.list_matching_files().await
|
|
}
|
|
}
|
|
|
|
async fn list_selected_patterns(&self) -> Result<(), Error> {
|
|
for entry in self.selected.keys() {
|
|
println!("{:?}", entry);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn build_match_list(&self) -> Vec<MatchEntry> {
|
|
let mut list = Vec::with_capacity(self.selected.len());
|
|
for entry in self.selected.values() {
|
|
list.push(entry.clone());
|
|
}
|
|
list
|
|
}
|
|
|
|
async fn list_matching_files(&mut self) -> Result<(), Error> {
|
|
let matches = self.build_match_list();
|
|
|
|
self.catalog.find(
|
|
&self.position[0].catalog,
|
|
&mut Vec::new(),
|
|
&matches,
|
|
&mut |path: &[u8]| -> Result<(), Error> {
|
|
let mut out = std::io::stdout();
|
|
out.write_all(path)?;
|
|
out.write_all(b"\n")?;
|
|
Ok(())
|
|
},
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn find(&mut self, pattern: String, select: bool) -> Result<(), Error> {
|
|
let pattern_os = OsString::from(pattern.clone());
|
|
let pattern_entry =
|
|
MatchEntry::parse_pattern(pattern, PatternFlag::PATH_NAME, MatchType::Include)?;
|
|
|
|
let mut found_some = false;
|
|
self.catalog.find(
|
|
&self.position[0].catalog,
|
|
&mut Vec::new(),
|
|
&[&pattern_entry],
|
|
&mut |path: &[u8]| -> Result<(), Error> {
|
|
found_some = true;
|
|
let mut out = std::io::stdout();
|
|
out.write_all(path)?;
|
|
out.write_all(b"\n")?;
|
|
Ok(())
|
|
},
|
|
)?;
|
|
|
|
if found_some && select {
|
|
self.selected.insert(pattern_os, pattern_entry);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn restore_selected(&mut self, destination: PathBuf) -> Result<(), Error> {
|
|
if self.selected.is_empty() {
|
|
bail!("no entries selected");
|
|
}
|
|
|
|
let match_list = self.build_match_list();
|
|
|
|
self.restore_with_match_list(destination, &match_list).await
|
|
}
|
|
|
|
async fn restore(
|
|
&mut self,
|
|
destination: PathBuf,
|
|
pattern: Option<String>,
|
|
) -> Result<(), Error> {
|
|
let tmp;
|
|
let match_list: &[MatchEntry] = match pattern {
|
|
None => &[],
|
|
Some(pattern) => {
|
|
tmp = [MatchEntry::parse_pattern(
|
|
pattern,
|
|
PatternFlag::PATH_NAME,
|
|
MatchType::Include,
|
|
)?];
|
|
&tmp
|
|
}
|
|
};
|
|
|
|
self.restore_with_match_list(destination, match_list).await
|
|
}
|
|
|
|
async fn restore_with_match_list(
|
|
&mut self,
|
|
destination: PathBuf,
|
|
match_list: &[MatchEntry],
|
|
) -> Result<(), Error> {
|
|
create_path(
|
|
&destination,
|
|
None,
|
|
Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
|
|
)
|
|
.map_err(|err| format_err!("error creating directory {:?}: {}", destination, err))?;
|
|
|
|
let rootdir = Dir::open(
|
|
&destination,
|
|
OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
|
|
Mode::empty(),
|
|
)
|
|
.map_err(|err| {
|
|
format_err!("unable to open target directory {:?}: {}", destination, err,)
|
|
})?;
|
|
|
|
let mut dir_stack = self.new_path_stack();
|
|
Self::walk_pxar_archive(&self.accessor, &mut dir_stack).await?;
|
|
let root_meta = dir_stack
|
|
.last()
|
|
.unwrap()
|
|
.pxar
|
|
.as_ref()
|
|
.unwrap()
|
|
.entry()
|
|
.metadata()
|
|
.clone();
|
|
|
|
let extractor = crate::pxar::extract::Extractor::new(rootdir, root_meta, true, Flags::DEFAULT);
|
|
|
|
let mut extractor = ExtractorState::new(
|
|
&mut self.catalog,
|
|
dir_stack,
|
|
extractor,
|
|
&match_list,
|
|
&self.accessor,
|
|
)?;
|
|
|
|
extractor.extract().await
|
|
}
|
|
}
|
|
|
|
enum LoopState {
|
|
Break,
|
|
Continue,
|
|
}
|
|
|
|
struct ExtractorState<'a> {
|
|
path: Vec<u8>,
|
|
path_len: usize,
|
|
path_len_stack: Vec<usize>,
|
|
|
|
dir_stack: Vec<PathStackEntry>,
|
|
|
|
matches: bool,
|
|
matches_stack: Vec<bool>,
|
|
|
|
read_dir: <Vec<catalog::DirEntry> as IntoIterator>::IntoIter,
|
|
read_dir_stack: Vec<<Vec<catalog::DirEntry> as IntoIterator>::IntoIter>,
|
|
|
|
extractor: crate::pxar::extract::Extractor,
|
|
|
|
catalog: &'a mut CatalogReader,
|
|
match_list: &'a [MatchEntry],
|
|
accessor: &'a Accessor,
|
|
}
|
|
|
|
impl<'a> ExtractorState<'a> {
|
|
pub fn new(
|
|
catalog: &'a mut CatalogReader,
|
|
dir_stack: Vec<PathStackEntry>,
|
|
extractor: crate::pxar::extract::Extractor,
|
|
match_list: &'a [MatchEntry],
|
|
accessor: &'a Accessor,
|
|
) -> Result<Self, Error> {
|
|
let read_dir = catalog
|
|
.read_dir(&dir_stack.last().unwrap().catalog)?
|
|
.into_iter();
|
|
Ok(Self {
|
|
path: Vec::new(),
|
|
path_len: 0,
|
|
path_len_stack: Vec::new(),
|
|
|
|
dir_stack,
|
|
|
|
matches: match_list.is_empty(),
|
|
matches_stack: Vec::new(),
|
|
|
|
read_dir,
|
|
read_dir_stack: Vec::new(),
|
|
|
|
extractor,
|
|
|
|
catalog,
|
|
match_list,
|
|
accessor,
|
|
})
|
|
}
|
|
|
|
pub async fn extract(&mut self) -> Result<(), Error> {
|
|
loop {
|
|
let entry = match self.read_dir.next() {
|
|
Some(entry) => entry,
|
|
None => match self.handle_end_of_directory()? {
|
|
LoopState::Break => break, // done with root directory
|
|
LoopState::Continue => continue,
|
|
},
|
|
};
|
|
|
|
self.path.truncate(self.path_len);
|
|
if !entry.name.starts_with(b"/") {
|
|
self.path.reserve(entry.name.len() + 1);
|
|
self.path.push(b'/');
|
|
}
|
|
self.path.extend(&entry.name);
|
|
|
|
self.extractor.set_path(OsString::from_vec(self.path.clone()));
|
|
self.handle_entry(entry).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_end_of_directory(&mut self) -> Result<LoopState, Error> {
|
|
// go up a directory:
|
|
self.read_dir = match self.read_dir_stack.pop() {
|
|
Some(r) => r,
|
|
None => return Ok(LoopState::Break), // out of root directory
|
|
};
|
|
|
|
self.matches = self
|
|
.matches_stack
|
|
.pop()
|
|
.ok_or_else(|| format_err!("internal iterator error (matches_stack)"))?;
|
|
|
|
self.dir_stack
|
|
.pop()
|
|
.ok_or_else(|| format_err!("internal iterator error (dir_stack)"))?;
|
|
|
|
self.path_len = self
|
|
.path_len_stack
|
|
.pop()
|
|
.ok_or_else(|| format_err!("internal iterator error (path_len_stack)"))?;
|
|
|
|
self.extractor.leave_directory()?;
|
|
|
|
Ok(LoopState::Continue)
|
|
}
|
|
|
|
async fn handle_new_directory(
|
|
&mut self,
|
|
entry: catalog::DirEntry,
|
|
match_result: Option<MatchType>,
|
|
) -> Result<(), Error> {
|
|
// enter a new directory:
|
|
self.read_dir_stack.push(mem::replace(
|
|
&mut self.read_dir,
|
|
self.catalog.read_dir(&entry)?.into_iter(),
|
|
));
|
|
self.matches_stack.push(self.matches);
|
|
self.dir_stack.push(PathStackEntry::new(entry));
|
|
self.path_len_stack.push(self.path_len);
|
|
self.path_len = self.path.len();
|
|
|
|
Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
|
|
let dir_pxar = self.dir_stack.last().unwrap().pxar.as_ref().unwrap();
|
|
let dir_meta = dir_pxar.entry().metadata().clone();
|
|
let create = self.matches && match_result != Some(MatchType::Exclude);
|
|
self.extractor.enter_directory(dir_pxar.file_name().to_os_string(), dir_meta, create)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn handle_entry(&mut self, entry: catalog::DirEntry) -> Result<(), Error> {
|
|
let match_result = self.match_list.matches(&self.path, entry.get_file_mode());
|
|
let did_match = match match_result {
|
|
Some(MatchType::Include) => true,
|
|
Some(MatchType::Exclude) => false,
|
|
None => self.matches,
|
|
};
|
|
|
|
match (did_match, &entry.attr) {
|
|
(_, DirEntryAttribute::Directory { .. }) => {
|
|
self.handle_new_directory(entry, match_result).await?;
|
|
}
|
|
(true, DirEntryAttribute::File { .. }) => {
|
|
self.dir_stack.push(PathStackEntry::new(entry));
|
|
let file = Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
|
|
self.extract_file(file).await?;
|
|
self.dir_stack.pop();
|
|
}
|
|
(true, DirEntryAttribute::Symlink)
|
|
| (true, DirEntryAttribute::BlockDevice)
|
|
| (true, DirEntryAttribute::CharDevice)
|
|
| (true, DirEntryAttribute::Fifo)
|
|
| (true, DirEntryAttribute::Socket)
|
|
| (true, DirEntryAttribute::Hardlink) => {
|
|
let attr = entry.attr.clone();
|
|
self.dir_stack.push(PathStackEntry::new(entry));
|
|
let file = Shell::walk_pxar_archive(&self.accessor, &mut self.dir_stack).await?;
|
|
self.extract_special(file, attr).await?;
|
|
self.dir_stack.pop();
|
|
}
|
|
(false, _) => (), // skip
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn path(&self) -> &OsStr {
|
|
OsStr::from_bytes(&self.path)
|
|
}
|
|
|
|
async fn extract_file(&mut self, entry: FileEntry) -> Result<(), Error> {
|
|
match entry.kind() {
|
|
pxar::EntryKind::File { size, .. } => {
|
|
let file_name = CString::new(entry.file_name().as_bytes())?;
|
|
let mut contents = entry.contents().await?;
|
|
self.extractor.async_extract_file(
|
|
&file_name,
|
|
entry.metadata(),
|
|
*size,
|
|
&mut contents,
|
|
)
|
|
.await
|
|
}
|
|
_ => {
|
|
bail!(
|
|
"catalog file {:?} not a regular file in the archive",
|
|
self.path()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn extract_special(
|
|
&mut self,
|
|
entry: FileEntry,
|
|
catalog_attr: DirEntryAttribute,
|
|
) -> Result<(), Error> {
|
|
let file_name = CString::new(entry.file_name().as_bytes())?;
|
|
match (catalog_attr, entry.kind()) {
|
|
(DirEntryAttribute::Symlink, pxar::EntryKind::Symlink(symlink)) => {
|
|
block_in_place(|| self.extractor.extract_symlink(
|
|
&file_name,
|
|
entry.metadata(),
|
|
symlink.as_os_str(),
|
|
))
|
|
}
|
|
(DirEntryAttribute::Symlink, _) => {
|
|
bail!(
|
|
"catalog symlink {:?} not a symlink in the archive",
|
|
self.path()
|
|
);
|
|
}
|
|
|
|
(DirEntryAttribute::Hardlink, pxar::EntryKind::Hardlink(hardlink)) => {
|
|
block_in_place(|| self.extractor.extract_hardlink(&file_name, hardlink.as_os_str()))
|
|
}
|
|
(DirEntryAttribute::Hardlink, _) => {
|
|
bail!(
|
|
"catalog hardlink {:?} not a hardlink in the archive",
|
|
self.path()
|
|
);
|
|
}
|
|
|
|
(ref attr, pxar::EntryKind::Device(device)) => {
|
|
self.extract_device(attr.clone(), &file_name, device, entry.metadata())
|
|
}
|
|
|
|
(DirEntryAttribute::Fifo, pxar::EntryKind::Fifo) => {
|
|
block_in_place(|| self.extractor.extract_special(&file_name, entry.metadata(), 0))
|
|
}
|
|
(DirEntryAttribute::Fifo, _) => {
|
|
bail!("catalog fifo {:?} not a fifo in the archive", self.path());
|
|
}
|
|
|
|
(DirEntryAttribute::Socket, pxar::EntryKind::Socket) => {
|
|
block_in_place(|| self.extractor.extract_special(&file_name, entry.metadata(), 0))
|
|
}
|
|
(DirEntryAttribute::Socket, _) => {
|
|
bail!(
|
|
"catalog socket {:?} not a socket in the archive",
|
|
self.path()
|
|
);
|
|
}
|
|
|
|
attr => bail!("unhandled file type {:?} for {:?}", attr, self.path()),
|
|
}
|
|
}
|
|
|
|
fn extract_device(
|
|
&mut self,
|
|
attr: DirEntryAttribute,
|
|
file_name: &CStr,
|
|
device: &pxar::format::Device,
|
|
metadata: &Metadata,
|
|
) -> Result<(), Error> {
|
|
match attr {
|
|
DirEntryAttribute::BlockDevice => {
|
|
if !metadata.stat.is_blockdev() {
|
|
bail!(
|
|
"catalog block device {:?} is not a block device in the archive",
|
|
self.path(),
|
|
);
|
|
}
|
|
}
|
|
DirEntryAttribute::CharDevice => {
|
|
if !metadata.stat.is_chardev() {
|
|
bail!(
|
|
"catalog character device {:?} is not a character device in the archive",
|
|
self.path(),
|
|
);
|
|
}
|
|
}
|
|
_ => {
|
|
bail!(
|
|
"unexpected file type for {:?} in the catalog, \
|
|
which is a device special file in the archive",
|
|
self.path(),
|
|
);
|
|
}
|
|
}
|
|
block_in_place(|| self.extractor.extract_special(file_name, metadata, device.to_dev_t()))
|
|
}
|
|
}
|