Skip to main content

acyclic_objects/
grpc.rs

1//! Idiomatic gRPC client for the public Objects contract.
2
3use crate::{Condition, wire};
4use bytes::Bytes;
5use futures::{Stream, StreamExt, stream::iter};
6use prost::Message;
7use thiserror::Error;
8use tonic::{
9    Request,
10    metadata::{Ascii, MetadataValue},
11    transport::{Certificate, Channel, ClientTlsConfig, Endpoint},
12};
13
14const BODY_FRAME_BYTES: usize = 64 * 1024;
15/// Maximum caller-supplied private CA bundle accepted by the client.
16pub const MAX_CA_CERTIFICATE_BYTES: usize = 64 * 1024;
17
18/// Errors returned while constructing or using a client.
19#[derive(Debug, Error)]
20pub enum Error {
21    /// The configured endpoint was not a valid URI.
22    #[error("invalid Objects endpoint: {0}")]
23    InvalidEndpoint(#[from] tonic::transport::Error),
24    /// Customer endpoints must use authenticated TLS.
25    #[error("Objects customer endpoints must use https")]
26    InsecureEndpoint,
27    /// The supplied bearer credential could not be encoded as request metadata.
28    #[error("invalid Objects bearer credential")]
29    InvalidCredential,
30    /// The caller-supplied private CA bundle was empty or exceeded its fixed bound.
31    #[error("invalid Objects CA certificate bundle")]
32    InvalidCaCertificate,
33    /// The service rejected a request with a canonical public semantic code.
34    #[error("Objects request rejected ({code:?}, request {request_id}): {message}")]
35    Rejected {
36        /// Stable public error category.
37        code: wire::ErrorCode,
38        /// Request identity supplied by the service when available.
39        request_id: String,
40        /// Human-readable diagnostic, not suitable for control flow.
41        message: String,
42    },
43    /// The transport failed without a valid canonical Objects error detail.
44    #[error("Objects transport failed: {0}")]
45    Transport(tonic::Status),
46    /// A successful response violated the versioned protocol contract.
47    #[error("invalid Objects protocol response: {0}")]
48    Protocol(&'static str),
49}
50
51impl From<tonic::Status> for Error {
52    fn from(status: tonic::Status) -> Self {
53        if let Ok(detail) = wire::ErrorDetail::decode(status.details())
54            && let Ok(code) = wire::ErrorCode::try_from(detail.code)
55            && code != wire::ErrorCode::Unspecified
56        {
57            return Self::Rejected {
58                code,
59                request_id: detail.request_id,
60                message: status.message().to_owned(),
61            };
62        }
63        Self::Transport(status)
64    }
65}
66
67/// Result type used by this SDK.
68pub type Result<T> = std::result::Result<T, Error>;
69
70/// A configured Objects account client.
71#[derive(Clone)]
72pub struct Client {
73    channel: Channel,
74    authorization: MetadataValue<Ascii>,
75}
76
77impl Client {
78    /// Connect to an account endpoint using its account-bound bearer credential.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error when the endpoint/credential is invalid or the TLS connection fails.
83    pub async fn connect(endpoint: impl AsRef<str>, bearer_token: impl AsRef<str>) -> Result<Self> {
84        Self::connect_with_tls(endpoint, bearer_token, None).await
85    }
86
87    /// Connect to an account endpoint augmented by one caller-supplied private CA bundle.
88    ///
89    /// The certificate augments the client's roots for this connection only. HTTPS remains
90    /// mandatory, and the process-wide trust store is never changed.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error when the endpoint, credential, or bounded CA bundle is invalid, or when
95    /// the authenticated TLS connection fails.
96    pub async fn connect_with_ca_certificate(
97        endpoint: impl AsRef<str>,
98        bearer_token: impl AsRef<str>,
99        certificate_pem: impl AsRef<[u8]>,
100    ) -> Result<Self> {
101        let certificate_pem = bounded_ca_certificate(certificate_pem.as_ref())?;
102        Self::connect_with_tls(endpoint, bearer_token, Some(certificate_pem)).await
103    }
104
105    async fn connect_with_tls(
106        endpoint: impl AsRef<str>,
107        bearer_token: impl AsRef<str>,
108        certificate_pem: Option<&[u8]>,
109    ) -> Result<Self> {
110        let mut endpoint = Endpoint::new(endpoint.as_ref().to_owned())?;
111        if endpoint.uri().scheme_str() != Some("https") {
112            return Err(Error::InsecureEndpoint);
113        }
114        if let Some(certificate_pem) = certificate_pem {
115            endpoint = endpoint.tls_config(
116                ClientTlsConfig::new()
117                    .with_enabled_roots()
118                    .ca_certificate(Certificate::from_pem(certificate_pem)),
119            )?;
120        }
121        let authorization = format!("Bearer {}", bearer_token.as_ref())
122            .parse::<MetadataValue<Ascii>>()
123            .map_err(|_| Error::InvalidCredential)?;
124        let channel = endpoint.connect().await?;
125        Ok(Self {
126            channel,
127            authorization,
128        })
129    }
130
131    /// Create a permanently versioned bucket.
132    ///
133    /// # Errors
134    ///
135    /// Returns an error when the service rejects or cannot complete the request.
136    pub async fn create_bucket(
137        &self,
138        name: impl Into<String>,
139        options: MutationOptions,
140    ) -> Result<Bucket> {
141        let mut client =
142            wire::buckets_service_client::BucketsServiceClient::new(self.channel.clone());
143        let response = client
144            .create_bucket(authenticated(
145                &self.authorization,
146                wire::CreateBucketRequest {
147                    name: name.into(),
148                    mutation: options.wire(),
149                },
150            ))
151            .await?
152            .into_inner();
153        let reference = response
154            .bucket
155            .ok_or(Error::Protocol("missing bucket identity"))?;
156        Ok(Bucket::new(self, reference))
157    }
158
159    /// Validate and open one exact persisted bucket identity.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error when the identity is stale, mismatched, or unavailable.
164    pub async fn open_bucket(&self, reference: wire::BucketRef) -> Result<Bucket> {
165        let mut client =
166            wire::buckets_service_client::BucketsServiceClient::new(self.channel.clone());
167        let response = client
168            .head_bucket(authenticated(
169                &self.authorization,
170                wire::HeadBucketRequest {
171                    bucket: Some(reference.clone()),
172                },
173            ))
174            .await?
175            .into_inner();
176        if response.bucket.as_ref() != Some(&reference) {
177            return Err(Error::Protocol("bucket identity changed"));
178        }
179        Ok(Bucket::new(self, reference))
180    }
181
182    /// Reconstruct a read-only snapshot handle from its exact persisted identity.
183    #[must_use]
184    pub fn snapshot(&self, reference: wire::SnapshotRef) -> Snapshot {
185        Snapshot::new(self, reference)
186    }
187}
188
189fn bounded_ca_certificate(certificate_pem: &[u8]) -> Result<&[u8]> {
190    if certificate_pem.is_empty() || certificate_pem.len() > MAX_CA_CERTIFICATE_BYTES {
191        return Err(Error::InvalidCaCertificate);
192    }
193    Ok(certificate_pem)
194}
195
196/// Options common to retryable mutations.
197#[derive(Clone, Debug, Default)]
198pub struct MutationOptions {
199    idempotency_key: String,
200}
201
202impl MutationOptions {
203    /// Attach an account-scoped idempotency key retained for seven days.
204    #[must_use]
205    pub fn idempotency_key(mut self, value: impl Into<String>) -> Self {
206        self.idempotency_key = value.into();
207        self
208    }
209
210    fn wire(&self) -> Option<wire::MutationIdentity> {
211        (!self.idempotency_key.is_empty()).then(|| wire::MutationIdentity {
212            idempotency_key: self.idempotency_key.clone(),
213        })
214    }
215}
216
217/// Options for a single object put.
218#[derive(Clone, Debug, Default)]
219pub struct PutOptions {
220    metadata: wire::ObjectMetadata,
221    mutation: MutationOptions,
222    condition: Option<Condition>,
223}
224
225impl PutOptions {
226    /// Attach an account-scoped idempotency key retained for seven days.
227    #[must_use]
228    pub fn idempotency_key(mut self, value: impl Into<String>) -> Self {
229        self.mutation = self.mutation.idempotency_key(value);
230        self
231    }
232
233    /// Set the immutable content type for this version.
234    #[must_use]
235    pub fn content_type(mut self, value: impl Into<String>) -> Self {
236        self.metadata.content_type = value.into();
237        self
238    }
239
240    /// Set the immutable content encoding for this version.
241    #[must_use]
242    pub fn content_encoding(mut self, value: impl Into<String>) -> Self {
243        self.metadata.content_encoding = value.into();
244        self
245    }
246
247    /// Set the immutable cache-control value for this version.
248    #[must_use]
249    pub fn cache_control(mut self, value: impl Into<String>) -> Self {
250        self.metadata.cache_control = value.into();
251        self
252    }
253
254    /// Set the immutable content disposition for this version.
255    #[must_use]
256    pub fn content_disposition(mut self, value: impl Into<String>) -> Self {
257        self.metadata.content_disposition = value.into();
258        self
259    }
260
261    /// Set the immutable content language for this version.
262    #[must_use]
263    pub fn content_language(mut self, value: impl Into<String>) -> Self {
264        self.metadata.content_language = value.into();
265        self
266    }
267
268    /// Set the immutable HTTP expiry time as Unix seconds.
269    #[must_use]
270    pub fn expires_unix_seconds(mut self, value: i64) -> Self {
271        self.metadata.expires_unix_seconds = Some(value);
272        self
273    }
274
275    /// Add immutable user metadata to this version.
276    #[must_use]
277    pub fn user_metadata(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
278        self.metadata.user.insert(name.into(), value.into());
279        self
280    }
281
282    /// Attach the sole current-version condition.
283    #[must_use]
284    pub fn condition(mut self, value: Condition) -> Self {
285        self.condition = Some(value);
286        self
287    }
288}
289
290/// Options for an object read or head.
291#[derive(Clone, Debug)]
292pub struct GetOptions {
293    version_id: String,
294    range: Option<(u64, Option<u64>)>,
295    if_match: String,
296    if_none_match: String,
297    maximum_bytes: u64,
298}
299
300impl Default for GetOptions {
301    fn default() -> Self {
302        Self {
303            version_id: String::new(),
304            range: None,
305            if_match: String::new(),
306            if_none_match: String::new(),
307            maximum_bytes: crate::limits::OBJECT_BYTES,
308        }
309    }
310}
311
312impl GetOptions {
313    /// Read one exact immutable version.
314    #[must_use]
315    pub fn version(mut self, value: impl Into<String>) -> Self {
316        self.version_id = value.into();
317        self
318    }
319
320    /// Read one inclusive byte range.
321    #[must_use]
322    pub fn range(mut self, start: u64, end_inclusive: Option<u64>) -> Self {
323        self.range = Some((start, end_inclusive));
324        self
325    }
326
327    /// Require one opaque `ETag`.
328    #[must_use]
329    pub fn if_match(mut self, value: impl Into<String>) -> Self {
330        self.if_match = value.into();
331        self
332    }
333
334    /// Reject one opaque `ETag`.
335    #[must_use]
336    pub fn if_none_match(mut self, value: impl Into<String>) -> Self {
337        self.if_none_match = value.into();
338        self
339    }
340
341    /// Bound the selected response body before any body frame is decoded.
342    #[must_use]
343    pub fn maximum_bytes(mut self, value: u64) -> Self {
344        self.maximum_bytes = value;
345        self
346    }
347}
348
349/// Options for object deletion.
350#[derive(Clone, Debug, Default)]
351pub struct DeleteOptions {
352    version_id: String,
353    mutation: MutationOptions,
354    condition: Option<Condition>,
355}
356
357impl DeleteOptions {
358    /// Delete one exact immutable version rather than publishing a marker.
359    #[must_use]
360    pub fn version(mut self, value: impl Into<String>) -> Self {
361        self.version_id = value.into();
362        self
363    }
364
365    /// Attach an account-scoped idempotency key.
366    #[must_use]
367    pub fn idempotency_key(mut self, value: impl Into<String>) -> Self {
368        self.mutation = self.mutation.idempotency_key(value);
369        self
370    }
371
372    /// Attach the sole current-version condition for marker publication.
373    #[must_use]
374    pub fn condition(mut self, value: Condition) -> Self {
375        self.condition = Some(value);
376        self
377    }
378}
379
380/// Options for one stable listing page.
381#[derive(Clone, Debug)]
382pub struct ListOptions {
383    /// Prefix filter.
384    pub prefix: String,
385    /// Optional delimiter.
386    pub delimiter: String,
387    /// Include every version instead of live current versions.
388    pub versions: bool,
389    /// Maximum combined objects and common prefixes.
390    pub page_size: u32,
391    /// Opaque continuation token.
392    pub continuation: String,
393}
394
395impl Default for ListOptions {
396    fn default() -> Self {
397        Self {
398            prefix: String::new(),
399            delimiter: String::new(),
400            versions: false,
401            page_size: 1_000,
402            continuation: String::new(),
403        }
404    }
405}
406
407impl ListOptions {
408    /// Restrict results to keys beginning with `value`.
409    #[must_use]
410    pub fn prefix(mut self, value: impl Into<String>) -> Self {
411        self.prefix = value.into();
412        self
413    }
414
415    /// Group keys at the first occurrence of `value` after the prefix.
416    #[must_use]
417    pub fn delimiter(mut self, value: impl Into<String>) -> Self {
418        self.delimiter = value.into();
419        self
420    }
421
422    /// Include every immutable version instead of only current live objects.
423    #[must_use]
424    pub fn versions(mut self, value: bool) -> Self {
425        self.versions = value;
426        self
427    }
428
429    /// Bound the combined number of entries and common prefixes.
430    #[must_use]
431    pub fn page_size(mut self, value: u32) -> Self {
432        self.page_size = value;
433        self
434    }
435
436    /// Continue a previous listing from its opaque token.
437    #[must_use]
438    pub fn continuation(mut self, value: impl Into<String>) -> Self {
439        self.continuation = value.into();
440        self
441    }
442}
443
444/// One typed stable listing page.
445#[derive(Clone, Debug)]
446pub struct ListPage {
447    /// Ordered object/version entries.
448    pub entries: Vec<wire::ListEntry>,
449    /// Ordered delimiter-derived prefixes.
450    pub common_prefixes: Vec<String>,
451    /// Opaque continuation token, absent on the final page.
452    pub continuation: Option<String>,
453}
454
455impl ListPage {
456    /// Ordered object/version entries in this page.
457    #[must_use]
458    pub fn entries(&self) -> &[wire::ListEntry] {
459        &self.entries
460    }
461}
462
463/// A handle bound to one exact bucket identity.
464#[derive(Clone)]
465pub struct Bucket {
466    channel: Channel,
467    authorization: MetadataValue<Ascii>,
468    reference: wire::BucketRef,
469}
470
471impl Bucket {
472    fn new(client: &Client, reference: wire::BucketRef) -> Self {
473        Self {
474            channel: client.channel.clone(),
475            authorization: client.authorization.clone(),
476            reference,
477        }
478    }
479
480    /// The opaque bucket identity. Name reuse never revives it.
481    #[must_use]
482    pub fn id(&self) -> &str {
483        &self.reference.bucket_id
484    }
485
486    /// The account-scoped bucket name.
487    #[must_use]
488    pub fn name(&self) -> &str {
489        &self.reference.name
490    }
491
492    /// Copy the complete opaque bucket identity for durable application state.
493    #[must_use]
494    pub fn reference(&self) -> wire::BucketRef {
495        self.reference.clone()
496    }
497
498    /// Delete this exact bucket when it has no versions, markers, or multipart uploads.
499    ///
500    /// # Errors
501    ///
502    /// Returns an error when the bucket is not empty or the service cannot complete the request.
503    pub async fn destroy(&self, options: MutationOptions) -> Result<bool> {
504        let mut client =
505            wire::buckets_service_client::BucketsServiceClient::new(self.channel.clone());
506        Ok(client
507            .delete_bucket(authenticated(
508                &self.authorization,
509                wire::DeleteBucketRequest {
510                    bucket: Some(self.reference.clone()),
511                    mutation: options.wire(),
512                },
513            ))
514            .await?
515            .into_inner()
516            .existed)
517    }
518
519    /// Create one immutable version from buffered bytes.
520    ///
521    /// # Errors
522    ///
523    /// Returns an error when the service rejects or cannot complete the request.
524    pub async fn put(
525        &self,
526        object_key: impl Into<String>,
527        body: Bytes,
528        options: PutOptions,
529    ) -> Result<wire::ObjectVersion> {
530        self.put_stream(object_key, iter([body]), options).await
531    }
532
533    /// Stream one immutable version without buffering it in the SDK.
534    ///
535    /// # Errors
536    ///
537    /// Returns an error when the service rejects or cannot complete the request.
538    pub async fn put_stream<S>(
539        &self,
540        object_key: impl Into<String>,
541        body: S,
542        options: PutOptions,
543    ) -> Result<wire::ObjectVersion>
544    where
545        S: Stream<Item = Bytes> + Send + 'static,
546    {
547        let header = wire::PutObjectRequest {
548            frame: Some(wire::put_object_request::Frame::Header(
549                wire::PutObjectHeader {
550                    bucket: Some(self.reference.clone()),
551                    object_key: object_key.into(),
552                    metadata: Some(options.metadata),
553                    preconditions: options.condition.map(Condition::wire),
554                    mutation: options.mutation.wire(),
555                },
556            )),
557        };
558        let frames = iter([header]).chain(body.flat_map(|chunk| {
559            iter(
560                chunk
561                    .chunks(BODY_FRAME_BYTES)
562                    .map(|frame| wire::PutObjectRequest {
563                        frame: Some(wire::put_object_request::Frame::Body(frame.to_vec())),
564                    })
565                    .collect::<Vec<_>>(),
566            )
567        }));
568        let mut client =
569            wire::objects_service_client::ObjectsServiceClient::new(self.channel.clone());
570        Ok(client
571            .put_object(authenticated(&self.authorization, frames))
572            .await?
573            .into_inner())
574    }
575
576    /// Open a streaming object read.
577    ///
578    /// # Errors
579    ///
580    /// Returns an error when the service rejects the read or violates framing.
581    pub async fn get(
582        &self,
583        object_key: impl Into<String>,
584        options: GetOptions,
585    ) -> Result<StoredObject> {
586        get_from(
587            &self.channel,
588            &self.authorization,
589            bucket_target(&self.reference),
590            object_key.into(),
591            options,
592        )
593        .await
594    }
595
596    /// Read immutable object metadata without transferring its body.
597    ///
598    /// # Errors
599    ///
600    /// Returns an error when the service rejects or cannot complete the request.
601    pub async fn head(
602        &self,
603        object_key: impl Into<String>,
604        options: GetOptions,
605    ) -> Result<wire::ObjectVersion> {
606        head_from(
607            &self.channel,
608            &self.authorization,
609            bucket_target(&self.reference),
610            object_key.into(),
611            options,
612        )
613        .await
614    }
615
616    /// Publish a delete marker or delete one exact immutable version.
617    ///
618    /// # Errors
619    ///
620    /// Returns an error when the service rejects or cannot complete the request.
621    pub async fn delete(
622        &self,
623        object_key: impl Into<String>,
624        options: DeleteOptions,
625    ) -> Result<wire::DeleteObjectResponse> {
626        let mut client =
627            wire::objects_service_client::ObjectsServiceClient::new(self.channel.clone());
628        Ok(client
629            .delete_object(authenticated(
630                &self.authorization,
631                wire::DeleteObjectRequest {
632                    bucket: Some(self.reference.clone()),
633                    object_key: object_key.into(),
634                    version_id: options.version_id,
635                    preconditions: options.condition.map(Condition::wire),
636                    mutation: options.mutation.wire(),
637                },
638            ))
639            .await?
640            .into_inner())
641    }
642
643    /// Read one page from a fixed 24-hour listing view.
644    ///
645    /// # Errors
646    ///
647    /// Returns an error when the service rejects or cannot complete the request.
648    pub async fn list_page(&self, options: ListOptions) -> Result<ListPage> {
649        list_from(
650            &self.channel,
651            &self.authorization,
652            bucket_target(&self.reference),
653            options,
654        )
655        .await
656    }
657
658    /// Capture an immutable whole-bucket snapshot.
659    ///
660    /// # Errors
661    ///
662    /// Returns an error when the service rejects or cannot complete the request.
663    pub async fn snapshot(&self, options: MutationOptions) -> Result<Snapshot> {
664        let mut client =
665            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
666        let response = client
667            .create_snapshot(authenticated(
668                &self.authorization,
669                wire::CreateSnapshotRequest {
670                    bucket: Some(self.reference.clone()),
671                    mutation: options.wire(),
672                },
673            ))
674            .await?
675            .into_inner();
676        let reference = response
677            .snapshot
678            .ok_or(Error::Protocol("missing snapshot identity"))?;
679        Ok(Snapshot {
680            channel: self.channel.clone(),
681            authorization: self.authorization.clone(),
682            reference,
683        })
684    }
685
686    /// Atomically fork the current complete bucket state.
687    ///
688    /// # Errors
689    ///
690    /// Returns an error when the service rejects or cannot complete the request.
691    pub async fn fork_current(
692        &self,
693        destination_name: impl Into<String>,
694        options: MutationOptions,
695    ) -> Result<Self> {
696        let mut client =
697            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
698        let response = client
699            .fork_bucket(authenticated(
700                &self.authorization,
701                wire::ForkBucketRequest {
702                    source: Some(self.reference.clone()),
703                    destination_name: destination_name.into(),
704                    mutation: options.wire(),
705                },
706            ))
707            .await?
708            .into_inner();
709        let reference = response
710            .bucket
711            .ok_or(Error::Protocol("missing fork bucket identity"))?;
712        Ok(Self {
713            channel: self.channel.clone(),
714            authorization: self.authorization.clone(),
715            reference,
716        })
717    }
718
719    /// Begin a multipart upload.
720    ///
721    /// # Errors
722    ///
723    /// Returns an error when the service rejects or cannot complete the request.
724    pub async fn create_multipart(
725        &self,
726        object_key: impl Into<String>,
727        options: PutOptions,
728    ) -> Result<MultipartUpload> {
729        let object_key = object_key.into();
730        let mut client =
731            wire::multipart_service_client::MultipartServiceClient::new(self.channel.clone());
732        let response = client
733            .create_multipart(authenticated(
734                &self.authorization,
735                wire::CreateMultipartRequest {
736                    bucket: Some(self.reference.clone()),
737                    object_key: object_key.clone(),
738                    metadata: Some(options.metadata),
739                    preconditions: options.condition.map(Condition::wire),
740                    mutation: options.mutation.wire(),
741                },
742            ))
743            .await?
744            .into_inner();
745        Ok(MultipartUpload {
746            bucket: self.clone(),
747            object_key,
748            upload_id: response.upload_id,
749        })
750    }
751}
752
753/// Immutable snapshot read handle.
754#[derive(Clone)]
755pub struct Snapshot {
756    channel: Channel,
757    authorization: MetadataValue<Ascii>,
758    reference: wire::SnapshotRef,
759}
760
761impl Snapshot {
762    fn new(client: &Client, reference: wire::SnapshotRef) -> Self {
763        Self {
764            channel: client.channel.clone(),
765            authorization: client.authorization.clone(),
766            reference,
767        }
768    }
769
770    /// Opaque snapshot identity.
771    #[must_use]
772    pub fn id(&self) -> &str {
773        &self.reference.snapshot_id
774    }
775
776    /// Copy the complete opaque snapshot identity for durable application state.
777    #[must_use]
778    pub fn reference(&self) -> wire::SnapshotRef {
779        self.reference.clone()
780    }
781
782    /// Read immutable object metadata without transferring its body.
783    ///
784    /// # Errors
785    ///
786    /// Returns an error when the service rejects or cannot complete the request.
787    pub async fn head(
788        &self,
789        object_key: impl Into<String>,
790        options: GetOptions,
791    ) -> Result<wire::ObjectVersion> {
792        head_from(
793            &self.channel,
794            &self.authorization,
795            snapshot_target(&self.reference),
796            object_key.into(),
797            options,
798        )
799        .await
800    }
801
802    /// Open a streaming immutable object read.
803    ///
804    /// # Errors
805    ///
806    /// Returns an error when the service rejects the read or violates framing.
807    pub async fn get(
808        &self,
809        object_key: impl Into<String>,
810        options: GetOptions,
811    ) -> Result<StoredObject> {
812        get_from(
813            &self.channel,
814            &self.authorization,
815            snapshot_target(&self.reference),
816            object_key.into(),
817            options,
818        )
819        .await
820    }
821
822    /// Read one page from this immutable snapshot.
823    ///
824    /// # Errors
825    ///
826    /// Returns an error when the service rejects or cannot complete the request.
827    pub async fn list_page(&self, options: ListOptions) -> Result<ListPage> {
828        list_from(
829            &self.channel,
830            &self.authorization,
831            snapshot_target(&self.reference),
832            options,
833        )
834        .await
835    }
836
837    /// Atomically publish an independent bucket from this snapshot.
838    ///
839    /// # Errors
840    ///
841    /// Returns an error when the service rejects or cannot complete the request.
842    pub async fn fork(
843        &self,
844        destination_name: impl Into<String>,
845        options: MutationOptions,
846    ) -> Result<Bucket> {
847        let mut client =
848            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
849        let response = client
850            .fork_snapshot(authenticated(
851                &self.authorization,
852                wire::ForkSnapshotRequest {
853                    snapshot: Some(self.reference.clone()),
854                    destination_name: destination_name.into(),
855                    mutation: options.wire(),
856                },
857            ))
858            .await?
859            .into_inner();
860        let reference = response
861            .bucket
862            .ok_or(Error::Protocol("missing fork bucket identity"))?;
863        Ok(Bucket {
864            channel: self.channel.clone(),
865            authorization: self.authorization.clone(),
866            reference,
867        })
868    }
869
870    /// Destroy this snapshot identity.
871    ///
872    /// # Errors
873    ///
874    /// Returns an error when the service rejects or cannot complete the request.
875    pub async fn destroy(&self, options: MutationOptions) -> Result<bool> {
876        let mut client =
877            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
878        Ok(client
879            .destroy_snapshot(authenticated(
880                &self.authorization,
881                wire::DestroySnapshotRequest {
882                    snapshot: Some(self.reference.clone()),
883                    mutation: options.wire(),
884                },
885            ))
886            .await?
887            .into_inner()
888            .existed)
889    }
890}
891
892/// Streaming object response after its immutable descriptor has been validated.
893pub struct StoredObject {
894    /// Immutable version descriptor.
895    pub version: wire::ObjectVersion,
896    stream: tonic::Streaming<wire::GetObjectResponse>,
897    remaining: u64,
898}
899
900impl StoredObject {
901    /// Read the next body chunk.
902    ///
903    /// # Errors
904    ///
905    /// Returns an error for transport failure or invalid response framing.
906    pub async fn next_chunk(&mut self) -> Result<Option<Bytes>> {
907        let Some(response) = self.stream.message().await? else {
908            return if self.remaining == 0 {
909                Ok(None)
910            } else {
911                Err(Error::Protocol("object body ended before declared size"))
912            };
913        };
914        match response.frame {
915            Some(wire::get_object_response::Frame::Body(body)) => {
916                let size = u64::try_from(body.len())
917                    .map_err(|_| Error::Protocol("object body frame is too large"))?;
918                self.remaining = self
919                    .remaining
920                    .checked_sub(size)
921                    .ok_or(Error::Protocol("object body exceeded declared size"))?;
922                Ok(Some(Bytes::from(body)))
923            }
924            Some(wire::get_object_response::Frame::Version(_)) => {
925                Err(Error::Protocol("duplicate object version frame"))
926            }
927            None => Err(Error::Protocol("empty object response frame")),
928        }
929    }
930}
931
932/// Typed multipart upload handle.
933#[derive(Clone)]
934pub struct MultipartUpload {
935    bucket: Bucket,
936    object_key: String,
937    upload_id: String,
938}
939
940impl MultipartUpload {
941    /// Opaque upload identity.
942    #[must_use]
943    pub fn id(&self) -> &str {
944        &self.upload_id
945    }
946
947    /// Upload or replace one buffered part.
948    ///
949    /// # Errors
950    ///
951    /// Returns an error when the service rejects or cannot complete the request.
952    pub async fn upload_part(
953        &self,
954        part_number: u32,
955        body: Bytes,
956        options: MutationOptions,
957    ) -> Result<wire::UploadedPart> {
958        let header = wire::UploadPartRequest {
959            frame: Some(wire::upload_part_request::Frame::Header(
960                wire::UploadPartHeader {
961                    bucket: Some(self.bucket.reference.clone()),
962                    object_key: self.object_key.clone(),
963                    upload_id: self.upload_id.clone(),
964                    part_number,
965                    mutation: options.wire(),
966                },
967            )),
968        };
969        let content = body
970            .chunks(BODY_FRAME_BYTES)
971            .map(|frame| wire::UploadPartRequest {
972                frame: Some(wire::upload_part_request::Frame::Body(frame.to_vec())),
973            })
974            .collect::<Vec<_>>();
975        let mut client = wire::multipart_service_client::MultipartServiceClient::new(
976            self.bucket.channel.clone(),
977        );
978        Ok(client
979            .upload_part(authenticated(
980                &self.bucket.authorization,
981                iter([header]).chain(iter(content)),
982            ))
983            .await?
984            .into_inner())
985    }
986
987    /// List staged parts in ascending part-number order.
988    ///
989    /// # Errors
990    ///
991    /// Returns an error when the service rejects or cannot complete the request.
992    pub async fn list_parts(&self) -> Result<Vec<wire::UploadedPart>> {
993        let mut client = wire::multipart_service_client::MultipartServiceClient::new(
994            self.bucket.channel.clone(),
995        );
996        Ok(client
997            .list_parts(authenticated(
998                &self.bucket.authorization,
999                wire::ListPartsRequest {
1000                    bucket: Some(self.bucket.reference.clone()),
1001                    object_key: self.object_key.clone(),
1002                    upload_id: self.upload_id.clone(),
1003                },
1004            ))
1005            .await?
1006            .into_inner()
1007            .parts)
1008    }
1009
1010    /// Atomically publish one immutable version from exact part receipts.
1011    ///
1012    /// # Errors
1013    ///
1014    /// Returns an error when the service rejects or cannot complete the request.
1015    pub async fn complete(
1016        &self,
1017        parts: Vec<wire::UploadedPart>,
1018        options: MutationOptions,
1019    ) -> Result<wire::ObjectVersion> {
1020        let mut client = wire::multipart_service_client::MultipartServiceClient::new(
1021            self.bucket.channel.clone(),
1022        );
1023        Ok(client
1024            .complete_multipart(authenticated(
1025                &self.bucket.authorization,
1026                wire::CompleteMultipartRequest {
1027                    bucket: Some(self.bucket.reference.clone()),
1028                    object_key: self.object_key.clone(),
1029                    upload_id: self.upload_id.clone(),
1030                    parts,
1031                    mutation: options.wire(),
1032                },
1033            ))
1034            .await?
1035            .into_inner())
1036    }
1037
1038    /// Abort this exact upload.
1039    ///
1040    /// # Errors
1041    ///
1042    /// Returns an error when the service rejects or cannot complete the request.
1043    pub async fn abort(&self, options: MutationOptions) -> Result<bool> {
1044        let mut client = wire::multipart_service_client::MultipartServiceClient::new(
1045            self.bucket.channel.clone(),
1046        );
1047        Ok(client
1048            .abort_multipart(authenticated(
1049                &self.bucket.authorization,
1050                wire::AbortMultipartRequest {
1051                    bucket: Some(self.bucket.reference.clone()),
1052                    object_key: self.object_key.clone(),
1053                    upload_id: self.upload_id.clone(),
1054                    mutation: options.wire(),
1055                },
1056            ))
1057            .await?
1058            .into_inner()
1059            .existed)
1060    }
1061}
1062
1063async fn get_from(
1064    channel: &Channel,
1065    authorization: &MetadataValue<Ascii>,
1066    target: wire::ReadTarget,
1067    object_key: String,
1068    options: GetOptions,
1069) -> Result<StoredObject> {
1070    let requested_range = options.range;
1071    let (range_start, range_end_inclusive, range_requested) =
1072        requested_range.map_or((0, None, false), |(start, end)| (start, end, true));
1073    let frame_body_limit = usize::try_from(options.maximum_bytes.min(BODY_FRAME_BYTES as u64))
1074        .map_err(|_| Error::Protocol("object response bound is not representable"))?;
1075    let decoding_limit = frame_body_limit
1076        .checked_add(256)
1077        .ok_or(Error::Protocol("object response bound overflowed"))?;
1078    let mut client = wire::objects_service_client::ObjectsServiceClient::new(channel.clone())
1079        .max_decoding_message_size(decoding_limit);
1080    let mut stream = client
1081        .get_object(authenticated(
1082            authorization,
1083            wire::GetObjectRequest {
1084                target: Some(target),
1085                object_key,
1086                version_id: options.version_id,
1087                range_start,
1088                range_end_inclusive,
1089                if_match: options.if_match,
1090                if_none_match: options.if_none_match,
1091                range_requested,
1092            },
1093        ))
1094        .await?
1095        .into_inner();
1096    let first = stream
1097        .message()
1098        .await?
1099        .and_then(|response| response.frame)
1100        .ok_or(Error::Protocol("missing object version frame"))?;
1101    let wire::get_object_response::Frame::Version(version) = first else {
1102        return Err(Error::Protocol("object version frame must be first"));
1103    };
1104    if version.delete_marker {
1105        return Err(Error::Protocol("get returned a delete marker"));
1106    }
1107    let remaining = match requested_range {
1108        None => version.size,
1109        Some((start, end)) => {
1110            let end = end.unwrap_or_else(|| version.size.saturating_sub(1));
1111            if start > end || end >= version.size {
1112                return Err(Error::Protocol("response cannot satisfy requested range"));
1113            }
1114            end - start + 1
1115        }
1116    };
1117    if remaining > options.maximum_bytes {
1118        return Err(Error::Protocol("object body exceeds requested bound"));
1119    }
1120    Ok(StoredObject {
1121        version,
1122        stream,
1123        remaining,
1124    })
1125}
1126
1127async fn head_from(
1128    channel: &Channel,
1129    authorization: &MetadataValue<Ascii>,
1130    target: wire::ReadTarget,
1131    object_key: String,
1132    options: GetOptions,
1133) -> Result<wire::ObjectVersion> {
1134    let mut client = wire::objects_service_client::ObjectsServiceClient::new(channel.clone());
1135    client
1136        .head_object(authenticated(
1137            authorization,
1138            wire::HeadObjectRequest {
1139                target: Some(target),
1140                object_key,
1141                version_id: options.version_id,
1142                if_match: options.if_match,
1143                if_none_match: options.if_none_match,
1144            },
1145        ))
1146        .await?
1147        .into_inner()
1148        .version
1149        .ok_or(Error::Protocol("missing object version"))
1150}
1151
1152async fn list_from(
1153    channel: &Channel,
1154    authorization: &MetadataValue<Ascii>,
1155    target: wire::ReadTarget,
1156    options: ListOptions,
1157) -> Result<ListPage> {
1158    let mut client = wire::objects_service_client::ObjectsServiceClient::new(channel.clone());
1159    let response = client
1160        .list_objects(authenticated(
1161            authorization,
1162            wire::ListObjectsRequest {
1163                target: Some(target),
1164                prefix: options.prefix,
1165                delimiter: options.delimiter,
1166                mode: if options.versions {
1167                    wire::ListingMode::Versions as i32
1168                } else {
1169                    wire::ListingMode::Current as i32
1170                },
1171                page_size: options.page_size,
1172                continuation_token: options.continuation,
1173            },
1174        ))
1175        .await?
1176        .into_inner();
1177    Ok(ListPage {
1178        entries: response.entries,
1179        common_prefixes: response.common_prefixes,
1180        continuation: (!response.continuation_token.is_empty())
1181            .then_some(response.continuation_token),
1182    })
1183}
1184
1185fn authenticated<T>(authorization: &MetadataValue<Ascii>, message: T) -> Request<T> {
1186    let mut request = Request::new(message);
1187    request
1188        .metadata_mut()
1189        .insert("authorization", authorization.clone());
1190    request
1191}
1192
1193fn bucket_target(reference: &wire::BucketRef) -> wire::ReadTarget {
1194    wire::ReadTarget {
1195        target: Some(wire::read_target::Target::Bucket(reference.clone())),
1196    }
1197}
1198
1199fn snapshot_target(reference: &wire::SnapshotRef) -> wire::ReadTarget {
1200    wire::ReadTarget {
1201        target: Some(wire::read_target::Target::Snapshot(reference.clone())),
1202    }
1203}
1204
1205fn mutation_options(
1206    idempotency_key: Option<String>,
1207) -> std::result::Result<MutationOptions, crate::ObjectsError> {
1208    if idempotency_key
1209        .as_ref()
1210        .is_some_and(|key| key.is_empty() || key.len() > 256)
1211    {
1212        return Err(crate::ObjectsError::Invalid("invalid idempotency key"));
1213    }
1214    Ok(MutationOptions {
1215        idempotency_key: idempotency_key.unwrap_or_default(),
1216    })
1217}
1218
1219fn validate_bucket(
1220    bucket: wire::Bucket,
1221    expected: Option<&wire::BucketRef>,
1222    expected_name: Option<&str>,
1223) -> std::result::Result<wire::Bucket, crate::ObjectsError> {
1224    let reference = bucket
1225        .bucket
1226        .as_ref()
1227        .ok_or(crate::ObjectsError::Unavailable)?;
1228    if reference.bucket_id.is_empty()
1229        || reference.name.is_empty()
1230        || expected.is_some_and(|value| value != reference)
1231        || expected_name.is_some_and(|value| value != reference.name)
1232        || bucket.created_at.is_none()
1233    {
1234        return Err(crate::ObjectsError::Unavailable);
1235    }
1236    Ok(bucket)
1237}
1238
1239fn validate_snapshot(
1240    snapshot: wire::Snapshot,
1241    source: &wire::BucketRef,
1242) -> std::result::Result<wire::Snapshot, crate::ObjectsError> {
1243    let reference = snapshot
1244        .snapshot
1245        .as_ref()
1246        .ok_or(crate::ObjectsError::Unavailable)?;
1247    if reference.snapshot_id.is_empty()
1248        || reference.source_bucket_id != source.bucket_id
1249        || snapshot.created_at.is_none()
1250    {
1251        return Err(crate::ObjectsError::Unavailable);
1252    }
1253    Ok(snapshot)
1254}
1255
1256fn provider_target(target: crate::ReadTarget) -> wire::ReadTarget {
1257    match target {
1258        crate::ReadTarget::Bucket(reference) => bucket_target(&reference),
1259        crate::ReadTarget::Snapshot(reference) => snapshot_target(&reference),
1260    }
1261}
1262
1263fn provider_error(error: Error) -> crate::ObjectsError {
1264    match error {
1265        Error::Rejected { code, .. } => match code {
1266            wire::ErrorCode::InvalidArgument => {
1267                crate::ObjectsError::Invalid("service rejected the request")
1268            }
1269            wire::ErrorCode::TokenExpired => crate::ObjectsError::Unauthorized,
1270            wire::ErrorCode::NotFound => crate::ObjectsError::NotFound,
1271            wire::ErrorCode::AlreadyExists => crate::ObjectsError::AlreadyExists,
1272            wire::ErrorCode::PreconditionFailed => crate::ObjectsError::PreconditionFailed,
1273            wire::ErrorCode::IdempotencyMismatch => crate::ObjectsError::IdempotencyMismatch,
1274            wire::ErrorCode::QuotaExceeded => crate::ObjectsError::Capacity,
1275            wire::ErrorCode::Unsupported => crate::ObjectsError::Unsupported,
1276            wire::ErrorCode::Unavailable | wire::ErrorCode::Unspecified => {
1277                crate::ObjectsError::Unavailable
1278            }
1279        },
1280        Error::InvalidEndpoint(_)
1281        | Error::InsecureEndpoint
1282        | Error::InvalidCredential
1283        | Error::InvalidCaCertificate => {
1284            crate::ObjectsError::Invalid("invalid provider configuration")
1285        }
1286        Error::Transport(status)
1287            if matches!(
1288                status.code(),
1289                tonic::Code::Unauthenticated | tonic::Code::PermissionDenied
1290            ) =>
1291        {
1292            crate::ObjectsError::Unauthorized
1293        }
1294        Error::Transport(_) | Error::Protocol(_) => crate::ObjectsError::Unavailable,
1295    }
1296}
1297
1298#[async_trait::async_trait]
1299impl crate::ObjectsProvider for Client {
1300    async fn create_bucket(
1301        &self,
1302        name: String,
1303        idempotency_key: Option<String>,
1304    ) -> std::result::Result<wire::Bucket, crate::ObjectsError> {
1305        let expected_name = name.clone();
1306        let mut client =
1307            wire::buckets_service_client::BucketsServiceClient::new(self.channel.clone());
1308        let bucket = client
1309            .create_bucket(authenticated(
1310                &self.authorization,
1311                wire::CreateBucketRequest {
1312                    name,
1313                    mutation: mutation_options(idempotency_key)?.wire(),
1314                },
1315            ))
1316            .await
1317            .map(tonic::Response::into_inner)
1318            .map_err(Error::from)
1319            .map_err(provider_error)?;
1320        validate_bucket(bucket, None, Some(&expected_name))
1321    }
1322
1323    async fn head_bucket(
1324        &self,
1325        bucket: &wire::BucketRef,
1326    ) -> std::result::Result<wire::Bucket, crate::ObjectsError> {
1327        let mut client =
1328            wire::buckets_service_client::BucketsServiceClient::new(self.channel.clone());
1329        let response = client
1330            .head_bucket(authenticated(
1331                &self.authorization,
1332                wire::HeadBucketRequest {
1333                    bucket: Some(bucket.clone()),
1334                },
1335            ))
1336            .await
1337            .map(tonic::Response::into_inner)
1338            .map_err(Error::from)
1339            .map_err(provider_error)?;
1340        validate_bucket(response, Some(bucket), None)
1341    }
1342
1343    async fn delete_bucket(
1344        &self,
1345        bucket: &wire::BucketRef,
1346        idempotency_key: Option<String>,
1347    ) -> std::result::Result<bool, crate::ObjectsError> {
1348        Bucket::new(self, bucket.clone())
1349            .destroy(mutation_options(idempotency_key)?)
1350            .await
1351            .map_err(provider_error)
1352    }
1353
1354    async fn put(
1355        &self,
1356        request: crate::PutRequest,
1357    ) -> std::result::Result<wire::ObjectVersion, crate::ObjectsError> {
1358        let bucket = Bucket::new(self, request.bucket);
1359        bucket
1360            .put(
1361                request.object_key,
1362                request.body,
1363                PutOptions {
1364                    metadata: request.metadata,
1365                    mutation: mutation_options(request.idempotency_key)?,
1366                    condition: request.condition,
1367                },
1368            )
1369            .await
1370            .map_err(provider_error)
1371    }
1372
1373    async fn get(
1374        &self,
1375        request: crate::GetRequest,
1376    ) -> std::result::Result<crate::BufferedObject, crate::ObjectsError> {
1377        let mut object = get_from(
1378            &self.channel,
1379            &self.authorization,
1380            provider_target(request.target),
1381            request.object_key,
1382            GetOptions {
1383                version_id: request.version_id.unwrap_or_default(),
1384                range: request.range,
1385                if_match: request.if_match.unwrap_or_default(),
1386                if_none_match: request.if_none_match.unwrap_or_default(),
1387                maximum_bytes: request.maximum_bytes,
1388            },
1389        )
1390        .await
1391        .map_err(provider_error)?;
1392        let version = object.version.clone();
1393        if object.remaining > request.maximum_bytes {
1394            return Err(crate::ObjectsError::Capacity);
1395        }
1396        let capacity =
1397            usize::try_from(object.remaining).map_err(|_| crate::ObjectsError::Capacity)?;
1398        let mut body = Vec::with_capacity(capacity);
1399        while let Some(chunk) = object.next_chunk().await.map_err(provider_error)? {
1400            body.extend_from_slice(&chunk);
1401        }
1402        Ok(crate::BufferedObject {
1403            version,
1404            body: Bytes::from(body),
1405        })
1406    }
1407
1408    async fn delete(
1409        &self,
1410        bucket: wire::BucketRef,
1411        object_key: String,
1412        version_id: Option<String>,
1413        condition: Option<Condition>,
1414        idempotency_key: Option<String>,
1415    ) -> std::result::Result<crate::DeleteResult, crate::ObjectsError> {
1416        let response = Bucket::new(self, bucket)
1417            .delete(
1418                object_key,
1419                DeleteOptions {
1420                    version_id: version_id.unwrap_or_default(),
1421                    mutation: mutation_options(idempotency_key)?,
1422                    condition,
1423                },
1424            )
1425            .await
1426            .map_err(provider_error)?;
1427        Ok(crate::DeleteResult {
1428            existed: response.existed,
1429            marker: response.version,
1430        })
1431    }
1432
1433    async fn list(
1434        &self,
1435        target: crate::ReadTarget,
1436        prefix: String,
1437        delimiter: Option<String>,
1438        versions: bool,
1439        page_size: u32,
1440        continuation: Option<String>,
1441    ) -> std::result::Result<crate::ProviderListPage, crate::ObjectsError> {
1442        let page = list_from(
1443            &self.channel,
1444            &self.authorization,
1445            provider_target(target),
1446            ListOptions {
1447                prefix,
1448                delimiter: delimiter.unwrap_or_default(),
1449                versions,
1450                page_size,
1451                continuation: continuation.unwrap_or_default(),
1452            },
1453        )
1454        .await
1455        .map_err(provider_error)?;
1456        Ok(crate::ProviderListPage {
1457            entries: page.entries,
1458            common_prefixes: page.common_prefixes,
1459            continuation: page.continuation,
1460        })
1461    }
1462
1463    async fn snapshot(
1464        &self,
1465        bucket: wire::BucketRef,
1466        idempotency_key: Option<String>,
1467    ) -> std::result::Result<wire::Snapshot, crate::ObjectsError> {
1468        let mut client =
1469            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
1470        let snapshot = client
1471            .create_snapshot(authenticated(
1472                &self.authorization,
1473                wire::CreateSnapshotRequest {
1474                    bucket: Some(bucket.clone()),
1475                    mutation: mutation_options(idempotency_key)?.wire(),
1476                },
1477            ))
1478            .await
1479            .map(tonic::Response::into_inner)
1480            .map_err(Error::from)
1481            .map_err(provider_error)?;
1482        validate_snapshot(snapshot, &bucket)
1483    }
1484
1485    async fn destroy_snapshot(
1486        &self,
1487        snapshot: wire::SnapshotRef,
1488        idempotency_key: Option<String>,
1489    ) -> std::result::Result<bool, crate::ObjectsError> {
1490        Snapshot::new(self, snapshot)
1491            .destroy(mutation_options(idempotency_key)?)
1492            .await
1493            .map_err(provider_error)
1494    }
1495
1496    async fn fork(
1497        &self,
1498        source: crate::ReadTarget,
1499        destination_name: String,
1500        idempotency_key: Option<String>,
1501    ) -> std::result::Result<wire::Bucket, crate::ObjectsError> {
1502        let mut client =
1503            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
1504        let mutation = mutation_options(idempotency_key)?.wire();
1505        let expected_name = destination_name.clone();
1506        let response = match source {
1507            crate::ReadTarget::Bucket(source) => {
1508                client
1509                    .fork_bucket(authenticated(
1510                        &self.authorization,
1511                        wire::ForkBucketRequest {
1512                            source: Some(source),
1513                            destination_name: destination_name.clone(),
1514                            mutation,
1515                        },
1516                    ))
1517                    .await
1518            }
1519            crate::ReadTarget::Snapshot(snapshot) => {
1520                client
1521                    .fork_snapshot(authenticated(
1522                        &self.authorization,
1523                        wire::ForkSnapshotRequest {
1524                            snapshot: Some(snapshot),
1525                            destination_name,
1526                            mutation,
1527                        },
1528                    ))
1529                    .await
1530            }
1531        };
1532        let bucket = response
1533            .map(tonic::Response::into_inner)
1534            .map_err(Error::from)
1535            .map_err(provider_error)?;
1536        validate_bucket(bucket, None, Some(&expected_name))
1537    }
1538
1539    async fn create_multipart(
1540        &self,
1541        bucket: wire::BucketRef,
1542        object_key: String,
1543        metadata: wire::ObjectMetadata,
1544        condition: Option<Condition>,
1545        idempotency_key: Option<String>,
1546    ) -> std::result::Result<wire::MultipartUpload, crate::ObjectsError> {
1547        let upload = Bucket::new(self, bucket)
1548            .create_multipart(
1549                object_key,
1550                PutOptions {
1551                    metadata,
1552                    mutation: mutation_options(idempotency_key)?,
1553                    condition,
1554                },
1555            )
1556            .await
1557            .map_err(provider_error)?;
1558        Ok(wire::MultipartUpload {
1559            upload_id: upload.upload_id,
1560        })
1561    }
1562
1563    async fn upload_part(
1564        &self,
1565        bucket: wire::BucketRef,
1566        object_key: String,
1567        upload_id: String,
1568        part_number: u32,
1569        body: Bytes,
1570        idempotency_key: Option<String>,
1571    ) -> std::result::Result<wire::UploadedPart, crate::ObjectsError> {
1572        MultipartUpload {
1573            bucket: Bucket::new(self, bucket),
1574            object_key,
1575            upload_id,
1576        }
1577        .upload_part(part_number, body, mutation_options(idempotency_key)?)
1578        .await
1579        .map_err(provider_error)
1580    }
1581
1582    async fn list_parts(
1583        &self,
1584        bucket: wire::BucketRef,
1585        object_key: String,
1586        upload_id: String,
1587    ) -> std::result::Result<Vec<wire::UploadedPart>, crate::ObjectsError> {
1588        MultipartUpload {
1589            bucket: Bucket::new(self, bucket),
1590            object_key,
1591            upload_id,
1592        }
1593        .list_parts()
1594        .await
1595        .map_err(provider_error)
1596    }
1597
1598    async fn complete_multipart(
1599        &self,
1600        bucket: wire::BucketRef,
1601        object_key: String,
1602        upload_id: String,
1603        parts: Vec<wire::UploadedPart>,
1604        idempotency_key: Option<String>,
1605    ) -> std::result::Result<wire::ObjectVersion, crate::ObjectsError> {
1606        MultipartUpload {
1607            bucket: Bucket::new(self, bucket),
1608            object_key,
1609            upload_id,
1610        }
1611        .complete(parts, mutation_options(idempotency_key)?)
1612        .await
1613        .map_err(provider_error)
1614    }
1615
1616    async fn abort_multipart(
1617        &self,
1618        bucket: wire::BucketRef,
1619        object_key: String,
1620        upload_id: String,
1621        idempotency_key: Option<String>,
1622    ) -> std::result::Result<bool, crate::ObjectsError> {
1623        MultipartUpload {
1624            bucket: Bucket::new(self, bucket),
1625            object_key,
1626            upload_id,
1627        }
1628        .abort(mutation_options(idempotency_key)?)
1629        .await
1630        .map_err(provider_error)
1631    }
1632}
1633
1634#[cfg(test)]
1635mod tests {
1636    use super::*;
1637    use rcgen::generate_simple_self_signed;
1638    use tokio::net::TcpListener;
1639    use tokio_stream::wrappers::TcpListenerStream;
1640    use tonic::{
1641        Response, Status,
1642        transport::{Identity, Server, ServerTlsConfig},
1643    };
1644    use wire::buckets_service_server::{BucketsService, BucketsServiceServer};
1645    use wire::objects_service_server::{ObjectsService, ObjectsServiceServer};
1646
1647    struct AuthenticatedBuckets;
1648
1649    #[derive(Clone)]
1650    struct AuthenticatedObjects {
1651        requests: std::sync::Arc<std::sync::Mutex<Vec<wire::GetObjectRequest>>>,
1652    }
1653
1654    #[tonic::async_trait]
1655    impl ObjectsService for AuthenticatedObjects {
1656        async fn put_object(
1657            &self,
1658            _request: Request<tonic::Streaming<wire::PutObjectRequest>>,
1659        ) -> std::result::Result<Response<wire::ObjectVersion>, Status> {
1660            Err(Status::unimplemented("fixture"))
1661        }
1662
1663        type GetObjectStream = std::pin::Pin<
1664            Box<
1665                dyn tokio_stream::Stream<
1666                        Item = std::result::Result<wire::GetObjectResponse, Status>,
1667                    > + Send,
1668            >,
1669        >;
1670
1671        async fn get_object(
1672            &self,
1673            request: Request<wire::GetObjectRequest>,
1674        ) -> std::result::Result<Response<Self::GetObjectStream>, Status> {
1675            if request
1676                .metadata()
1677                .get("authorization")
1678                .and_then(|value| value.to_str().ok())
1679                != Some("Bearer exact-token")
1680            {
1681                return Err(Status::unauthenticated("missing exact bearer credential"));
1682            }
1683            let request = request.into_inner();
1684            if !request.range_requested
1685                && (request.range_start != 0 || request.range_end_inclusive.is_some())
1686            {
1687                return Err(Status::invalid_argument(
1688                    "range fields require range_requested",
1689                ));
1690            }
1691            let range_requested = request.range_requested;
1692            let body = match (
1693                range_requested,
1694                request.range_start,
1695                request.range_end_inclusive,
1696            ) {
1697                (true, 0, Some(1)) => b"ab".to_vec(),
1698                (true, 1, Some(1)) => b"b".to_vec(),
1699                _ => b"abc".to_vec(),
1700            };
1701            self.requests
1702                .lock()
1703                .map_err(|_| Status::internal("fixture request lock poisoned"))?
1704                .push(request);
1705            let frames = vec![
1706                Ok(wire::GetObjectResponse {
1707                    frame: Some(wire::get_object_response::Frame::Version(
1708                        wire::ObjectVersion {
1709                            version_id: "version-1".into(),
1710                            etag: "etag-1".into(),
1711                            size: 3,
1712                            metadata: Some(wire::ObjectMetadata::default()),
1713                            created_at: Some(prost_types::Timestamp::default()),
1714                            ..wire::ObjectVersion::default()
1715                        },
1716                    )),
1717                }),
1718                Ok(wire::GetObjectResponse {
1719                    frame: Some(wire::get_object_response::Frame::Body(body)),
1720                }),
1721            ];
1722            Ok(Response::new(Box::pin(tokio_stream::iter(frames))))
1723        }
1724
1725        async fn head_object(
1726            &self,
1727            _request: Request<wire::HeadObjectRequest>,
1728        ) -> std::result::Result<Response<wire::HeadObjectResponse>, Status> {
1729            Err(Status::unimplemented("fixture"))
1730        }
1731
1732        async fn delete_object(
1733            &self,
1734            _request: Request<wire::DeleteObjectRequest>,
1735        ) -> std::result::Result<Response<wire::DeleteObjectResponse>, Status> {
1736            Err(Status::unimplemented("fixture"))
1737        }
1738
1739        async fn list_objects(
1740            &self,
1741            _request: Request<wire::ListObjectsRequest>,
1742        ) -> std::result::Result<Response<wire::ListObjectsResponse>, Status> {
1743            Err(Status::unimplemented("fixture"))
1744        }
1745    }
1746
1747    #[tonic::async_trait]
1748    impl BucketsService for AuthenticatedBuckets {
1749        async fn create_bucket(
1750            &self,
1751            request: Request<wire::CreateBucketRequest>,
1752        ) -> std::result::Result<Response<wire::Bucket>, Status> {
1753            if request
1754                .metadata()
1755                .get("authorization")
1756                .and_then(|value| value.to_str().ok())
1757                != Some("Bearer exact-token")
1758            {
1759                return Err(Status::unauthenticated("missing exact bearer credential"));
1760            }
1761            let name = request.into_inner().name;
1762            Ok(Response::new(wire::Bucket {
1763                bucket: Some(wire::BucketRef {
1764                    bucket_id: "bucket-1".into(),
1765                    name,
1766                }),
1767                created_at: Some(prost_types::Timestamp::default()),
1768            }))
1769        }
1770
1771        async fn head_bucket(
1772            &self,
1773            _request: Request<wire::HeadBucketRequest>,
1774        ) -> std::result::Result<Response<wire::Bucket>, Status> {
1775            Err(Status::unimplemented("fixture"))
1776        }
1777
1778        async fn delete_bucket(
1779            &self,
1780            _request: Request<wire::DeleteBucketRequest>,
1781        ) -> std::result::Result<Response<wire::DeleteBucketResponse>, Status> {
1782            Err(Status::unimplemented("fixture"))
1783        }
1784    }
1785
1786    fn assert_provider<T: crate::ObjectsProvider>() {}
1787
1788    async fn assert_implicit_range_is_rejected(
1789        client: &Client,
1790        bucket: &Bucket,
1791    ) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
1792        let mut wire_client =
1793            wire::objects_service_client::ObjectsServiceClient::new(client.channel.clone());
1794        let error = wire_client
1795            .get_object(authenticated(
1796                &client.authorization,
1797                wire::GetObjectRequest {
1798                    target: Some(bucket_target(&bucket.reference())),
1799                    object_key: "object".into(),
1800                    range_start: 1,
1801                    range_end_inclusive: Some(1),
1802                    ..wire::GetObjectRequest::default()
1803                },
1804            ))
1805            .await
1806            .err()
1807            .ok_or("implicit range unexpectedly succeeded")?;
1808        assert_eq!(error.code(), tonic::Code::InvalidArgument);
1809        Ok(())
1810    }
1811
1812    #[test]
1813    fn remote_client_implements_the_public_provider_boundary() {
1814        assert_provider::<Client>();
1815    }
1816
1817    #[test]
1818    fn provider_idempotency_keys_match_memory_validation() {
1819        assert!(matches!(
1820            mutation_options(Some(String::new())),
1821            Err(crate::ObjectsError::Invalid(_))
1822        ));
1823        assert!(matches!(
1824            mutation_options(Some("x".repeat(257))),
1825            Err(crate::ObjectsError::Invalid(_))
1826        ));
1827        assert!(mutation_options(None).is_ok());
1828        assert!(mutation_options(Some("key".into())).is_ok());
1829    }
1830
1831    #[test]
1832    fn provider_rejects_malformed_and_substituted_identities() {
1833        let expected = wire::BucketRef {
1834            bucket_id: "bucket-1".into(),
1835            name: "expected".into(),
1836        };
1837        assert!(matches!(
1838            validate_bucket(wire::Bucket::default(), Some(&expected), None),
1839            Err(crate::ObjectsError::Unavailable)
1840        ));
1841        let substituted = wire::Bucket {
1842            bucket: Some(wire::BucketRef {
1843                bucket_id: "bucket-2".into(),
1844                name: "expected".into(),
1845            }),
1846            created_at: Some(prost_types::Timestamp::default()),
1847        };
1848        assert!(matches!(
1849            validate_bucket(substituted, Some(&expected), None),
1850            Err(crate::ObjectsError::Unavailable)
1851        ));
1852    }
1853
1854    #[tokio::test]
1855    async fn plaintext_customer_endpoint_fails_before_connecting() {
1856        assert!(matches!(
1857            Client::connect("http://127.0.0.1:1", "token").await,
1858            Err(Error::InsecureEndpoint)
1859        ));
1860        assert!(matches!(
1861            Client::connect_with_ca_certificate("http://127.0.0.1:1", "token", b"certificate")
1862                .await,
1863            Err(Error::InsecureEndpoint)
1864        ));
1865    }
1866
1867    #[tokio::test]
1868    async fn private_ca_connection_rejects_unrepresentable_bearer_before_network_io() {
1869        assert!(matches!(
1870            Client::connect_with_ca_certificate(
1871                "https://127.0.0.1:1",
1872                "invalid\nbearer",
1873                b"certificate",
1874            )
1875            .await,
1876            Err(Error::InvalidCredential)
1877        ));
1878    }
1879
1880    #[test]
1881    fn private_ca_bundle_is_exactly_bounded() {
1882        assert!(matches!(
1883            bounded_ca_certificate(&[]),
1884            Err(Error::InvalidCaCertificate)
1885        ));
1886        assert!(matches!(
1887            bounded_ca_certificate(&vec![b'x'; MAX_CA_CERTIFICATE_BYTES]),
1888            Ok(certificate) if certificate.len() == MAX_CA_CERTIFICATE_BYTES
1889        ));
1890        assert!(matches!(
1891            bounded_ca_certificate(&vec![b'x'; MAX_CA_CERTIFICATE_BYTES + 1]),
1892            Err(Error::InvalidCaCertificate)
1893        ));
1894    }
1895
1896    #[tokio::test]
1897    async fn private_ca_https_connection_preserves_exact_bearer_authentication()
1898    -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
1899        let certified = generate_simple_self_signed(["localhost".to_owned()])?;
1900        let certificate_pem = certified.cert.pem();
1901        let private_key_pem = certified.signing_key.serialize_pem();
1902        let listener = TcpListener::bind("127.0.0.1:0").await?;
1903        let address = listener.local_addr()?;
1904        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
1905        let object_requests = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1906        let object_service = AuthenticatedObjects {
1907            requests: object_requests.clone(),
1908        };
1909        let server_certificate_pem = certificate_pem.clone();
1910        let server = tokio::spawn(async move {
1911            Server::builder()
1912                .tls_config(
1913                    ServerTlsConfig::new()
1914                        .identity(Identity::from_pem(server_certificate_pem, private_key_pem)),
1915                )?
1916                .add_service(BucketsServiceServer::new(AuthenticatedBuckets))
1917                .add_service(ObjectsServiceServer::new(object_service))
1918                .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async {
1919                    let _ = shutdown_rx.await;
1920                })
1921                .await
1922        });
1923
1924        let endpoint = format!("https://localhost:{}", address.port());
1925        assert!(Client::connect(&endpoint, "exact-token").await.is_err());
1926        let client =
1927            Client::connect_with_ca_certificate(endpoint, "exact-token", certificate_pem).await?;
1928        let bucket = client
1929            .create_bucket("fixture", MutationOptions::default())
1930            .await?;
1931        assert_eq!(bucket.id(), "bucket-1");
1932        assert_eq!(bucket.name(), "fixture");
1933
1934        let mut absent = bucket
1935            .get("object", GetOptions::default().maximum_bytes(3))
1936            .await?;
1937        assert_eq!(absent.next_chunk().await?, Some(Bytes::from_static(b"abc")));
1938        assert_eq!(absent.next_chunk().await?, None);
1939        let mut open_from_zero = bucket
1940            .get(
1941                "object",
1942                GetOptions::default().range(0, None).maximum_bytes(3),
1943            )
1944            .await?;
1945        assert_eq!(
1946            open_from_zero.next_chunk().await?,
1947            Some(Bytes::from_static(b"abc"))
1948        );
1949        assert_eq!(open_from_zero.next_chunk().await?, None);
1950        let mut bounded = bucket
1951            .get(
1952                "object",
1953                GetOptions::default().range(1, Some(1)).maximum_bytes(1),
1954            )
1955            .await?;
1956        assert_eq!(bounded.next_chunk().await?, Some(Bytes::from_static(b"b")));
1957        assert_eq!(bounded.next_chunk().await?, None);
1958        assert!(matches!(
1959            bucket
1960                .get("object", GetOptions::default().maximum_bytes(2))
1961                .await,
1962            Err(Error::Protocol("object body exceeds requested bound"))
1963        ));
1964        {
1965            let requests = object_requests
1966                .lock()
1967                .map_err(|_| "fixture request lock poisoned")?;
1968            let mut requests = requests.iter();
1969            let unbounded = requests.next().ok_or("missing unbounded request")?;
1970            assert!(!unbounded.range_requested);
1971            assert_eq!(unbounded.range_start, 0);
1972            assert_eq!(unbounded.range_end_inclusive, None);
1973            let open_from_zero = requests.next().ok_or("missing zero range request")?;
1974            assert!(open_from_zero.range_requested);
1975            assert_eq!(open_from_zero.range_start, 0);
1976            assert_eq!(open_from_zero.range_end_inclusive, None);
1977            let bounded = requests.next().ok_or("missing bounded range request")?;
1978            assert!(bounded.range_requested);
1979            assert_eq!(bounded.range_start, 1);
1980            assert_eq!(bounded.range_end_inclusive, Some(1));
1981        }
1982
1983        assert_implicit_range_is_rejected(&client, &bucket).await?;
1984
1985        let _ = shutdown_tx.send(());
1986        server.await??;
1987        Ok(())
1988    }
1989
1990    #[test]
1991    fn default_mutation_does_not_emit_an_empty_identity() {
1992        assert!(MutationOptions::default().wire().is_none());
1993    }
1994
1995    #[test]
1996    fn list_options_support_the_documented_fluent_contract() {
1997        let options = ListOptions::default()
1998            .prefix("logs/")
1999            .delimiter("/")
2000            .versions(true)
2001            .page_size(17)
2002            .continuation("next");
2003        assert_eq!(options.prefix, "logs/");
2004        assert_eq!(options.delimiter, "/");
2005        assert!(options.versions);
2006        assert_eq!(options.page_size, 17);
2007        assert_eq!(options.continuation, "next");
2008
2009        let page = ListPage {
2010            entries: vec![wire::ListEntry::default()],
2011            common_prefixes: Vec::new(),
2012            continuation: None,
2013        };
2014        assert_eq!(page.entries().len(), 1);
2015    }
2016
2017    #[test]
2018    fn wire_preserves_an_explicit_open_ended_range_from_zero() {
2019        let absent = wire::GetObjectRequest::default().encode_to_vec();
2020        let explicit = wire::GetObjectRequest {
2021            range_start: 0,
2022            range_requested: true,
2023            ..wire::GetObjectRequest::default()
2024        }
2025        .encode_to_vec();
2026        assert_ne!(absent, explicit);
2027    }
2028
2029    #[test]
2030    fn canonical_error_details_remain_typed() {
2031        let detail = wire::ErrorDetail {
2032            code: wire::ErrorCode::IdempotencyMismatch as i32,
2033            request_id: "request-7".into(),
2034        }
2035        .encode_to_vec();
2036        let error = Error::from(tonic::Status::with_details(
2037            tonic::Code::AlreadyExists,
2038            "idempotency mismatch",
2039            detail.into(),
2040        ));
2041        assert!(matches!(
2042            error,
2043            Error::Rejected {
2044                code: wire::ErrorCode::IdempotencyMismatch,
2045                request_id,
2046                ..
2047            } if request_id == "request-7"
2048        ));
2049    }
2050}