[pdm-devel] [RFC PATCH datacenter-manager 1/2] server: add new streaming 'content' api call for pbs

Dominik Csapak d.csapak at proxmox.com
Wed Oct 8 15:54:04 CEST 2025


that makes use of 'application/json-seq' mime type to stream data from
pbs to pdm, removing the need to buffer the data on pdm in the api call.

The pbs-client code is more or less a copy of the current snapshot call
list, but if needed this method can be refactored.

This requires the new 'content' api call for pbs [0]

0: https://lore.proxmox.com/pbs-devel/20251008134344.3512958-1-d.csapak@proxmox.com/

Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
 server/src/api/pbs/mod.rs | 40 +++++++++++++++++++++++++++++-
 server/src/pbs_client.rs  | 51 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 90 insertions(+), 1 deletion(-)

diff --git a/server/src/api/pbs/mod.rs b/server/src/api/pbs/mod.rs
index dc31f620..6e53ce6f 100644
--- a/server/src/api/pbs/mod.rs
+++ b/server/src/api/pbs/mod.rs
@@ -1,7 +1,7 @@
 use anyhow::{format_err, Error};
 use futures::StreamExt;
 
-use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
+use proxmox_router::{list_subdirs_api_method, Permission, Record, Router, SubdirMap};
 use proxmox_schema::api;
 use proxmox_schema::property_string::PropertyString;
 use proxmox_sortable_macro::sortable;
@@ -63,6 +63,7 @@ const DATASTORE_ITEM_SUBDIRS: SubdirMap = &sorted!([
         "snapshots",
         &Router::new().get(&API_METHOD_LIST_SNAPSHOTS_2)
     ),
+    ("content", &Router::new().get(&API_METHOD_LIST_CONTENT)),
 ]);
 
 #[api(
@@ -175,6 +176,43 @@ async fn list_snapshots_2(
     .into())
 }
 
+#[api(
+    stream: true,
+    input: {
+        properties: {
+            remote: { schema: REMOTE_ID_SCHEMA },
+            datastore: { schema: pbs_api_types::DATASTORE_SCHEMA },
+            ns: {
+                schema: pbs_api_types::BACKUP_NAMESPACE_SCHEMA,
+                optional: true,
+            },
+            "max-depth": {
+                schema: pbs_api_types::NS_MAX_DEPTH_SCHEMA,
+                optional: true,
+            }
+        },
+    },
+    returns: pbs_api_types::ADMIN_DATASTORE_LIST_CONTENT_RETURN_TYPE,
+    access: {
+        permission: &Permission::Privilege(&["resource", "{remote}", "datastore", "{datastore}"], PRIV_RESOURCE_AUDIT, false),
+    },
+)]
+/// List the PBS remote's datastores.
+async fn list_content(
+    remote: String,
+    datastore: String,
+    ns: Option<String>,
+    max_depth: Option<usize>,
+) -> Result<proxmox_router::Stream, Error> {
+    let (remotes, _) = pdm_config::remotes::config()?;
+    let remote = get_remote(&remotes, &remote)?;
+    let snapshots = connection::make_pbs_client(remote)?
+        .list_content(&datastore, ns.as_deref(), max_depth)
+        .await?
+        .map(Record::from_result);
+    Ok(snapshots.into())
+}
+
 #[api(
     input: {
         properties: {
diff --git a/server/src/pbs_client.rs b/server/src/pbs_client.rs
index d8278c8a..76041090 100644
--- a/server/src/pbs_client.rs
+++ b/server/src/pbs_client.rs
@@ -190,6 +190,57 @@ impl PbsClient {
         }
     }
 
+    /// List a datastore's content.
+    pub async fn list_content(
+        &self,
+        datastore: &str,
+        namespace: Option<&str>,
+        max_depth: Option<usize>,
+    ) -> Result<JsonRecords<pbs_api_types::DatastoreContent>, anyhow::Error> {
+        let path = ApiPathBuilder::new(format!("/api2/json/admin/datastore/{datastore}/content"))
+            .maybe_arg("ns", &namespace)
+            .maybe_arg("max-depth", &max_depth)
+            .build();
+        let response = self
+            .0
+            .streaming_request(http::Method::GET, &path, None::<()>)
+            .await?;
+
+        let body = response
+            .body
+            .ok_or_else(|| Error::Other("missing response body"))?;
+
+        if response.status == 200 {
+            if response
+                .content_type
+                .is_some_and(|c| c.starts_with("application/json-seq"))
+            {
+                Ok(JsonRecords::from_body(body))
+            } else {
+                let response: JsonData<_> = serde_json::from_slice(
+                    &body
+                        .collect()
+                        .await
+                        .map_err(|err| {
+                            Error::Anyhow(Box::new(err).context("failed to retrieve response body"))
+                        })?
+                        .to_bytes(),
+                )?;
+                Ok(JsonRecords::from_vec(response.data))
+            }
+        } else {
+            let data = body
+                .collect()
+                .await
+                .map_err(|err| {
+                    Error::Anyhow(Box::new(err).context("failed to retrieve response body"))
+                })?
+                .to_bytes();
+            let error = String::from_utf8_lossy(&data).into_owned();
+            Err(anyhow::Error::msg(error))
+        }
+    }
+
     /// create an API-Token on the PBS remote and give the token admin ACL on everything.
     pub async fn create_admin_token(
         &self,
-- 
2.47.3





More information about the pdm-devel mailing list