Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub mod builder {
pub mod storage {
//! Request builders for [Storage][crate::client::Storage].
pub use crate::storage::client::ClientBuilder;
pub use crate::storage::compose_object::ComposeObject;
pub use crate::storage::open_object::OpenObject;
pub use crate::storage::read_object::ReadObject;
pub use crate::storage::signed_url::SignedUrlBuilder;
Expand Down
1 change: 1 addition & 0 deletions src/storage/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub(crate) mod bidi;
pub(crate) mod checksum;
pub(crate) mod client;
pub(crate) mod common_options;
pub(crate) mod compose_object;
pub(crate) mod open_object;
pub(crate) mod perform_upload;
pub(crate) mod read_object;
Expand Down
29 changes: 29 additions & 0 deletions src/storage/src/storage/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

use super::request_options::RequestOptions;
use crate::builder::storage::ComposeObject;
use crate::builder::storage::ReadObject;
use crate::builder::storage::WriteObject;
use crate::read_resume_policy::ReadResumePolicy;
Expand Down Expand Up @@ -222,6 +223,34 @@ where
ReadObject::new(self.stub.clone(), bucket, object, self.options.clone())
}

/// Concatenates multiple source objects in the same bucket into a single composite object.
///
/// # Example
/// ```
/// # use google_cloud_storage::client::Storage;
/// # async fn sample(client: &Storage) -> anyhow::Result<()> {
/// let response = client
/// .compose_object("projects/_/buckets/my-bucket", "my-composite-object")
/// .add_source("part-1")
/// .add_source("part-2")
/// .send()
/// .await?;
/// println!("composite object={response:?}");
/// # Ok(()) }
/// ```
///
/// # Parameters
/// * `bucket` - the bucket name containing the source and destination objects. In
/// `projects/_/buckets/{bucket_id}` format.
/// * `destination` - the name of the resulting composite object.
pub fn compose_object<B, D>(&self, bucket: B, destination: D) -> ComposeObject<S>
where
B: Into<String>,
D: Into<String>,
{
ComposeObject::new(self.stub.clone(), bucket, destination, self.options.clone())
}

/// Opens an object to read its contents using concurrent ranged reads.
///
/// # Example
Expand Down
149 changes: 149 additions & 0 deletions src/storage/src/storage/compose_object.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::Result;
use crate::model::Object;
use crate::request_options::RequestOptions;
use std::sync::Arc;

/// A request builder for [Storage::compose_object][crate::client::Storage::compose_object].
///
/// # Example
/// ```
/// use google_cloud_storage::client::Storage;
/// # use google_cloud_storage::builder::storage::ComposeObject;
/// async fn sample(client: &Storage) -> anyhow::Result<()> {
/// let response = client
/// .compose_object("projects/_/buckets/my-bucket", "composite-object")
/// .add_source("part-1")
/// .add_source_with_generation("part-2", 123456)
/// .set_content_type("application/json")
/// .send()
/// .await?;
/// println!("composite object details={response:?}");
/// Ok(())
/// }
/// ```
#[derive(Clone, Debug)]
pub struct ComposeObject<S = crate::storage::transport::Storage> {
stub: Arc<S>,
request: crate::model::ComposeObjectRequest,
options: RequestOptions,
}

impl<S> ComposeObject<S> {
pub(crate) fn new<B, D>(
stub: Arc<S>,
bucket: B,
destination: D,
options: RequestOptions,
) -> Self
where
B: Into<String>,
D: Into<String>,
{
let mut request = crate::model::ComposeObjectRequest::default();
request.destination = Some(crate::model::Object {
bucket: bucket.into(),
name: destination.into(),
..Default::default()
});
Self {
stub,
request,
options,
}
}
}

impl<S> ComposeObject<S>
where
S: crate::storage::stub::Storage + 'static,
{
/// Appends a source object name to the compose request.
pub fn add_source<T: Into<String>>(mut self, name: T) -> Self {
let mut source = crate::model::compose_object_request::SourceObject::default();
source.name = name.into();
self.request.source_objects.push(source);
self
}

/// Appends a source object name and its expected generation to the compose request.
pub fn add_source_with_generation<T: Into<String>>(mut self, name: T, generation: i64) -> Self {
let mut source = crate::model::compose_object_request::SourceObject::default();
source.name = name.into();
source.generation = generation;
self.request.source_objects.push(source);
self
}

/// Appends a source object name, generation, and generation match precondition.
pub fn add_source_with_preconditions<T: Into<String>>(
mut self,
name: T,
generation: i64,
if_generation_match: i64,
) -> Self {
let mut source = crate::model::compose_object_request::SourceObject::default();
source.name = name.into();
source.generation = generation;
let mut preconditions = crate::model::compose_object_request::source_object::ObjectPreconditions::default();
preconditions.if_generation_match = Some(if_generation_match);
source.object_preconditions = Some(preconditions);
self.request.source_objects.push(source);
self
}

/// Sets the generation match precondition for the destination object.
pub fn set_if_generation_match(mut self, v: i64) -> Self {
self.request.if_generation_match = Some(v);
self
}

/// Sets the metageneration match precondition for the destination object.
pub fn set_if_metageneration_match(mut self, v: i64) -> Self {
self.request.if_metageneration_match = Some(v);
self
}

/// Sets the content type for the destination composite object.
pub fn set_content_type<T: Into<String>>(mut self, v: T) -> Self {
if let Some(ref mut dest) = self.request.destination {
dest.content_type = v.into();
}
self
}

/// Sets custom metadata attributes for the destination composite object.
pub fn set_metadata(mut self, metadata: std::collections::HashMap<String, String>) -> Self {
if let Some(ref mut dest) = self.request.destination {
dest.metadata = metadata;
}
self
}
Comment on lines +129 to +134
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While set_metadata allows replacing the entire metadata map, it is often more idiomatic in builder patterns to provide a fluent method for adding individual metadata entries. This improves usability when the user only needs to set a few custom attributes without constructing a full HashMap themselves.

    /// Adds a custom metadata attribute for the destination composite object.
    pub fn add_metadata<K, V>(mut self, key: K, value: V) -> Self
    where
        K: Into<String>,
        V: Into<String>,
    {
        if let Some(ref mut dest) = self.request.destination {
            dest.metadata.insert(key.into(), value.into());
        }
        self
    }

    /// Sets custom metadata attributes for the destination composite object.
    pub fn set_metadata(mut self, metadata: std::collections::HashMap<String, String>) -> Self {
        if let Some(ref mut dest) = self.request.destination {
            dest.metadata = metadata;
        }
        self
    }
References
  1. In code samples and examples, prioritize demonstrating the methods that are most likely to be used by developers in typical scenarios, even if safer alternatives exist.


/// Configures a custom retry policy for this compose request.
pub fn with_retry_policy<V: Into<google_cloud_gax::retry_policy::RetryPolicyArg>>(
mut self,
v: V,
) -> Self {
self.options.retry_policy = v.into().into();
self
}

/// Sends the compose request to the GCS service and returns the created composite object.
pub async fn send(self) -> Result<Object> {
self.stub.compose_object(self.request, self.options).await
}
}
9 changes: 9 additions & 0 deletions src/storage/src/storage/stub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ pub trait Storage: std::fmt::Debug + Send + Sync {
{
unimplemented_stub::<(Descriptor, Vec<ReadObjectResponse>)>()
}

/// Implements [crate::client::Storage::compose_object].
fn compose_object(
&self,
_req: crate::model::ComposeObjectRequest,
_options: RequestOptions,
) -> impl std::future::Future<Output = Result<Object>> + Send {
unimplemented_stub::<Object>()
}
}

/// Defines the trait used to implement [crate::object_descriptor::ObjectDescriptor].
Expand Down
Loading
Loading