Skip to content
34 changes: 33 additions & 1 deletion src/storage/examples/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use google_cloud_gax::{
};
use google_cloud_storage::client::{Storage, StorageControl};
use google_cloud_storage::model::bucket::{
ObjectRetention,
CustomPlacementConfig, ObjectRetention,
iam_config::UniformBucketLevelAccess,
{HierarchicalNamespace, IamConfig},
};
Expand Down Expand Up @@ -714,6 +714,38 @@ pub async fn create_test_hns_bucket() -> anyhow::Result<(StorageControl, Bucket)
Ok((client, create))
}

pub async fn create_test_rapid_bucket() -> anyhow::Result<(StorageControl, Bucket)> {
let project_id = std::env::var("GOOGLE_CLOUD_PROJECT")?;
let client = client_for_create_bucket().await?;
cleanup_stale_buckets(&client, &project_id).await?;

let bucket_id = crate::random_bucket_id();

let create = client
.create_bucket()
.set_parent("projects/_")
.set_bucket_id(bucket_id)
.set_bucket(
Bucket::new()
.set_project(format!("projects/{project_id}"))
.set_location("us-central1")
.set_custom_placement_config(
CustomPlacementConfig::new().set_data_locations(["us-central1-a".to_string()]),
Comment thread
vsharonlynn marked this conversation as resolved.
)
.set_storage_class("RAPID")
.set_labels([("integration-test", "true")])
.set_hierarchical_namespace(HierarchicalNamespace::new().set_enabled(true))
.set_iam_config(IamConfig::new().set_uniform_bucket_level_access(
UniformBucketLevelAccess::new().set_enabled(true),
)),
)
.with_idempotency(true)
.send()
.await?;
println!("create_test_rapid_bucket(): {create:?}");
Ok((client, create))
}

async fn client_for_create_bucket() -> anyhow::Result<StorageControl> {
let client = StorageControl::builder()
.with_tracing()
Expand Down
6 changes: 6 additions & 0 deletions src/storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,15 @@ pub mod builder {
pub mod storage {
//! Request builders for [Storage][crate::client::Storage].
pub use crate::storage::client::ClientBuilder;
#[cfg(google_cloud_unstable_storage_bidi)]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
pub use crate::storage::open_appendable_object::OpenAppendableObject;
pub use crate::storage::open_object::OpenObject;
pub use crate::storage::post_policy::{PostPolicyV4Builder, PostPolicyV4Result};
pub use crate::storage::read_object::ReadObject;
#[cfg(google_cloud_unstable_storage_bidi)]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
pub use crate::storage::reopen_appendable_object::ReopenAppendableObject;
pub use crate::storage::signed_url::SignedUrlBuilder;
pub use crate::storage::write_object::WriteObject;
}
Expand Down
107 changes: 12 additions & 95 deletions src/storage/src/model_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ use sha2::{Digest, Sha256};
mod open_object_request;
pub use open_object_request::OpenObjectRequest;

#[cfg(google_cloud_unstable_storage_bidi)]
mod open_appendable_object_request;
#[cfg(google_cloud_unstable_storage_bidi)]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
pub use open_appendable_object_request::OpenAppendableObjectRequest;

#[cfg(google_cloud_unstable_storage_bidi)]
mod reopen_appendable_object_request;
#[cfg(google_cloud_unstable_storage_bidi)]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
pub use reopen_appendable_object_request::ReopenAppendableObjectRequest;

/// ObjectHighlights contains select metadata from a [crate::model::Object].
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
Expand Down Expand Up @@ -316,61 +328,6 @@ pub struct WriteObjectRequest {
pub params: Option<crate::model::CommonObjectRequestParams>,
}

#[cfg(google_cloud_unstable_storage_bidi)]
/// Represents the parameters of a request to open a new object for exclusive appends.
///
/// Consumers of the `google-cloud-storage` crate rarely have a need to use this type directly, the most common exception is when mocking of the `Storage` client.
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub struct OpenAppendableObjectRequest {
/// The object attributes and pre-conditions for the open operation.
pub spec: crate::model::WriteObjectSpec,
/// Additional request parameters.
pub params: Option<crate::model::CommonObjectRequestParams>,
}

#[cfg(google_cloud_unstable_storage_bidi)]
/// Represents the parameters of a request to reopen an existing object for appends.
///
/// Consumers of the `google-cloud-storage` crate rarely have a need to use this type directly, the most common exception is when mocking of the `Storage` client.
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub struct ReopenAppendableObjectRequest {
/// The bucket containing the target object.
pub bucket: String,
/// The target object name.
pub object: String,
/// The target object generation to append to.
pub generation: i64,
/// If set, return an error if the current metageneration does not match the value.
pub if_metageneration_match: Option<i64>,
/// If set, return an error if the current metageneration matches the value.
pub if_metageneration_not_match: Option<i64>,
/// A routing token from a previous operation.
pub routing_token: Option<String>,
/// A write handle from a previous operation.
pub write_handle: Option<bytes::Bytes>,
/// Additional request parameters.
pub params: Option<crate::model::CommonObjectRequestParams>,
}

#[cfg(google_cloud_unstable_storage_bidi)]
impl From<ReopenAppendableObjectRequest> for crate::google::storage::v2::AppendObjectSpec {
fn from(value: ReopenAppendableObjectRequest) -> Self {
Self {
bucket: value.bucket,
object: value.object,
generation: value.generation,
if_metageneration_match: value.if_metageneration_match,
if_metageneration_not_match: value.if_metageneration_not_match,
routing_token: value.routing_token,
write_handle: value
.write_handle
.map(|h| crate::google::storage::v2::BidiWriteHandle { handle: h }),
}
}
}

#[cfg(test)]
pub(crate) mod tests {
use super::*;
Expand Down Expand Up @@ -490,44 +447,4 @@ pub(crate) mod tests {
assert_eq!(key_aes_256.to_string(), key_base64);
Ok(())
}

#[cfg(google_cloud_unstable_storage_bidi)]
#[test]
fn test_open_appendable_object_request() {
let req = OpenAppendableObjectRequest {
spec: crate::model::WriteObjectSpec::default(),
params: None,
};
assert_eq!(req.spec.resource, None);
assert_eq!(req.params, None);
}

#[cfg(google_cloud_unstable_storage_bidi)]
#[test]
fn test_reopen_appendable_object_request_from() {
let req = ReopenAppendableObjectRequest {
bucket: "my-bucket".into(),
object: "my-object".into(),
generation: 42,
if_metageneration_match: Some(1),
if_metageneration_not_match: Some(2),
routing_token: Some("token".into()),
write_handle: Some(bytes::Bytes::from("handle")),
params: None,
};

let spec = crate::google::storage::v2::AppendObjectSpec::from(req);
assert_eq!(spec.bucket, "my-bucket");
assert_eq!(spec.object, "my-object");
assert_eq!(spec.generation, 42);
assert_eq!(spec.if_metageneration_match, Some(1));
assert_eq!(spec.if_metageneration_not_match, Some(2));
assert_eq!(spec.routing_token, Some("token".into()));
assert_eq!(
spec.write_handle,
Some(crate::google::storage::v2::BidiWriteHandle {
handle: bytes::Bytes::from("handle")
})
);
}
}
41 changes: 41 additions & 0 deletions src/storage/src/model_ext/open_appendable_object_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2026 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.

#[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
/// Represents the parameters of a request to open a new object for exclusive appends.
///
/// Consumers of the `google-cloud-storage` crate rarely have a need to use this type directly, the most common exception is when mocking of the `Storage` client.
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct OpenAppendableObjectRequest {
/// The object attributes and pre-conditions for the open operation.
pub spec: crate::model::WriteObjectSpec,
/// Additional request parameters.
pub params: Option<crate::model::CommonObjectRequestParams>,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_open_appendable_object_request() {
let req = OpenAppendableObjectRequest {
spec: crate::model::WriteObjectSpec::default(),
params: None,
};
assert_eq!(req.spec.resource, None);
assert_eq!(req.params, None);
}
}
87 changes: 87 additions & 0 deletions src/storage/src/model_ext/reopen_appendable_object_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2026 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.

#[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
/// Represents the parameters of a request to reopen an existing object for appends.
///
/// Consumers of the `google-cloud-storage` crate rarely have a need to use this type directly, the most common exception is when mocking of the `Storage` client.
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct ReopenAppendableObjectRequest {
/// The bucket containing the target object.
pub bucket: String,
/// The target object name.
pub object: String,
/// The target object generation to append to.
pub generation: i64,
/// If set, return an error if the current metageneration does not match the value.
pub if_metageneration_match: Option<i64>,
/// If set, return an error if the current metageneration matches the value.
pub if_metageneration_not_match: Option<i64>,
/// A routing token from a previous operation.
pub routing_token: Option<String>,
/// A write handle from a previous operation.
pub write_handle: Option<bytes::Bytes>,
/// Additional request parameters.
pub params: Option<crate::model::CommonObjectRequestParams>,
}

impl From<ReopenAppendableObjectRequest> for crate::google::storage::v2::AppendObjectSpec {
fn from(value: ReopenAppendableObjectRequest) -> Self {
Self {
bucket: value.bucket,
object: value.object,
generation: value.generation,
if_metageneration_match: value.if_metageneration_match,
if_metageneration_not_match: value.if_metageneration_not_match,
routing_token: value.routing_token,
write_handle: value
.write_handle
.map(|h| crate::google::storage::v2::BidiWriteHandle { handle: h }),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_reopen_appendable_object_request_from() {
let req = ReopenAppendableObjectRequest {
bucket: "my-bucket".into(),
object: "my-object".into(),
generation: 42,
if_metageneration_match: Some(1),
if_metageneration_not_match: Some(2),
routing_token: Some("token".into()),
write_handle: Some(bytes::Bytes::from("handle")),
params: None,
};

let spec = crate::google::storage::v2::AppendObjectSpec::from(req);
assert_eq!(spec.bucket, "my-bucket");
assert_eq!(spec.object, "my-object");
assert_eq!(spec.generation, 42);
assert_eq!(spec.if_metageneration_match, Some(1));
assert_eq!(spec.if_metageneration_not_match, Some(2));
assert_eq!(spec.routing_token, Some("token".into()));
assert_eq!(
spec.write_handle,
Some(crate::google::storage::v2::BidiWriteHandle {
handle: bytes::Bytes::from("handle")
})
);
}
}
4 changes: 4 additions & 0 deletions src/storage/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ pub(crate) mod bidi_write;
pub(crate) mod checksum;
pub(crate) mod client;
pub(crate) mod common_options;
#[cfg(google_cloud_unstable_storage_bidi)]
pub(crate) mod open_appendable_object;
pub(crate) mod open_object;
pub(crate) mod perform_upload;
pub(crate) mod post_policy;
pub(crate) mod read_object;
#[cfg(google_cloud_unstable_storage_bidi)]
pub(crate) mod reopen_appendable_object;
pub mod request_options;
pub(crate) mod signed_url;
pub mod streaming_source;
Expand Down
12 changes: 8 additions & 4 deletions src/storage/src/storage/bidi_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@

//! Internal traits and types for Appendable Object Write (Bidi Write).

#![allow(dead_code)]

pub(crate) mod connector;
mod redirect;
mod retry_redirect;
pub(crate) mod stub;
pub(crate) mod transport;
mod worker;

use crate::google::storage::v2::{BidiWriteObjectRequest, BidiWriteObjectResponse};
use crate::request_options::RequestOptions;
Expand All @@ -27,7 +32,6 @@ use tokio::sync::mpsc::Receiver;
/// A trait to mock `Streaming<T>` in the unit tests.
///
/// This is not a public trait, we only need this for our own testing.
#[allow(dead_code)]
pub(crate) trait TonicStreaming: std::fmt::Debug + Send + 'static {
fn next_message(
&mut self,
Expand All @@ -44,7 +48,6 @@ impl TonicStreaming for Streaming<BidiWriteObjectResponse> {
/// A trait to mock `gaxi::grpc::Client` in the unit tests.
///
/// This is not a public trait, we only need this for our own testing.
#[allow(dead_code)]
pub(crate) trait Client: std::fmt::Debug + Send + 'static {
type Stream: Sized;
fn start(
Expand Down Expand Up @@ -82,6 +85,9 @@ impl Client for gaxi::grpc::Client {
}
}

#[cfg(test)]
mod mocks;

#[cfg(test)]
pub(crate) mod tests {
use crate::Error;
Expand Down Expand Up @@ -135,14 +141,12 @@ pub(crate) mod tests {
)
}

#[allow(dead_code)]
pub(crate) fn test_options() -> RequestOptions {
let mut options = RequestOptions::new();
options.backoff_policy = Arc::new(test_backoff());
options
}

#[allow(dead_code)]
fn test_backoff() -> impl google_cloud_gax::backoff_policy::BackoffPolicy {
use std::time::Duration;
google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder::new()
Expand Down
Loading
Loading