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()))
|
|
}
|
|
}
|