5
0
mirror of git://git.proxmox.com/git/proxmox-backup.git synced 2025-01-21 18:03:59 +03:00

client: catalog shell: fallback to accessor for navigation

Make the catalog optional and use the pxar accessor for navigation if
the catalog is not provided.
This allows to use the metadata archive for navigraion, as for split
pxar archives no dedicated catalog is encoded.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
This commit is contained in:
Christian Ebner 2024-08-12 12:31:39 +02:00 committed by Fabian Grünbichler
parent 78e4098eae
commit 5ddd59e167
2 changed files with 254 additions and 59 deletions

View File

@ -21,7 +21,7 @@ use pxar::accessor::ReadAt;
use pxar::{EntryKind, Metadata}; use pxar::{EntryKind, Metadata};
use pbs_datastore::catalog::{self, DirEntryAttribute}; use pbs_datastore::catalog::{self, DirEntryAttribute};
use proxmox_async::runtime::block_in_place; use proxmox_async::runtime::{block_in_place, block_on};
use crate::pxar::Flags; use crate::pxar::Flags;
@ -312,8 +312,9 @@ pub struct Shell {
/// Interactive prompt. /// Interactive prompt.
prompt: String, prompt: String,
/// Catalog reader instance to navigate /// Optional catalog reader instance to navigate, if not present the Accessor is used for
catalog: CatalogReader, /// navigation
catalog: Option<CatalogReader>,
/// List of selected paths for restore /// List of selected paths for restore
selected: HashMap<OsString, MatchEntry>, selected: HashMap<OsString, MatchEntry>,
@ -347,7 +348,7 @@ impl PathStackEntry {
impl Shell { impl Shell {
/// Create a new shell for the given catalog and pxar archive. /// Create a new shell for the given catalog and pxar archive.
pub async fn new( pub async fn new(
mut catalog: CatalogReader, mut catalog: Option<CatalogReader>,
archive_name: &str, archive_name: &str,
archive: Accessor, archive: Accessor,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
@ -355,11 +356,31 @@ impl Shell {
let mut rl = rustyline::Editor::<CliHelper>::new(); let mut rl = rustyline::Editor::<CliHelper>::new();
rl.set_helper(Some(cli_helper)); rl.set_helper(Some(cli_helper));
let catalog_root = catalog.root()?; let mut position = Vec::new();
let archive_root = catalog if let Some(catalog) = catalog.as_mut() {
.lookup(&catalog_root, archive_name.as_bytes())? let catalog_root = catalog.root()?;
.ok_or_else(|| format_err!("archive not found in catalog"))?; let archive_root = catalog
let position = vec![PathStackEntry::new(archive_root)]; .lookup(&catalog_root, archive_name.as_bytes())?
.ok_or_else(|| format_err!("archive not found in catalog"))?;
position.push(PathStackEntry::new(archive_root));
} else {
let root = archive.open_root().await?;
let root_entry = root.lookup_self().await?;
if let EntryKind::Directory = root_entry.kind() {
let entry_attr = DirEntryAttribute::Directory {
start: root_entry.entry_range_info().entry_range.start,
};
position.push(PathStackEntry {
catalog: catalog::DirEntry {
name: archive_name.into(),
attr: entry_attr,
},
pxar: Some(root_entry),
});
} else {
bail!("unexpected root entry type");
}
}
let mut this = Self { let mut this = Self {
rl, rl,
@ -450,7 +471,7 @@ impl Shell {
async fn resolve_symlink( async fn resolve_symlink(
stack: &mut Vec<PathStackEntry>, stack: &mut Vec<PathStackEntry>,
catalog: &mut CatalogReader, catalog: &mut Option<CatalogReader>,
accessor: &Accessor, accessor: &Accessor,
follow_symlinks: &mut Option<usize>, follow_symlinks: &mut Option<usize>,
) -> Result<(), Error> { ) -> Result<(), Error> {
@ -468,7 +489,7 @@ impl Shell {
}; };
let new_stack = let new_stack =
Self::lookup(stack, &mut *catalog, accessor, Some(path), follow_symlinks).await?; Self::lookup(stack, catalog, accessor, Some(path), follow_symlinks).await?;
*stack = new_stack; *stack = new_stack;
@ -484,7 +505,7 @@ impl Shell {
/// out. /// out.
async fn step( async fn step(
stack: &mut Vec<PathStackEntry>, stack: &mut Vec<PathStackEntry>,
catalog: &mut CatalogReader, catalog: &mut Option<CatalogReader>,
accessor: &Accessor, accessor: &Accessor,
component: std::path::Component<'_>, component: std::path::Component<'_>,
follow_symlinks: &mut Option<usize>, follow_symlinks: &mut Option<usize>,
@ -503,9 +524,27 @@ impl Shell {
if stack.last().unwrap().catalog.is_symlink() { if stack.last().unwrap().catalog.is_symlink() {
Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?; Self::resolve_symlink(stack, catalog, accessor, follow_symlinks).await?;
} }
match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? { if let Some(catalog) = catalog {
Some(dir) => stack.push(PathStackEntry::new(dir)), match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? {
None => bail!("no such file or directory: {:?}", entry), Some(dir) => stack.push(PathStackEntry::new(dir)),
None => bail!("no such file or directory: {entry:?}"),
}
} else {
let pxar_entry = parent_pxar_entry(&stack)?;
let parent_dir = pxar_entry.enter_directory().await?;
match parent_dir.lookup(entry).await? {
Some(entry) => {
let entry_attr = DirEntryAttribute::try_from(&entry)?;
stack.push(PathStackEntry {
catalog: catalog::DirEntry {
name: entry.entry().file_name().as_bytes().into(),
attr: entry_attr,
},
pxar: Some(entry),
})
}
None => bail!("no such file or directory: {entry:?}"),
}
} }
} }
} }
@ -515,7 +554,7 @@ impl Shell {
fn step_nofollow( fn step_nofollow(
stack: &mut Vec<PathStackEntry>, stack: &mut Vec<PathStackEntry>,
catalog: &mut CatalogReader, catalog: &mut Option<CatalogReader>,
component: std::path::Component<'_>, component: std::path::Component<'_>,
) -> Result<(), Error> { ) -> Result<(), Error> {
use std::path::Component; use std::path::Component;
@ -531,11 +570,27 @@ impl Shell {
Component::Normal(entry) => { Component::Normal(entry) => {
if stack.last().unwrap().catalog.is_symlink() { if stack.last().unwrap().catalog.is_symlink() {
bail!("target is a symlink"); bail!("target is a symlink");
} else { } else if let Some(catalog) = catalog.as_mut() {
match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? { match catalog.lookup(&stack.last().unwrap().catalog, entry.as_bytes())? {
Some(dir) => stack.push(PathStackEntry::new(dir)), Some(dir) => stack.push(PathStackEntry::new(dir)),
None => bail!("no such file or directory: {:?}", entry), None => bail!("no such file or directory: {:?}", entry),
} }
} else {
let pxar_entry = parent_pxar_entry(&stack)?;
let parent_dir = block_on(pxar_entry.enter_directory())?;
match block_on(parent_dir.lookup(entry))? {
Some(entry) => {
let entry_attr = DirEntryAttribute::try_from(&entry)?;
stack.push(PathStackEntry {
catalog: catalog::DirEntry {
name: entry.entry().file_name().as_bytes().into(),
attr: entry_attr,
},
pxar: Some(entry),
})
}
None => bail!("no such file or directory: {entry:?}"),
}
} }
} }
} }
@ -545,7 +600,7 @@ impl Shell {
/// The pxar accessor is required to resolve symbolic links /// The pxar accessor is required to resolve symbolic links
async fn walk_catalog( async fn walk_catalog(
stack: &mut Vec<PathStackEntry>, stack: &mut Vec<PathStackEntry>,
catalog: &mut CatalogReader, catalog: &mut Option<CatalogReader>,
accessor: &Accessor, accessor: &Accessor,
path: &Path, path: &Path,
follow_symlinks: &mut Option<usize>, follow_symlinks: &mut Option<usize>,
@ -559,7 +614,7 @@ impl Shell {
/// Non-async version cannot follow symlinks. /// Non-async version cannot follow symlinks.
fn walk_catalog_nofollow( fn walk_catalog_nofollow(
stack: &mut Vec<PathStackEntry>, stack: &mut Vec<PathStackEntry>,
catalog: &mut CatalogReader, catalog: &mut Option<CatalogReader>,
path: &Path, path: &Path,
) -> Result<(), Error> { ) -> Result<(), Error> {
for c in path.components() { for c in path.components() {
@ -612,12 +667,34 @@ impl Shell {
tmp_stack = self.position.clone(); tmp_stack = self.position.clone();
} }
Self::walk_catalog_nofollow(&mut tmp_stack, &mut self.catalog, &path)?; Self::walk_catalog_nofollow(&mut tmp_stack, &mut self.catalog, &path)?;
(&tmp_stack.last().unwrap().catalog, base, part) (&tmp_stack.last().unwrap(), base, part)
} }
None => (&self.position.last().unwrap().catalog, "", input), None => (&self.position.last().unwrap(), "", input),
}; };
let entries = self.catalog.read_dir(parent)?; let entries = if let Some(catalog) = self.catalog.as_mut() {
catalog.read_dir(&parent.catalog)?
} else {
let dir = if let Some(entry) = parent.pxar.as_ref() {
block_on(entry.enter_directory())?
} else {
bail!("missing pxar entry for parent");
};
let mut out = Vec::new();
let entries = block_on(crate::pxar::tools::pxar_metadata_read_dir(dir))?;
for entry in entries {
let mut name = base.to_string();
let file_name = entry.file_name().as_bytes();
if file_name.starts_with(part.as_bytes()) {
name.push_str(std::str::from_utf8(file_name)?);
if entry.is_dir() {
name.push('/');
}
out.push(name);
}
}
return Ok(out);
};
let mut out = Vec::new(); let mut out = Vec::new();
for entry in entries { for entry in entries {
@ -637,7 +714,7 @@ impl Shell {
// Break async recursion here: lookup -> walk_catalog -> step -> lookup // Break async recursion here: lookup -> walk_catalog -> step -> lookup
fn lookup<'future, 's, 'c, 'a, 'p, 'y>( fn lookup<'future, 's, 'c, 'a, 'p, 'y>(
stack: &'s [PathStackEntry], stack: &'s [PathStackEntry],
catalog: &'c mut CatalogReader, catalog: &'c mut Option<CatalogReader>,
accessor: &'a Accessor, accessor: &'a Accessor,
path: Option<&'p Path>, path: Option<&'p Path>,
follow_symlinks: &'y mut Option<usize>, follow_symlinks: &'y mut Option<usize>,
@ -678,7 +755,23 @@ impl Shell {
let last = stack.last().unwrap(); let last = stack.last().unwrap();
if last.catalog.is_directory() { if last.catalog.is_directory() {
let items = self.catalog.read_dir(&stack.last().unwrap().catalog)?; let items = if let Some(catalog) = self.catalog.as_mut() {
catalog.read_dir(&stack.last().unwrap().catalog)?
} else {
let dir = if let Some(entry) = last.pxar.as_ref() {
entry.enter_directory().await?
} else {
bail!("missing pxar entry for parent");
};
let mut out = std::io::stdout();
let items = crate::pxar::tools::pxar_metadata_read_dir(dir).await?;
for item in items {
out.write_all(&item.file_name().as_bytes())?;
out.write_all(b"\n")?;
}
return Ok(());
};
let mut out = std::io::stdout(); let mut out = std::io::stdout();
// FIXME: columnize // FIXME: columnize
for item in items { for item in items {
@ -820,17 +913,36 @@ impl Shell {
async fn list_matching_files(&mut self) -> Result<(), Error> { async fn list_matching_files(&mut self) -> Result<(), Error> {
let matches = self.build_match_list(); let matches = self.build_match_list();
self.catalog.find( if let Some(catalog) = self.catalog.as_mut() {
&self.position[0].catalog, catalog.find(
&mut Vec::new(), &self.position[0].catalog,
&matches, &mut Vec::new(),
&mut |path: &[u8]| -> Result<(), Error> { &matches,
let mut out = std::io::stdout(); &mut |path: &[u8]| -> Result<(), Error> {
out.write_all(path)?; let mut out = std::io::stdout();
out.write_all(b"\n")?; out.write_all(path)?;
Ok(()) out.write_all(b"\n")?;
}, Ok(())
)?; },
)?;
} else {
let parent_dir = if let Some(pxar_entry) = self.position[0].pxar.as_ref() {
pxar_entry.enter_directory().await?
} else {
bail!("missing pxar entry for archive root");
};
crate::pxar::tools::pxar_metadata_catalog_find(
parent_dir,
&matches,
&|path: &[u8]| -> Result<(), Error> {
let mut out = std::io::stdout();
out.write_all(path)?;
out.write_all(b"\n")?;
Ok(())
},
)
.await?;
}
Ok(()) Ok(())
} }
@ -841,18 +953,37 @@ impl Shell {
MatchEntry::parse_pattern(pattern, PatternFlag::PATH_NAME, MatchType::Include)?; MatchEntry::parse_pattern(pattern, PatternFlag::PATH_NAME, MatchType::Include)?;
let mut found_some = false; let mut found_some = false;
self.catalog.find( if let Some(catalog) = self.catalog.as_mut() {
&self.position[0].catalog, catalog.find(
&mut Vec::new(), &self.position[0].catalog,
&[&pattern_entry], &mut Vec::new(),
&mut |path: &[u8]| -> Result<(), Error> { &[&pattern_entry],
found_some = true; &mut |path: &[u8]| -> Result<(), Error> {
let mut out = std::io::stdout(); found_some = true;
out.write_all(path)?; let mut out = std::io::stdout();
out.write_all(b"\n")?; out.write_all(path)?;
Ok(()) out.write_all(b"\n")?;
}, Ok(())
)?; },
)?;
} else {
let parent_dir = if let Some(pxar_entry) = self.position[0].pxar.as_ref() {
pxar_entry.enter_directory().await?
} else {
bail!("missing pxar entry for archive root");
};
crate::pxar::tools::pxar_metadata_catalog_find(
parent_dir,
&[&pattern_entry],
&|path: &[u8]| -> Result<(), Error> {
let mut out = std::io::stdout();
out.write_all(path)?;
out.write_all(b"\n")?;
Ok(())
},
)
.await?;
}
if found_some && select { if found_some && select {
self.selected.insert(pattern_os, pattern_entry); self.selected.insert(pattern_os, pattern_entry);
@ -945,6 +1076,18 @@ impl Shell {
} }
} }
fn parent_pxar_entry(dir_stack: &[PathStackEntry]) -> Result<&FileEntry, Error> {
if let Some(parent) = dir_stack.last().as_ref() {
if let Some(entry) = parent.pxar.as_ref() {
Ok(entry)
} else {
bail!("missing pxar entry for parent");
}
} else {
bail!("missing parent entry on stack");
}
}
struct ExtractorState<'a> { struct ExtractorState<'a> {
path: Vec<u8>, path: Vec<u8>,
path_len: usize, path_len: usize,
@ -960,22 +1103,38 @@ struct ExtractorState<'a> {
extractor: crate::pxar::extract::Extractor, extractor: crate::pxar::extract::Extractor,
catalog: &'a mut CatalogReader, catalog: &'a mut Option<CatalogReader>,
match_list: &'a [MatchEntry], match_list: &'a [MatchEntry],
accessor: &'a Accessor, accessor: &'a Accessor,
} }
impl<'a> ExtractorState<'a> { impl<'a> ExtractorState<'a> {
pub fn new( pub fn new(
catalog: &'a mut CatalogReader, catalog: &'a mut Option<CatalogReader>,
dir_stack: Vec<PathStackEntry>, dir_stack: Vec<PathStackEntry>,
extractor: crate::pxar::extract::Extractor, extractor: crate::pxar::extract::Extractor,
match_list: &'a [MatchEntry], match_list: &'a [MatchEntry],
accessor: &'a Accessor, accessor: &'a Accessor,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let read_dir = catalog let read_dir = if let Some(catalog) = catalog.as_mut() {
.read_dir(&dir_stack.last().unwrap().catalog)? catalog
.into_iter(); .read_dir(&dir_stack.last().unwrap().catalog)?
.into_iter()
} else {
let pxar_entry = parent_pxar_entry(&dir_stack)?;
let dir = block_on(pxar_entry.enter_directory())?;
let entries = block_on(crate::pxar::tools::pxar_metadata_read_dir(dir))?;
let mut catalog_entries = Vec::with_capacity(entries.len());
for entry in entries {
let entry_attr = DirEntryAttribute::try_from(&entry).unwrap();
catalog_entries.push(catalog::DirEntry {
name: entry.entry().file_name().as_bytes().into(),
attr: entry_attr,
});
}
catalog_entries.into_iter()
};
Ok(Self { Ok(Self {
path: Vec::new(), path: Vec::new(),
path_len: 0, path_len: 0,
@ -1053,11 +1212,29 @@ impl<'a> ExtractorState<'a> {
entry: catalog::DirEntry, entry: catalog::DirEntry,
match_result: Option<MatchType>, match_result: Option<MatchType>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let entry_iter = if let Some(catalog) = self.catalog.as_mut() {
catalog.read_dir(&entry)?.into_iter()
} else {
self.dir_stack.push(PathStackEntry::new(entry.clone()));
let dir = Shell::walk_pxar_archive(self.accessor, &mut self.dir_stack).await?;
self.dir_stack.pop();
let dir = dir.enter_directory().await?;
let entries = block_on(crate::pxar::tools::pxar_metadata_read_dir(dir))?;
entries
.into_iter()
.map(|entry| {
let entry_attr = DirEntryAttribute::try_from(&entry).unwrap();
catalog::DirEntry {
name: entry.entry().file_name().as_bytes().into(),
attr: entry_attr,
}
})
.collect::<Vec<catalog::DirEntry>>()
.into_iter()
};
// enter a new directory: // enter a new directory:
self.read_dir_stack.push(mem::replace( self.read_dir_stack
&mut self.read_dir, .push(mem::replace(&mut self.read_dir, entry_iter));
self.catalog.read_dir(&entry)?.into_iter(),
));
self.matches_stack.push(self.matches); self.matches_stack.push(self.matches);
self.dir_stack.push(PathStackEntry::new(entry)); self.dir_stack.push(PathStackEntry::new(entry));
self.path_len_stack.push(self.path_len); self.path_len_stack.push(self.path_len);

View File

@ -230,11 +230,29 @@ async fn catalog_shell(param: Value) -> Result<(), Error> {
) )
.await?; .await?;
let mut tmpfile = pbs_client::tools::create_tmp_file()?;
let (manifest, _) = client.download_manifest().await?; let (manifest, _) = client.download_manifest().await?;
manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?; manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?;
if let Err(_err) = manifest.lookup_file_info(CATALOG_NAME) {
// No catalog, fallback to pxar archive accessor if present
let accessor = helper::get_pxar_fuse_accessor(
&server_archive_name,
client.clone(),
&manifest,
crypt_config.clone(),
)
.await?;
let state = Shell::new(None, &server_archive_name, accessor).await?;
log::info!("Starting interactive shell");
state.shell().await?;
record_repository(&repo);
return Ok(());
}
let mut tmpfile = pbs_client::tools::create_tmp_file()?;
let decoder = helper::get_pxar_fuse_accessor( let decoder = helper::get_pxar_fuse_accessor(
&server_archive_name, &server_archive_name,
client.clone(), client.clone(),
@ -268,7 +286,7 @@ async fn catalog_shell(param: Value) -> Result<(), Error> {
catalogfile.seek(SeekFrom::Start(0))?; catalogfile.seek(SeekFrom::Start(0))?;
let catalog_reader = CatalogReader::new(catalogfile); let catalog_reader = CatalogReader::new(catalogfile);
let state = Shell::new(catalog_reader, &server_archive_name, decoder).await?; let state = Shell::new(Some(catalog_reader), &server_archive_name, decoder).await?;
log::info!("Starting interactive shell"); log::info!("Starting interactive shell");
state.shell().await?; state.shell().await?;