[pbs-devel] [PATCH v9 proxmox-backup 52/58] api: datastore: add endpoint to lookup entries via pxar archive

Christian Ebner c.ebner at proxmox.com
Wed Jun 5 12:54:10 CEST 2024


Add an api endpoint `pxar-lookup` to access the contents of a pxar
archive via a server side pxar accessor, providing the same response
as currently the `catalog` lookup. The intention is to fully replace
the catalog for split pxar archives, accessing all the entries via
the metadata archive instead of the catalog.

Signed-off-by: Christian Ebner <c.ebner at proxmox.com>
---
changes since version 8:
- not present in previous version

 src/api2/admin/datastore.rs | 149 +++++++++++++++++++++++++++++++++++-
 1 file changed, 147 insertions(+), 2 deletions(-)

diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
index 34a9105dd..abc4a4fba 100644
--- a/src/api2/admin/datastore.rs
+++ b/src/api2/admin/datastore.rs
@@ -30,7 +30,8 @@ use proxmox_sys::{task_log, task_warn};
 use proxmox_time::CalendarEvent;
 
 use pxar::accessor::aio::Accessor;
-use pxar::EntryKind;
+use pxar::format::SignedDuration;
+use pxar::{mode, EntryKind};
 
 use pbs_api_types::{
     print_ns_and_snapshot, print_store_and_ns, Authid, BackupContent, BackupNamespace, BackupType,
@@ -47,7 +48,7 @@ use pbs_client::pxar::{create_tar, create_zip};
 use pbs_config::CachedUserInfo;
 use pbs_datastore::backup_info::BackupInfo;
 use pbs_datastore::cached_chunk_reader::CachedChunkReader;
-use pbs_datastore::catalog::{ArchiveEntry, CatalogReader};
+use pbs_datastore::catalog::{ArchiveEntry, CatalogReader, DirEntryAttribute};
 use pbs_datastore::data_blob::DataBlob;
 use pbs_datastore::data_blob_reader::DataBlobReader;
 use pbs_datastore::dynamic_index::{BufferedDynamicReader, DynamicIndexReader, LocalDynamicReadAt};
@@ -1720,6 +1721,149 @@ pub async fn catalog(
     .await?
 }
 
+#[api(
+    input: {
+        properties: {
+            store: { schema: DATASTORE_SCHEMA },
+            "archive-name": {
+                type: String,
+                description: "Name of the archive to lookup given filepath (base64 encoded)",
+            },
+            ns: {
+                type: BackupNamespace,
+                optional: true,
+            },
+            backup_dir: {
+                type: pbs_api_types::BackupDir,
+                flatten: true,
+            },
+            "filepath": {
+                description: "Base64 encoded path.",
+                type: String,
+            }
+        },
+    },
+    access: {
+        description: "Requires on /datastore/{store}[/{namespace}] either DATASTORE_READ for any or \
+            DATASTORE_BACKUP and being the owner of the group",
+        permission: &Permission::Anybody,
+    },
+)]
+/// Get the entries of the given path of the pxar (metadata) archive
+pub async fn pxar_lookup(
+    store: String,
+    archive_name: String,
+    ns: Option<BackupNamespace>,
+    backup_dir: pbs_api_types::BackupDir,
+    filepath: String,
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<ArchiveEntry>, Error> {
+    let archive_name = base64::decode(archive_name)
+        .map_err(|err| format_err!("base64 decode of archive-name failed - {err}"))?;
+
+    let archive_name = std::str::from_utf8(&archive_name)?;
+
+    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
+
+    let ns = ns.unwrap_or_default();
+
+    let datastore = check_privs_and_load_store(
+        &store,
+        &ns,
+        &auth_id,
+        PRIV_DATASTORE_READ,
+        PRIV_DATASTORE_BACKUP,
+        Some(Operation::Read),
+        &backup_dir.group,
+    )?;
+
+    let backup_dir = datastore.backup_dir(ns, backup_dir)?;
+
+    let file_path = if filepath != "root" && filepath != "/" {
+        base64::decode(filepath)
+            .map_err(|err| format_err!("base64 decode of filepath failed - {err}"))?
+    } else {
+        vec![b'/']
+    };
+
+    let (manifest, files) = read_backup_index(&backup_dir)?;
+    for file in files {
+        if file.filename == archive_name && file.crypt_mode == Some(CryptMode::Encrypt) {
+            bail!("cannot decode '{archive_name}' - is encrypted");
+        }
+    }
+
+    let (archive_name, payload_archive_name) =
+        pbs_client::tools::get_pxar_archive_names(archive_name, &manifest)?;
+    let (reader, archive_size) =
+        get_local_pxar_reader(datastore.clone(), &manifest, &backup_dir, &archive_name)?;
+
+    let reader = if let Some(payload_archive_name) = payload_archive_name {
+        let payload_input =
+            get_local_pxar_reader(datastore, &manifest, &backup_dir, &payload_archive_name)?;
+        pxar::PxarVariant::Split(reader, payload_input)
+    } else {
+        pxar::PxarVariant::Unified(reader)
+    };
+    let accessor = Accessor::new(reader, archive_size).await?;
+
+    let root = accessor.open_root().await?;
+    let path = OsStr::from_bytes(&file_path).to_os_string();
+    let dir_entry = root
+        .lookup(&path)
+        .await
+        .map_err(|err| format_err!("lookup failed - {err}"))?
+        .ok_or_else(|| format_err!("lookup failed - error opening '{path:?}'"))?;
+
+    let mut entries = Vec::new();
+    if let EntryKind::Directory = dir_entry.kind() {
+        let dir_entry = dir_entry
+            .enter_directory()
+            .map_err(|err| format_err!("failed to enter directory - {err}"))
+            .await?;
+
+        let mut entries_iter = dir_entry.read_dir();
+        while let Some(entry) = entries_iter.next().await {
+            let entry = entry?.decode_entry().await?;
+
+            let entry_attr = match entry.kind() {
+                EntryKind::Version(_) | EntryKind::Prelude(_) | EntryKind::GoodbyeTable => continue,
+                EntryKind::Directory => DirEntryAttribute::Directory {
+                    start: entry.entry_range_info().entry_range.start,
+                },
+                EntryKind::File { size, .. } => {
+                    let mtime = match entry.metadata().mtime_as_duration() {
+                        SignedDuration::Positive(val) => i64::try_from(val.as_secs())?,
+                        SignedDuration::Negative(val) => -1 * i64::try_from(val.as_secs())?,
+                    };
+                    DirEntryAttribute::File { size: *size, mtime }
+                }
+                EntryKind::Device(_) => match entry.metadata().file_type() {
+                    mode::IFBLK => DirEntryAttribute::BlockDevice,
+                    mode::IFCHR => DirEntryAttribute::CharDevice,
+                    _ => bail!("encountered unknown device type"),
+                },
+                EntryKind::Symlink(_) => DirEntryAttribute::Symlink,
+                EntryKind::Hardlink(_) => DirEntryAttribute::Hardlink,
+                EntryKind::Fifo => DirEntryAttribute::Fifo,
+                EntryKind::Socket => DirEntryAttribute::Socket,
+            };
+
+            entries.push(ArchiveEntry::new(
+                entry.path().as_os_str().as_bytes(),
+                Some(&entry_attr),
+            ));
+        }
+    } else {
+        bail!(format!(
+            "expected directory entry, got entry kind '{:?}'",
+            dir_entry.kind()
+        ));
+    }
+
+    Ok(entries)
+}
+
 #[sortable]
 pub const API_METHOD_PXAR_FILE_DOWNLOAD: ApiMethod = ApiMethod::new(
     &ApiHandler::AsyncHttp(&pxar_file_download),
@@ -2414,6 +2558,7 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
         "pxar-file-download",
         &Router::new().download(&API_METHOD_PXAR_FILE_DOWNLOAD),
     ),
+    ("pxar-lookup", &Router::new().get(&API_METHOD_PXAR_LOOKUP)),
     ("rrd", &Router::new().get(&API_METHOD_GET_RRD_STATS)),
     (
         "snapshots",
-- 
2.39.2





More information about the pbs-devel mailing list