1use 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;
15pub const MAX_CA_CERTIFICATE_BYTES: usize = 64 * 1024;
17
18#[derive(Debug, Error)]
20pub enum Error {
21 #[error("invalid Objects endpoint: {0}")]
23 InvalidEndpoint(#[from] tonic::transport::Error),
24 #[error("Objects customer endpoints must use https")]
26 InsecureEndpoint,
27 #[error("invalid Objects bearer credential")]
29 InvalidCredential,
30 #[error("invalid Objects CA certificate bundle")]
32 InvalidCaCertificate,
33 #[error("Objects request rejected ({code:?}, request {request_id}): {message}")]
35 Rejected {
36 code: wire::ErrorCode,
38 request_id: String,
40 message: String,
42 },
43 #[error("Objects transport failed: {0}")]
45 Transport(tonic::Status),
46 #[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
67pub type Result<T> = std::result::Result<T, Error>;
69
70#[derive(Clone)]
72pub struct Client {
73 channel: Channel,
74 authorization: MetadataValue<Ascii>,
75}
76
77impl Client {
78 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 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 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 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 #[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#[derive(Clone, Debug, Default)]
198pub struct MutationOptions {
199 idempotency_key: String,
200}
201
202impl MutationOptions {
203 #[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#[derive(Clone, Debug, Default)]
219pub struct PutOptions {
220 metadata: wire::ObjectMetadata,
221 mutation: MutationOptions,
222 condition: Option<Condition>,
223}
224
225impl PutOptions {
226 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
284 pub fn condition(mut self, value: Condition) -> Self {
285 self.condition = Some(value);
286 self
287 }
288}
289
290#[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 #[must_use]
315 pub fn version(mut self, value: impl Into<String>) -> Self {
316 self.version_id = value.into();
317 self
318 }
319
320 #[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 #[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 #[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 #[must_use]
343 pub fn maximum_bytes(mut self, value: u64) -> Self {
344 self.maximum_bytes = value;
345 self
346 }
347}
348
349#[derive(Clone, Debug, Default)]
351pub struct DeleteOptions {
352 version_id: String,
353 mutation: MutationOptions,
354 condition: Option<Condition>,
355}
356
357impl DeleteOptions {
358 #[must_use]
360 pub fn version(mut self, value: impl Into<String>) -> Self {
361 self.version_id = value.into();
362 self
363 }
364
365 #[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 #[must_use]
374 pub fn condition(mut self, value: Condition) -> Self {
375 self.condition = Some(value);
376 self
377 }
378}
379
380#[derive(Clone, Debug)]
382pub struct ListOptions {
383 pub prefix: String,
385 pub delimiter: String,
387 pub versions: bool,
389 pub page_size: u32,
391 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 #[must_use]
410 pub fn prefix(mut self, value: impl Into<String>) -> Self {
411 self.prefix = value.into();
412 self
413 }
414
415 #[must_use]
417 pub fn delimiter(mut self, value: impl Into<String>) -> Self {
418 self.delimiter = value.into();
419 self
420 }
421
422 #[must_use]
424 pub fn versions(mut self, value: bool) -> Self {
425 self.versions = value;
426 self
427 }
428
429 #[must_use]
431 pub fn page_size(mut self, value: u32) -> Self {
432 self.page_size = value;
433 self
434 }
435
436 #[must_use]
438 pub fn continuation(mut self, value: impl Into<String>) -> Self {
439 self.continuation = value.into();
440 self
441 }
442}
443
444#[derive(Clone, Debug)]
446pub struct ListPage {
447 pub entries: Vec<wire::ListEntry>,
449 pub common_prefixes: Vec<String>,
451 pub continuation: Option<String>,
453}
454
455impl ListPage {
456 #[must_use]
458 pub fn entries(&self) -> &[wire::ListEntry] {
459 &self.entries
460 }
461}
462
463#[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 #[must_use]
482 pub fn id(&self) -> &str {
483 &self.reference.bucket_id
484 }
485
486 #[must_use]
488 pub fn name(&self) -> &str {
489 &self.reference.name
490 }
491
492 #[must_use]
494 pub fn reference(&self) -> wire::BucketRef {
495 self.reference.clone()
496 }
497
498 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 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 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 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 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 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 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 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 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 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#[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 #[must_use]
772 pub fn id(&self) -> &str {
773 &self.reference.snapshot_id
774 }
775
776 #[must_use]
778 pub fn reference(&self) -> wire::SnapshotRef {
779 self.reference.clone()
780 }
781
782 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 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 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 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 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
892pub struct StoredObject {
894 pub version: wire::ObjectVersion,
896 stream: tonic::Streaming<wire::GetObjectResponse>,
897 remaining: u64,
898}
899
900impl StoredObject {
901 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#[derive(Clone)]
934pub struct MultipartUpload {
935 bucket: Bucket,
936 object_key: String,
937 upload_id: String,
938}
939
940impl MultipartUpload {
941 #[must_use]
943 pub fn id(&self) -> &str {
944 &self.upload_id
945 }
946
947 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 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 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 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}