diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs index 6f02e460..ab88d172 100644 --- a/src/api2/admin/datastore.rs +++ b/src/api2/admin/datastore.rs @@ -27,6 +27,7 @@ use pxar::EntryKind; use crate::api2::types::*; use crate::api2::node::rrd::create_value_from_rrd; +use crate::api2::helpers; use crate::backup::*; use crate::config::datastore; use crate::config::cached_user_info::CachedUserInfo; @@ -1294,7 +1295,7 @@ pub fn catalog( backup_time: i64, filepath: String, rpcenv: &mut dyn RpcEnvironment, -) -> Result { +) -> Result, Error> { let datastore = DataStore::lookup_datastore(&store)?; let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; @@ -1326,52 +1327,14 @@ pub fn catalog( let reader = BufferedDynamicReader::new(index, chunk_reader); let mut catalog_reader = CatalogReader::new(reader); - let mut current = catalog_reader.root()?; - let mut components = vec![]; + let path = if filepath != "root" { + base64::decode(filepath)? + } else { + vec![b'/'] + }; - if filepath != "root" { - components = base64::decode(filepath)?; - if !components.is_empty() && components[0] == b'/' { - components.remove(0); - } - for component in components.split(|c| *c == b'/') { - if let Some(entry) = catalog_reader.lookup(¤t, component)? { - current = entry; - } else { - bail!("path {:?} not found in catalog", &String::from_utf8_lossy(&components)); - } - } - } - - let mut res = Vec::new(); - - for direntry in catalog_reader.read_dir(¤t)? { - let mut components = components.clone(); - components.push(b'/'); - components.extend(&direntry.name); - let path = base64::encode(components); - let text = String::from_utf8_lossy(&direntry.name); - let mut entry = json!({ - "filepath": path, - "text": text, - "type": CatalogEntryType::from(&direntry.attr).to_string(), - "leaf": true, - }); - match direntry.attr { - DirEntryAttribute::Directory { start: _ } => { - entry["leaf"] = false.into(); - }, - DirEntryAttribute::File { size, mtime } => { - entry["size"] = size.into(); - entry["mtime"] = mtime.into(); - }, - _ => {}, - } - res.push(entry); - } - - Ok(res.into()) + helpers::list_dir_content(&mut catalog_reader, &path) } fn recurse_files<'a, T, W>( diff --git a/src/api2/helpers.rs b/src/api2/helpers.rs index 2a822654..41391b77 100644 --- a/src/api2/helpers.rs +++ b/src/api2/helpers.rs @@ -1,3 +1,4 @@ +use std::io::{Read, Seek}; use std::path::PathBuf; use anyhow::Error; @@ -6,6 +7,9 @@ use hyper::{Body, Response, StatusCode, header}; use proxmox::http_bail; +use crate::api2::types::ArchiveEntry; +use crate::backup::{CatalogReader, DirEntryAttribute}; + pub async fn create_download_response(path: PathBuf) -> Result, Error> { let file = match tokio::fs::File::open(path.clone()).await { Ok(file) => file, @@ -27,3 +31,30 @@ pub async fn create_download_response(path: PathBuf) -> Result, E .body(body) .unwrap()) } + +/// Returns the list of content of the given path +pub fn list_dir_content( + reader: &mut CatalogReader, + path: &[u8], +) -> Result, Error> { + let dir = reader.lookup_recursive(path)?; + let mut res = vec![]; + let mut path = path.to_vec(); + if !path.is_empty() && path[0] == b'/' { + path.remove(0); + } + + for direntry in reader.read_dir(&dir)? { + let mut components = path.clone(); + components.push(b'/'); + components.extend(&direntry.name); + let mut entry = ArchiveEntry::new(&components, &direntry.attr); + if let DirEntryAttribute::File { size, mtime } = direntry.attr { + entry.size = size.into(); + entry.mtime = mtime.into(); + } + res.push(entry); + } + + Ok(res) +} diff --git a/src/api2/types/mod.rs b/src/api2/types/mod.rs index d9394586..4c663335 100644 --- a/src/api2/types/mod.rs +++ b/src/api2/types/mod.rs @@ -12,6 +12,8 @@ use crate::{ CryptMode, Fingerprint, BACKUP_ID_REGEX, + DirEntryAttribute, + CatalogEntryType, }, server::UPID, config::acl::Role, @@ -1303,6 +1305,47 @@ pub struct DatastoreNotify { pub sync: Option, } +/// An entry in a hierarchy of files for restore and listing. +#[api()] +#[derive(Serialize, Deserialize)] +pub struct ArchiveEntry { + /// Base64-encoded full path to the file, including the filename + pub filepath: String, + /// Displayable filename text for UIs + pub text: String, + /// File or directory type of this entry + #[serde(rename = "type")] + pub entry_type: String, + /// Is this entry a leaf node, or does it have children (i.e. a directory)? + pub leaf: bool, + /// The file size, if entry_type is 'f' (file) + #[serde(skip_serializing_if="Option::is_none")] + pub size: Option, + /// The file "last modified" time stamp, if entry_type is 'f' (file) + #[serde(skip_serializing_if="Option::is_none")] + pub mtime: Option, +} + +impl ArchiveEntry { + pub fn new(filepath: &[u8], entry_type: &DirEntryAttribute) -> Self { + Self { + filepath: base64::encode(filepath), + text: String::from_utf8_lossy(filepath.split(|x| *x == b'/').last().unwrap()) + .to_string(), + entry_type: CatalogEntryType::from(entry_type).to_string(), + leaf: matches!(entry_type, DirEntryAttribute::Directory { .. }), + size: match entry_type { + DirEntryAttribute::File { size, .. } => Some(*size), + _ => None + }, + mtime: match entry_type { + DirEntryAttribute::File { mtime, .. } => Some(*mtime), + _ => None + }, + } + } +} + pub const DATASTORE_NOTIFY_STRING_SCHEMA: Schema = StringSchema::new( "Datastore notification setting") .format(&ApiStringFormat::PropertyString(&DatastoreNotify::API_SCHEMA)) diff --git a/src/backup/catalog.rs b/src/backup/catalog.rs index 224e6bf7..a307f9d8 100644 --- a/src/backup/catalog.rs +++ b/src/backup/catalog.rs @@ -468,6 +468,32 @@ impl CatalogReader { Ok(entry_list) } + /// Lookup a DirEntry from an absolute path + pub fn lookup_recursive( + &mut self, + path: &[u8], + ) -> Result { + let mut current = self.root()?; + if path == b"/" { + return Ok(current); + } + + let components = if !path.is_empty() && path[0] == b'/' { + &path[1..] + } else { + path + }.split(|c| *c == b'/'); + + for comp in components { + if let Some(entry) = self.lookup(¤t, comp)? { + current = entry; + } else { + bail!("path {:?} not found in catalog", String::from_utf8_lossy(&path)); + } + } + Ok(current) + } + /// Lockup a DirEntry inside a parent directory pub fn lookup( &mut self,