[pbs-devel] [PATCH proxmox v3 3/4] s3 client: implement list buckets method
Lukas Wagner
l.wagner at proxmox.com
Thu Jul 31 13:43:25 CEST 2025
On Thu Jul 31, 2025 at 12:48 PM CEST, Christian Ebner wrote:
> Extends the client by the list buckets method which allows to
> fetch the buckets owned by the user with who's access key the
> request is signed.
>
> Signed-off-by: Christian Ebner <c.ebner at proxmox.com>
> ---
> changes since version 2:
> - no changes
>
> changes since version 1:
> - fixed parsing issue
>
> proxmox-s3-client/src/client.rs | 14 +++++-
> proxmox-s3-client/src/response_reader.rs | 56 ++++++++++++++++++++++++
> 2 files changed, 69 insertions(+), 1 deletion(-)
>
> diff --git a/proxmox-s3-client/src/client.rs b/proxmox-s3-client/src/client.rs
> index 91188f1a..a2c62811 100644
> --- a/proxmox-s3-client/src/client.rs
> +++ b/proxmox-s3-client/src/client.rs
> @@ -28,7 +28,7 @@ use crate::aws_sign_v4::{aws_sign_v4_signature, aws_sign_v4_uri_encode};
> use crate::object_key::S3ObjectKey;
> use crate::response_reader::{
> CopyObjectResponse, DeleteObjectsResponse, GetObjectResponse, HeadObjectResponse,
> - ListObjectsV2Response, PutObjectResponse, ResponseReader,
> + ListBucketsResponse, ListObjectsV2Response, PutObjectResponse, ResponseReader,
> };
>
> const S3_HTTP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
> @@ -319,6 +319,18 @@ impl S3Client {
> Ok(())
> }
>
> + /// List all buckets owned by the user authenticated via the access key.
> + /// See reference docs: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html
> + pub async fn list_buckets(&self) -> Result<ListBucketsResponse, Error> {
> + let request = Request::builder()
> + .method(Method::GET)
> + .uri(self.build_uri("/", &[])?)
> + .body(Body::empty())?;
> + let response = self.send(request).await?;
> + let response_reader = ResponseReader::new(response);
> + response_reader.list_buckets_response().await
> + }
> +
> /// Fetch metadata from an object without returning the object itself.
> /// See reference docs: https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html
> pub async fn head_object(
> diff --git a/proxmox-s3-client/src/response_reader.rs b/proxmox-s3-client/src/response_reader.rs
> index a7c71639..f9d7f1ac 100644
> --- a/proxmox-s3-client/src/response_reader.rs
> +++ b/proxmox-s3-client/src/response_reader.rs
> @@ -153,6 +153,38 @@ pub struct CopyObjectResult {
> pub last_modified: LastModifiedTimestamp,
> }
>
> +/// Subset of the list buckets response
> +/// https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html#API_ListBuckets_ResponseElements
> +#[derive(Deserialize, Debug)]
> +#[serde(rename_all = "PascalCase")]
> +pub struct ListBucketsResponse {
> + pub buckets: Vec<Bucket>,
Missing doc-comments for `pbu` struct member.
> +}
> +/// Subset of the list buckets response
> +/// https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html#API_ListBuckets_ResponseElements
> +#[derive(Deserialize, Debug)]
> +#[serde(rename_all = "PascalCase")]
> +pub struct ListAllMyBucketsResult {
> + pub buckets: Option<Buckets>,
> +}
> +
> +/// Subset used to deserialize the list buckets response
> +#[derive(Deserialize, Debug)]
> +#[serde(rename_all = "PascalCase")]
> +pub struct Buckets {
> + bucket: Vec<Bucket>,
> +}
> +
> +/// Subset used to deserialize the list buckets response
> +#[derive(Deserialize, Debug)]
> +#[serde(rename_all = "PascalCase")]
> +pub struct Bucket {
> + pub name: String,
> + pub bucket_arn: Option<String>,
> + pub bucket_region: Option<String>,
> + pub creation_date: LastModifiedTimestamp,
Missing doc-comments for `pub` struct member.
> +}
> +
> impl ResponseReader {
> pub(crate) fn new(response: Response<Incoming>) -> Self {
> Self { response }
> @@ -339,6 +371,30 @@ impl ResponseReader {
> })
> }
>
Missing doc comment for this function here.
> + pub(crate) async fn list_buckets_response(self) -> Result<ListBucketsResponse, Error> {
> + let (parts, body) = self.response.into_parts();
> + let body = body.collect().await?.to_bytes();
> +
> + match parts.status {
> + StatusCode::OK => (),
> + status_code => {
> + Self::log_error_response_utf8(body);
> + bail!("unexpected status code {status_code}")
> + }
> + };
This 'match' reads a bit weird to me, how about:
if !matches!(parts.status, Status::OK) {
...
bail!(...)
}
> +
> + let body = String::from_utf8(body.to_vec())?;
> +
> + let list_buckets_result: ListAllMyBucketsResult =
> + serde_xml_rs::from_str(&body).context("failed to parse response body")?;
> +
> + let buckets = match list_buckets_result.buckets {
> + Some(buckets) => buckets.bucket,
> + None => Vec::new(),
> + };
This could be
let buckets = list_buckets_results.buckets.map(|b| b.bucket).unwrap_or_default();
and be a bit shorter, but no hard feelings.
> + Ok(ListBucketsResponse { buckets })
> + }
> +
> fn log_error_response_utf8(body: Bytes) {
> if let Ok(body) = String::from_utf8(body.to_vec()) {
> if !body.is_empty() {
Otherwise it looks good to me:
Reviewed-by: Lukas Wagner <l.wagner at proxmox.com>
More information about the pbs-devel
mailing list