1use crate::BytesRange;
23use crate::UserMetadata;
24use crate::options;
25use crate::raw::*;
26use crate::types::compact::CompactValues;
27use crate::types::metadata::user_metadata_encoded_len;
28use crate::types::metadata::write_user_metadata;
29use crate::{Capability, Error, ErrorKind, Result};
30
31use std::collections::HashMap;
32
33#[inline]
34fn string_value(values: &CompactValues, field: usize) -> Option<&str> {
35 values.get_str(field)
36}
37
38#[inline]
39fn encode_timestamp(value: Timestamp) -> [u8; 12] {
40 let value = value.into_inner();
41 let mut encoded = [0; 12];
42 encoded[..8].copy_from_slice(&value.as_second().to_le_bytes());
43 encoded[8..].copy_from_slice(&value.subsec_nanosecond().to_le_bytes());
44 encoded
45}
46
47#[inline]
48fn decode_timestamp(value: &[u8]) -> Timestamp {
49 let seconds = i64::from_le_bytes(value[..8].try_into().unwrap());
50 let nanoseconds = i32::from_le_bytes(value[8..].try_into().unwrap());
51 Timestamp::new(seconds, nanoseconds).expect("operation stores a previously validated timestamp")
52}
53
54fn sorted_user_metadata(value: HashMap<String, String>) -> Vec<(String, String)> {
55 let mut value: Vec<_> = value.into_iter().collect();
56 value.sort_unstable_by(|left, right| left.0.cmp(&right.0));
57 value
58}
59
60#[derive(Debug, Clone, Default)]
64pub struct OpCreateDir {}
65
66impl OpCreateDir {
67 pub fn new() -> Self {
69 Self::default()
70 }
71}
72
73#[derive(Debug, Clone, Default, Eq, Hash, PartialEq)]
77pub struct OpDelete {
78 flags: u8,
79 values: CompactValues,
80}
81
82const OP_DELETE_RECURSIVE: u8 = 1;
83
84#[repr(usize)]
85enum DeleteField {
86 Version,
87 IfMatch,
88 IfNoneMatch,
89 IfVersionMatch,
90 IfVersionNotMatch,
91}
92
93impl OpDelete {
94 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn from_options(
110 capability: &Capability,
111 mut options: options::DeleteOptions,
112 ) -> Result<Self> {
113 if let Some(metadata) = options.if_not_changed.take() {
114 let (target, identity, name) = if capability.delete_with_if_version_match
115 && let Some(version) = metadata.version()
116 {
117 (&mut options.if_version_match, version, "if_version_match")
118 } else if let Some(etag) = metadata.etag() {
119 (&mut options.if_match, etag, "if_match")
120 } else if let Some(version) = metadata.version() {
121 (&mut options.if_version_match, version, "if_version_match")
122 } else {
123 return Err(Error::new(
124 ErrorKind::ConfigInvalid,
125 "if_not_changed metadata contains neither version nor ETag",
126 )
127 .with_operation(Operation::Delete));
128 };
129
130 if let Some(explicit) = target {
131 if explicit != identity {
132 return Err(Error::new(
133 ErrorKind::ConditionNotMatch,
134 format!("if_not_changed conflicts with {name}"),
135 )
136 .with_operation(Operation::Delete));
137 }
138 } else {
139 *target = Some(identity.to_owned());
140 }
141 }
142
143 let fields = [
144 options.version.as_deref().map(str::as_bytes),
145 options.if_match.as_deref().map(str::as_bytes),
146 options.if_none_match.as_deref().map(str::as_bytes),
147 options.if_version_match.as_deref().map(str::as_bytes),
148 options.if_version_not_match.as_deref().map(str::as_bytes),
149 ];
150 Ok(Self {
151 flags: if options.recursive {
152 OP_DELETE_RECURSIVE
153 } else {
154 0
155 },
156 values: CompactValues::encode(&fields),
157 })
158 }
159}
160
161impl OpDelete {
162 #[inline]
164 pub fn version(&self) -> Option<&str> {
165 string_value(&self.values, DeleteField::Version as usize)
166 }
167
168 #[inline]
170 pub fn recursive(&self) -> bool {
171 self.flags & OP_DELETE_RECURSIVE != 0
172 }
173
174 #[inline]
176 pub fn if_match(&self) -> Option<&str> {
177 string_value(&self.values, DeleteField::IfMatch as usize)
178 }
179
180 #[inline]
182 pub fn if_none_match(&self) -> Option<&str> {
183 string_value(&self.values, DeleteField::IfNoneMatch as usize)
184 }
185
186 #[inline]
188 pub fn if_version_match(&self) -> Option<&str> {
189 string_value(&self.values, DeleteField::IfVersionMatch as usize)
190 }
191
192 #[inline]
194 pub fn if_version_not_match(&self) -> Option<&str> {
195 string_value(&self.values, DeleteField::IfVersionNotMatch as usize)
196 }
197
198 #[inline]
200 pub fn is_conditional(&self) -> bool {
201 self.if_match().is_some()
202 || self.if_none_match().is_some()
203 || self.if_version_match().is_some()
204 || self.if_version_not_match().is_some()
205 }
206
207 pub(crate) fn set_recursive(&mut self, recursive: bool) {
208 if recursive {
209 self.flags |= OP_DELETE_RECURSIVE;
210 } else {
211 self.flags &= !OP_DELETE_RECURSIVE;
212 }
213 }
214
215 #[doc(hidden)]
216 pub fn into_version(mut self, version: &str) -> Self {
217 self.values = self
218 .values
219 .replace(DeleteField::Version as usize, version.as_bytes());
220 self
221 }
222}
223
224#[derive(Debug, Clone, Default)]
228pub struct OpDeleter {}
229
230impl OpDeleter {
231 pub fn new() -> Self {
233 Self::default()
234 }
235}
236
237#[derive(Debug, Clone, Default)]
239pub struct OpList {
240 limit: usize,
244 flags: u8,
245 values: CompactValues,
246}
247
248const OP_LIST_HAS_LIMIT: u8 = 1;
249const OP_LIST_RECURSIVE: u8 = 1 << 1;
250const OP_LIST_VERSIONS: u8 = 1 << 2;
251const OP_LIST_DELETED: u8 = 1 << 3;
252
253impl OpList {
254 pub fn new() -> Self {
256 Self::default()
257 }
258
259 #[inline]
261 pub fn limit(&self) -> Option<usize> {
262 (self.flags & OP_LIST_HAS_LIMIT != 0).then_some(self.limit)
263 }
264
265 #[inline]
267 pub fn start_after(&self) -> Option<&str> {
268 string_value(&self.values, 0)
269 }
270
271 #[inline]
273 pub fn recursive(&self) -> bool {
274 self.flags & OP_LIST_RECURSIVE != 0
275 }
276
277 #[deprecated(since = "0.53.2", note = "concurrent in list is no-op")]
279 #[inline]
280 pub fn concurrent(&self) -> usize {
281 0
282 }
283
284 #[inline]
286 pub fn versions(&self) -> bool {
287 self.flags & OP_LIST_VERSIONS != 0
288 }
289
290 #[inline]
292 pub fn deleted(&self) -> bool {
293 self.flags & OP_LIST_DELETED != 0
294 }
295}
296
297impl From<options::ListOptions> for OpList {
298 fn from(value: options::ListOptions) -> Self {
299 let mut flags = 0;
300 if value.limit.is_some() {
301 flags |= OP_LIST_HAS_LIMIT;
302 }
303 if value.recursive {
304 flags |= OP_LIST_RECURSIVE;
305 }
306 if value.versions {
307 flags |= OP_LIST_VERSIONS;
308 }
309 if value.deleted {
310 flags |= OP_LIST_DELETED;
311 }
312 Self {
313 limit: value.limit.unwrap_or_default(),
314 flags,
315 values: CompactValues::encode(&[value.start_after.as_deref().map(str::as_bytes)]),
316 }
317 }
318}
319
320#[derive(Debug, Clone)]
324pub struct OpPresign {
325 expire: Duration,
326
327 op: PresignOperation,
328}
329
330impl OpPresign {
331 pub fn new(op: impl Into<PresignOperation>, expire: Duration) -> Self {
333 Self {
334 op: op.into(),
335 expire,
336 }
337 }
338
339 pub fn operation(&self) -> &PresignOperation {
341 &self.op
342 }
343
344 pub fn expire(&self) -> Duration {
346 self.expire
347 }
348
349 pub fn into_parts(self) -> (Duration, PresignOperation) {
351 (self.expire, self.op)
352 }
353}
354
355#[derive(Debug, Clone)]
357#[non_exhaustive]
358pub enum PresignOperation {
359 Stat(OpStat),
361 Read(BytesRange, OpRead),
363 Write(OpWrite),
365 Delete(OpDelete),
367}
368
369impl From<OpStat> for PresignOperation {
370 fn from(op: OpStat) -> Self {
371 Self::Stat(op)
372 }
373}
374
375impl From<OpRead> for PresignOperation {
376 fn from(v: OpRead) -> Self {
377 Self::Read(BytesRange::default(), v)
378 }
379}
380
381impl From<OpWrite> for PresignOperation {
382 fn from(v: OpWrite) -> Self {
383 Self::Write(v)
384 }
385}
386
387impl From<OpDelete> for PresignOperation {
388 fn from(v: OpDelete) -> Self {
389 Self::Delete(v)
390 }
391}
392
393#[derive(Debug, Clone, Default)]
395pub struct OpRead {
396 values: CompactValues,
397}
398
399#[repr(usize)]
400enum ReadField {
401 IfMatch,
402 IfNoneMatch,
403 IfVersionMatch,
404 IfVersionNotMatch,
405 IfModifiedSince,
406 IfUnmodifiedSince,
407 OverrideContentType,
408 OverrideCacheControl,
409 OverrideContentDisposition,
410 Version,
411 ContentLengthHint,
412}
413
414impl OpRead {
415 pub fn new() -> Self {
417 Self::default()
418 }
419
420 pub(crate) fn from_compose_source_options(
432 capability: &Capability,
433 mut options: options::ComposeSourceOptions,
434 ) -> Result<Self> {
435 if let Some(metadata) = options.if_not_changed.take() {
436 let (target, identity, name) = if capability.compose_with_source_version
437 && let Some(version) = metadata.version()
438 {
439 (&mut options.version, version, "source version")
440 } else if let Some(etag) = metadata.etag() {
441 (&mut options.if_match, etag, "source if_match")
442 } else if let Some(version) = metadata.version() {
443 (&mut options.version, version, "source version")
444 } else {
445 return Err(Error::new(
446 ErrorKind::ConfigInvalid,
447 "if_not_changed metadata contains neither version nor ETag",
448 )
449 .with_operation(Operation::Compose));
450 };
451
452 if let Some(explicit) = target {
453 if explicit != identity {
454 return Err(Error::new(
455 ErrorKind::ConditionNotMatch,
456 format!("if_not_changed conflicts with {name}"),
457 )
458 .with_operation(Operation::Compose));
459 }
460 } else {
461 *target = Some(identity.to_owned());
462 }
463 }
464
465 Ok(Self::from_read_options(options::ReadOptions {
466 version: options.version,
467 if_match: options.if_match,
468 ..Default::default()
469 }))
470 }
471
472 #[inline]
475 pub fn override_content_disposition(&self) -> Option<&str> {
476 string_value(&self.values, ReadField::OverrideContentDisposition as usize)
477 }
478
479 #[inline]
481 pub fn override_cache_control(&self) -> Option<&str> {
482 string_value(&self.values, ReadField::OverrideCacheControl as usize)
483 }
484
485 #[inline]
487 pub fn override_content_type(&self) -> Option<&str> {
488 string_value(&self.values, ReadField::OverrideContentType as usize)
489 }
490
491 #[inline]
493 pub fn if_match(&self) -> Option<&str> {
494 string_value(&self.values, ReadField::IfMatch as usize)
495 }
496
497 #[inline]
499 pub fn if_none_match(&self) -> Option<&str> {
500 string_value(&self.values, ReadField::IfNoneMatch as usize)
501 }
502
503 #[inline]
505 pub fn if_version_match(&self) -> Option<&str> {
506 string_value(&self.values, ReadField::IfVersionMatch as usize)
507 }
508
509 #[inline]
511 pub fn if_version_not_match(&self) -> Option<&str> {
512 string_value(&self.values, ReadField::IfVersionNotMatch as usize)
513 }
514
515 #[inline]
517 pub fn if_modified_since(&self) -> Option<Timestamp> {
518 self.values
519 .get(ReadField::IfModifiedSince as usize)
520 .map(decode_timestamp)
521 }
522
523 #[inline]
525 pub fn if_unmodified_since(&self) -> Option<Timestamp> {
526 self.values
527 .get(ReadField::IfUnmodifiedSince as usize)
528 .map(decode_timestamp)
529 }
530
531 #[inline]
533 pub fn is_conditional(&self) -> bool {
534 self.if_match().is_some()
535 || self.if_none_match().is_some()
536 || self.if_version_match().is_some()
537 || self.if_version_not_match().is_some()
538 || self.if_modified_since().is_some()
539 || self.if_unmodified_since().is_some()
540 }
541
542 #[inline]
544 pub fn version(&self) -> Option<&str> {
545 string_value(&self.values, ReadField::Version as usize)
546 }
547
548 pub(crate) fn content_length_hint(&self) -> Option<u64> {
549 self.values
550 .get(ReadField::ContentLengthHint as usize)
551 .map(|value| u64::from_le_bytes(value.try_into().unwrap()))
552 }
553
554 fn from_read_options(value: options::ReadOptions) -> Self {
555 let if_modified_since = value.if_modified_since.map(encode_timestamp);
556 let if_unmodified_since = value.if_unmodified_since.map(encode_timestamp);
557 let content_length_hint = value.content_length_hint.map(u64::to_le_bytes);
558 Self {
559 values: CompactValues::encode(&[
560 value.if_match.as_deref().map(str::as_bytes),
561 value.if_none_match.as_deref().map(str::as_bytes),
562 value.if_version_match.as_deref().map(str::as_bytes),
563 value.if_version_not_match.as_deref().map(str::as_bytes),
564 if_modified_since.as_ref().map(|value| value.as_slice()),
565 if_unmodified_since.as_ref().map(|value| value.as_slice()),
566 value.override_content_type.as_deref().map(str::as_bytes),
567 value.override_cache_control.as_deref().map(str::as_bytes),
568 value
569 .override_content_disposition
570 .as_deref()
571 .map(str::as_bytes),
572 value.version.as_deref().map(str::as_bytes),
573 content_length_hint.as_ref().map(|value| value.as_slice()),
574 ]),
575 }
576 }
577}
578
579#[derive(Debug, Clone)]
581pub struct OpReader {
582 concurrent: usize,
584 chunk: Option<usize>,
586 gap: Option<usize>,
588 prefetch: usize,
590}
591
592impl Default for OpReader {
593 fn default() -> Self {
594 Self {
595 concurrent: 1,
596 chunk: None,
597 gap: None,
598 prefetch: 0,
599 }
600 }
601}
602
603impl OpReader {
604 pub fn new() -> Self {
606 Self::default()
607 }
608
609 pub fn with_concurrent(mut self, concurrent: usize) -> Self {
611 self.concurrent = concurrent.max(1);
612 self
613 }
614
615 pub fn concurrent(&self) -> usize {
617 self.concurrent
618 }
619
620 pub fn with_chunk(mut self, chunk: usize) -> Self {
622 self.chunk = Some(chunk.max(1));
623 self
624 }
625
626 pub fn chunk(&self) -> Option<usize> {
628 self.chunk
629 }
630
631 pub fn with_gap(mut self, gap: usize) -> Self {
636 self.gap = Some(gap);
637 self
638 }
639
640 pub fn gap(&self) -> Option<usize> {
642 self.gap
643 }
644
645 pub fn with_prefetch(mut self, prefetch: usize) -> Self {
647 self.prefetch = prefetch;
648 self
649 }
650
651 pub fn prefetch(&self) -> usize {
653 self.prefetch
654 }
655}
656
657impl From<options::ReadOptions> for (BytesRange, OpRead, OpReader) {
658 fn from(value: options::ReadOptions) -> Self {
659 let range = value.range;
660 let reader = OpReader {
661 concurrent: value.concurrent.max(1),
662 chunk: value.chunk,
663 gap: value.gap,
664 prefetch: 0,
665 };
666 (range, OpRead::from_read_options(value), reader)
667 }
668}
669
670impl From<options::ReaderOptions> for (OpRead, OpReader) {
671 fn from(value: options::ReaderOptions) -> Self {
672 let reader = OpReader {
673 concurrent: value.concurrent.max(1),
674 chunk: value.chunk,
675 gap: value.gap,
676 prefetch: value.prefetch,
677 };
678 let read = options::ReadOptions {
679 version: value.version,
680 if_match: value.if_match,
681 if_none_match: value.if_none_match,
682 if_version_match: value.if_version_match,
683 if_version_not_match: value.if_version_not_match,
684 if_modified_since: value.if_modified_since,
685 if_unmodified_since: value.if_unmodified_since,
686 content_length_hint: value.content_length_hint,
687 ..Default::default()
688 };
689 (OpRead::from_read_options(read), reader)
690 }
691}
692
693#[derive(Debug, Clone, Default)]
695pub struct OpStat {
696 values: CompactValues,
697}
698
699impl OpStat {
700 pub fn new() -> Self {
702 Self::default()
703 }
704
705 #[inline]
707 pub fn if_match(&self) -> Option<&str> {
708 string_value(&self.values, ReadField::IfMatch as usize)
709 }
710
711 #[inline]
713 pub fn if_none_match(&self) -> Option<&str> {
714 string_value(&self.values, ReadField::IfNoneMatch as usize)
715 }
716
717 #[inline]
719 pub fn if_version_match(&self) -> Option<&str> {
720 string_value(&self.values, ReadField::IfVersionMatch as usize)
721 }
722
723 #[inline]
725 pub fn if_version_not_match(&self) -> Option<&str> {
726 string_value(&self.values, ReadField::IfVersionNotMatch as usize)
727 }
728
729 #[inline]
731 pub fn if_modified_since(&self) -> Option<Timestamp> {
732 self.values
733 .get(ReadField::IfModifiedSince as usize)
734 .map(decode_timestamp)
735 }
736
737 #[inline]
739 pub fn if_unmodified_since(&self) -> Option<Timestamp> {
740 self.values
741 .get(ReadField::IfUnmodifiedSince as usize)
742 .map(decode_timestamp)
743 }
744
745 #[inline]
747 pub fn is_conditional(&self) -> bool {
748 self.if_match().is_some()
749 || self.if_none_match().is_some()
750 || self.if_version_match().is_some()
751 || self.if_version_not_match().is_some()
752 || self.if_modified_since().is_some()
753 || self.if_unmodified_since().is_some()
754 }
755
756 #[inline]
759 pub fn override_content_disposition(&self) -> Option<&str> {
760 string_value(&self.values, ReadField::OverrideContentDisposition as usize)
761 }
762
763 #[inline]
765 pub fn override_cache_control(&self) -> Option<&str> {
766 string_value(&self.values, ReadField::OverrideCacheControl as usize)
767 }
768
769 #[inline]
771 pub fn override_content_type(&self) -> Option<&str> {
772 string_value(&self.values, ReadField::OverrideContentType as usize)
773 }
774
775 #[inline]
777 pub fn version(&self) -> Option<&str> {
778 string_value(&self.values, ReadField::Version as usize)
779 }
780
781 pub(crate) fn from_read(value: &OpRead) -> Self {
782 Self {
785 values: value.values.clone(),
786 }
787 }
788}
789
790impl From<options::StatOptions> for OpStat {
791 fn from(value: options::StatOptions) -> Self {
792 let if_modified_since = value.if_modified_since.map(encode_timestamp);
793 let if_unmodified_since = value.if_unmodified_since.map(encode_timestamp);
794 Self {
795 values: CompactValues::encode(&[
796 value.if_match.as_deref().map(str::as_bytes),
797 value.if_none_match.as_deref().map(str::as_bytes),
798 value.if_version_match.as_deref().map(str::as_bytes),
799 value.if_version_not_match.as_deref().map(str::as_bytes),
800 if_modified_since.as_ref().map(|value| value.as_slice()),
801 if_unmodified_since.as_ref().map(|value| value.as_slice()),
802 value.override_content_type.as_deref().map(str::as_bytes),
803 value.override_cache_control.as_deref().map(str::as_bytes),
804 value
805 .override_content_disposition
806 .as_deref()
807 .map(str::as_bytes),
808 value.version.as_deref().map(str::as_bytes),
809 ]),
810 }
811 }
812}
813
814#[derive(Debug, Clone, Default)]
816pub struct OpWrite {
817 concurrent: usize,
818 flags: u8,
819 values: CompactValues,
820}
821
822const OP_WRITE_APPEND: u8 = 1;
823const OP_WRITE_IF_NOT_EXISTS: u8 = 1 << 1;
824
825#[repr(usize)]
826enum WriteField {
827 ContentType,
828 ContentDisposition,
829 ContentEncoding,
830 CacheControl,
831 IfMatch,
832 IfNoneMatch,
833 IfVersionMatch,
834 IfVersionNotMatch,
835 UserMetadata,
836}
837
838impl OpWrite {
839 pub fn new() -> Self {
843 Self::default()
844 }
845
846 pub fn from_options(
857 capability: &Capability,
858 mut options: options::WriteOptions,
859 ) -> Result<(Self, OpWriter)> {
860 if let Some(metadata) = options.if_not_changed.take() {
861 let (target, identity, name) = if capability.write_with_if_version_match
862 && let Some(version) = metadata.version()
863 {
864 (&mut options.if_version_match, version, "if_version_match")
865 } else if let Some(etag) = metadata.etag() {
866 (&mut options.if_match, etag, "if_match")
867 } else if let Some(version) = metadata.version() {
868 (&mut options.if_version_match, version, "if_version_match")
869 } else {
870 return Err(Error::new(
871 ErrorKind::ConfigInvalid,
872 "if_not_changed metadata contains neither version nor ETag",
873 )
874 .with_operation(Operation::Write));
875 };
876
877 if let Some(explicit) = target {
878 if explicit != identity {
879 return Err(Error::new(
880 ErrorKind::ConditionNotMatch,
881 format!("if_not_changed conflicts with {name}"),
882 )
883 .with_operation(Operation::Write));
884 }
885 } else {
886 *target = Some(identity.to_owned());
887 }
888 }
889
890 let writer = OpWriter {
891 chunk: options.chunk,
892 };
893 let user_metadata = options.user_metadata.take().map(sorted_user_metadata);
894 let mut flags = 0;
895 if options.append {
896 flags |= OP_WRITE_APPEND;
897 }
898 if options.if_not_exists {
899 flags |= OP_WRITE_IF_NOT_EXISTS;
900 }
901 let fields = [
902 options.content_type.as_deref().map(str::as_bytes),
903 options.content_disposition.as_deref().map(str::as_bytes),
904 options.content_encoding.as_deref().map(str::as_bytes),
905 options.cache_control.as_deref().map(str::as_bytes),
906 options.if_match.as_deref().map(str::as_bytes),
907 options.if_none_match.as_deref().map(str::as_bytes),
908 options.if_version_match.as_deref().map(str::as_bytes),
909 options.if_version_not_match.as_deref().map(str::as_bytes),
910 None,
911 ];
912 let mut lengths = fields.map(|value| value.map(<[u8]>::len));
913 lengths[WriteField::UserMetadata as usize] =
914 user_metadata.as_deref().map(user_metadata_encoded_len);
915 let args = Self {
916 concurrent: options.concurrent.max(1),
917 flags,
918 values: CompactValues::encode_with(&lengths, |field, output| match field {
919 field if field == WriteField::UserMetadata as usize => {
920 write_user_metadata(
921 user_metadata.as_deref().expect("present field has a value"),
922 output,
923 );
924 }
925 _ => {
926 output.write(fields[field].expect("present field has a value"));
927 }
928 }),
929 };
930 Ok((args, writer))
931 }
932
933 #[inline]
937 pub fn append(&self) -> bool {
938 self.flags & OP_WRITE_APPEND != 0
939 }
940
941 #[inline]
943 pub fn content_type(&self) -> Option<&str> {
944 string_value(&self.values, WriteField::ContentType as usize)
945 }
946
947 #[inline]
949 pub fn content_disposition(&self) -> Option<&str> {
950 string_value(&self.values, WriteField::ContentDisposition as usize)
951 }
952
953 #[inline]
955 pub fn content_encoding(&self) -> Option<&str> {
956 string_value(&self.values, WriteField::ContentEncoding as usize)
957 }
958
959 #[inline]
961 pub fn cache_control(&self) -> Option<&str> {
962 string_value(&self.values, WriteField::CacheControl as usize)
963 }
964
965 #[inline]
967 pub fn concurrent(&self) -> usize {
968 self.concurrent
969 }
970
971 #[inline]
973 pub fn if_match(&self) -> Option<&str> {
974 string_value(&self.values, WriteField::IfMatch as usize)
975 }
976
977 #[inline]
979 pub fn if_none_match(&self) -> Option<&str> {
980 string_value(&self.values, WriteField::IfNoneMatch as usize)
981 }
982
983 #[inline]
985 pub fn if_version_match(&self) -> Option<&str> {
986 string_value(&self.values, WriteField::IfVersionMatch as usize)
987 }
988
989 #[inline]
991 pub fn if_version_not_match(&self) -> Option<&str> {
992 string_value(&self.values, WriteField::IfVersionNotMatch as usize)
993 }
994
995 #[inline]
997 pub fn if_not_exists(&self) -> bool {
998 self.flags & OP_WRITE_IF_NOT_EXISTS != 0
999 }
1000
1001 #[inline]
1006 pub fn is_conditional(&self) -> bool {
1007 self.if_not_exists()
1008 || self.if_match().is_some()
1009 || self.if_none_match().is_some()
1010 || self.if_version_match().is_some()
1011 || self.if_version_not_match().is_some()
1012 }
1013
1014 #[inline]
1016 pub fn user_metadata(&self) -> Option<UserMetadata<'_>> {
1017 self.values
1018 .get(WriteField::UserMetadata as usize)
1019 .map(UserMetadata::new)
1020 }
1021
1022 #[doc(hidden)]
1023 pub fn into_content_type(mut self, value: &str) -> Self {
1024 self.values = self
1025 .values
1026 .replace(WriteField::ContentType as usize, value.as_bytes());
1027 self
1028 }
1029}
1030
1031#[derive(Debug, Clone, Default)]
1033pub struct OpWriter {
1034 chunk: Option<usize>,
1035}
1036
1037impl OpWriter {
1038 pub fn new() -> Self {
1040 Self::default()
1041 }
1042
1043 pub fn chunk(&self) -> Option<usize> {
1047 self.chunk
1048 }
1049
1050 pub fn with_chunk(mut self, chunk: usize) -> Self {
1060 self.chunk = Some(chunk);
1061 self
1062 }
1063}
1064
1065#[derive(Debug, Clone, Default)]
1067pub struct OpCompose {
1068 concurrent: usize,
1069 flags: u8,
1070 values: CompactValues,
1071}
1072
1073const OP_COMPOSE_IF_NOT_EXISTS: u8 = 1;
1074
1075#[repr(usize)]
1076enum ComposeField {
1077 ContentType,
1078 ContentDisposition,
1079 ContentEncoding,
1080 CacheControl,
1081 IfMatch,
1082 IfNoneMatch,
1083 IfVersionMatch,
1084 IfVersionNotMatch,
1085 UserMetadata,
1086}
1087
1088impl OpCompose {
1089 pub fn new() -> Self {
1091 Self::default()
1092 }
1093
1094 pub fn from_options(
1105 capability: &Capability,
1106 mut options: options::ComposeOptions,
1107 ) -> Result<Self> {
1108 if let Some(metadata) = options.if_not_changed.take() {
1109 let (target, identity, name) = if capability.compose_with_if_version_match
1110 && let Some(version) = metadata.version()
1111 {
1112 (&mut options.if_version_match, version, "if_version_match")
1113 } else if let Some(etag) = metadata.etag() {
1114 (&mut options.if_match, etag, "if_match")
1115 } else if let Some(version) = metadata.version() {
1116 (&mut options.if_version_match, version, "if_version_match")
1117 } else {
1118 return Err(Error::new(
1119 ErrorKind::ConfigInvalid,
1120 "if_not_changed metadata contains neither version nor ETag",
1121 )
1122 .with_operation(Operation::Compose));
1123 };
1124
1125 if let Some(explicit) = target {
1126 if explicit != identity {
1127 return Err(Error::new(
1128 ErrorKind::ConditionNotMatch,
1129 format!("if_not_changed conflicts with {name}"),
1130 )
1131 .with_operation(Operation::Compose));
1132 }
1133 } else {
1134 *target = Some(identity.to_owned());
1135 }
1136 }
1137
1138 let user_metadata = options.user_metadata.take().map(sorted_user_metadata);
1139 let fields = [
1140 options.content_type.as_deref().map(str::as_bytes),
1141 options.content_disposition.as_deref().map(str::as_bytes),
1142 options.content_encoding.as_deref().map(str::as_bytes),
1143 options.cache_control.as_deref().map(str::as_bytes),
1144 options.if_match.as_deref().map(str::as_bytes),
1145 options.if_none_match.as_deref().map(str::as_bytes),
1146 options.if_version_match.as_deref().map(str::as_bytes),
1147 options.if_version_not_match.as_deref().map(str::as_bytes),
1148 None,
1149 ];
1150 let mut lengths = fields.map(|value| value.map(<[u8]>::len));
1151 lengths[ComposeField::UserMetadata as usize] =
1152 user_metadata.as_deref().map(user_metadata_encoded_len);
1153 Ok(Self {
1154 concurrent: options.concurrent.max(1),
1155 flags: if options.if_not_exists {
1156 OP_COMPOSE_IF_NOT_EXISTS
1157 } else {
1158 0
1159 },
1160 values: CompactValues::encode_with(&lengths, |field, output| match field {
1161 field if field == ComposeField::UserMetadata as usize => {
1162 write_user_metadata(
1163 user_metadata.as_deref().expect("present field has a value"),
1164 output,
1165 );
1166 }
1167 _ => {
1168 output.write(fields[field].expect("present field has a value"));
1169 }
1170 }),
1171 })
1172 }
1173
1174 pub fn concurrent(&self) -> usize {
1176 self.concurrent.max(1)
1177 }
1178
1179 pub fn content_type(&self) -> Option<&str> {
1181 string_value(&self.values, ComposeField::ContentType as usize)
1182 }
1183
1184 pub fn content_disposition(&self) -> Option<&str> {
1186 string_value(&self.values, ComposeField::ContentDisposition as usize)
1187 }
1188
1189 pub fn content_encoding(&self) -> Option<&str> {
1191 string_value(&self.values, ComposeField::ContentEncoding as usize)
1192 }
1193
1194 pub fn cache_control(&self) -> Option<&str> {
1196 string_value(&self.values, ComposeField::CacheControl as usize)
1197 }
1198
1199 pub fn user_metadata(&self) -> Option<UserMetadata<'_>> {
1201 self.values
1202 .get(ComposeField::UserMetadata as usize)
1203 .map(UserMetadata::new)
1204 }
1205
1206 pub fn if_match(&self) -> Option<&str> {
1208 string_value(&self.values, ComposeField::IfMatch as usize)
1209 }
1210
1211 pub fn if_none_match(&self) -> Option<&str> {
1213 string_value(&self.values, ComposeField::IfNoneMatch as usize)
1214 }
1215
1216 pub fn if_version_match(&self) -> Option<&str> {
1218 string_value(&self.values, ComposeField::IfVersionMatch as usize)
1219 }
1220
1221 pub fn if_version_not_match(&self) -> Option<&str> {
1223 string_value(&self.values, ComposeField::IfVersionNotMatch as usize)
1224 }
1225
1226 pub fn if_not_exists(&self) -> bool {
1228 self.flags & OP_COMPOSE_IF_NOT_EXISTS != 0
1229 }
1230
1231 #[inline]
1236 pub fn is_conditional(&self) -> bool {
1237 self.if_not_exists()
1238 || self.if_match().is_some()
1239 || self.if_none_match().is_some()
1240 || self.if_version_match().is_some()
1241 || self.if_version_not_match().is_some()
1242 }
1243
1244 #[doc(hidden)]
1245 pub fn into_content_type(mut self, value: &str) -> Self {
1246 self.values = self
1247 .values
1248 .replace(ComposeField::ContentType as usize, value.as_bytes());
1249 self
1250 }
1251}
1252
1253#[derive(Debug, Clone, Default)]
1255pub struct OpCopy {
1256 concurrent: usize,
1257 chunk: Option<usize>,
1258 source_content_length_hint: Option<u64>,
1259 flags: u8,
1260 values: CompactValues,
1261}
1262
1263const OP_COPY_IF_NOT_EXISTS: u8 = 1;
1264
1265#[repr(usize)]
1266enum CopyField {
1267 IfMatch,
1268 IfNoneMatch,
1269 IfVersionMatch,
1270 IfVersionNotMatch,
1271 SourceVersion,
1272}
1273
1274impl OpCopy {
1275 pub fn new() -> Self {
1277 Self::default()
1278 }
1279
1280 pub fn from_options(
1291 capability: &Capability,
1292 mut options: options::CopyOptions,
1293 ) -> Result<Self> {
1294 if let Some(metadata) = options.if_not_changed.take() {
1295 let (target, identity, name) = if capability.copy_with_if_version_match
1296 && let Some(version) = metadata.version()
1297 {
1298 (&mut options.if_version_match, version, "if_version_match")
1299 } else if let Some(etag) = metadata.etag() {
1300 (&mut options.if_match, etag, "if_match")
1301 } else if let Some(version) = metadata.version() {
1302 (&mut options.if_version_match, version, "if_version_match")
1303 } else {
1304 return Err(Error::new(
1305 ErrorKind::ConfigInvalid,
1306 "if_not_changed metadata contains neither version nor ETag",
1307 )
1308 .with_operation(Operation::Copy));
1309 };
1310
1311 if let Some(explicit) = target {
1312 if explicit != identity {
1313 return Err(Error::new(
1314 ErrorKind::ConditionNotMatch,
1315 format!("if_not_changed conflicts with {name}"),
1316 )
1317 .with_operation(Operation::Copy));
1318 }
1319 } else {
1320 *target = Some(identity.to_owned());
1321 }
1322 }
1323
1324 let fields = [
1325 options.if_match.as_deref().map(str::as_bytes),
1326 options.if_none_match.as_deref().map(str::as_bytes),
1327 options.if_version_match.as_deref().map(str::as_bytes),
1328 options.if_version_not_match.as_deref().map(str::as_bytes),
1329 options.source_version.as_deref().map(str::as_bytes),
1330 ];
1331 Ok(Self {
1332 concurrent: options.concurrent.max(1),
1333 chunk: options.chunk,
1334 source_content_length_hint: options.source_content_length_hint,
1335 flags: if options.if_not_exists {
1336 OP_COPY_IF_NOT_EXISTS
1337 } else {
1338 0
1339 },
1340 values: CompactValues::encode(&fields),
1341 })
1342 }
1343
1344 #[inline]
1346 pub fn if_not_exists(&self) -> bool {
1347 self.flags & OP_COPY_IF_NOT_EXISTS != 0
1348 }
1349
1350 #[inline]
1352 pub fn if_match(&self) -> Option<&str> {
1353 string_value(&self.values, CopyField::IfMatch as usize)
1354 }
1355
1356 #[inline]
1358 pub fn if_none_match(&self) -> Option<&str> {
1359 string_value(&self.values, CopyField::IfNoneMatch as usize)
1360 }
1361
1362 #[inline]
1364 pub fn if_version_match(&self) -> Option<&str> {
1365 string_value(&self.values, CopyField::IfVersionMatch as usize)
1366 }
1367
1368 #[inline]
1370 pub fn if_version_not_match(&self) -> Option<&str> {
1371 string_value(&self.values, CopyField::IfVersionNotMatch as usize)
1372 }
1373
1374 #[inline]
1379 pub fn is_conditional(&self) -> bool {
1380 self.if_not_exists()
1381 || self.if_match().is_some()
1382 || self.if_none_match().is_some()
1383 || self.if_version_match().is_some()
1384 || self.if_version_not_match().is_some()
1385 }
1386
1387 #[inline]
1389 pub fn source_version(&self) -> Option<&str> {
1390 string_value(&self.values, CopyField::SourceVersion as usize)
1391 }
1392
1393 pub fn concurrent(&self) -> usize {
1395 self.concurrent.max(1)
1396 }
1397
1398 pub fn chunk(&self) -> Option<usize> {
1400 self.chunk
1401 }
1402
1403 pub fn source_content_length_hint(&self) -> Option<u64> {
1405 self.source_content_length_hint
1406 }
1407}
1408
1409#[derive(Debug, Clone, Default)]
1411pub struct OpRename {
1412 if_not_exists: bool,
1417}
1418
1419impl OpRename {
1420 pub fn new() -> Self {
1422 Self::default()
1423 }
1424
1425 pub fn with_if_not_exists(mut self, if_not_exists: bool) -> Self {
1436 self.if_not_exists = if_not_exists;
1437 self
1438 }
1439
1440 pub fn if_not_exists(&self) -> bool {
1442 self.if_not_exists
1443 }
1444
1445 #[inline]
1447 pub fn is_conditional(&self) -> bool {
1448 self.if_not_exists()
1449 }
1450}
1451
1452impl From<options::RenameOptions> for OpRename {
1453 fn from(value: options::RenameOptions) -> Self {
1454 Self {
1455 if_not_exists: value.if_not_exists,
1456 }
1457 }
1458}
1459
1460#[derive(Debug, Clone, Default)]
1462pub struct OpRestore {
1463 flags: u8,
1464 values: CompactValues,
1465}
1466
1467const OP_RESTORE_IF_NOT_EXISTS: u8 = 1;
1468
1469impl OpRestore {
1470 pub fn new() -> Self {
1472 Self::default()
1473 }
1474
1475 #[inline]
1477 pub fn version(&self) -> Option<&str> {
1478 string_value(&self.values, 0)
1479 }
1480
1481 #[inline]
1483 pub fn if_not_exists(&self) -> bool {
1484 self.flags & OP_RESTORE_IF_NOT_EXISTS != 0
1485 }
1486
1487 #[inline]
1489 pub fn is_conditional(&self) -> bool {
1490 self.if_not_exists()
1491 }
1492}
1493
1494impl From<options::RestoreOptions> for OpRestore {
1495 fn from(value: options::RestoreOptions) -> Self {
1496 Self {
1497 flags: if value.if_not_exists {
1498 OP_RESTORE_IF_NOT_EXISTS
1499 } else {
1500 0
1501 },
1502 values: CompactValues::encode(&[value.version.as_deref().map(str::as_bytes)]),
1503 }
1504 }
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509 use super::*;
1510 use crate::Metadata;
1511 use crate::MetadataBuilder;
1512
1513 fn condition_metadata() -> Metadata {
1514 let mut metadata = MetadataBuilder::file(42);
1515 metadata
1516 .etag("etag")
1517 .version("version")
1518 .user_metadata([("owner".to_owned(), "opendal".to_owned())]);
1519 metadata.build()
1520 }
1521
1522 #[test]
1523 fn compact_operation_layouts() {
1524 assert_eq!(size_of::<OpRead>(), 16);
1525 assert_eq!(size_of::<OpStat>(), 16);
1526 assert_eq!(size_of::<OpWrite>(), 32);
1527 assert_eq!(size_of::<OpCompose>(), 32);
1528 assert_eq!(size_of::<OpDelete>(), 24);
1529 assert_eq!(size_of::<OpCopy>(), 64);
1530 assert_eq!(size_of::<OpList>(), 32);
1531 assert_eq!(size_of::<OpRestore>(), 24);
1532 }
1533
1534 #[test]
1535 fn read_options_roundtrip() {
1536 let modified = Timestamp::new(-1, -123).unwrap();
1537 let unmodified = Timestamp::new(2, 456).unwrap();
1538 let options = options::ReadOptions {
1539 version: Some("version".to_owned()),
1540 if_match: Some("etag".to_owned()),
1541 if_none_match: Some("other-etag".to_owned()),
1542 if_version_match: Some("version-match".to_owned()),
1543 if_version_not_match: Some("version-not-match".to_owned()),
1544 if_modified_since: Some(modified),
1545 if_unmodified_since: Some(unmodified),
1546 content_length_hint: Some(42),
1547 override_content_type: Some("text/plain".to_owned()),
1548 override_cache_control: Some("no-cache".to_owned()),
1549 override_content_disposition: Some("attachment".to_owned()),
1550 ..Default::default()
1551 };
1552 let (_, args, _) = options.into();
1553
1554 assert_eq!(args.version(), Some("version"));
1555 assert_eq!(args.if_match(), Some("etag"));
1556 assert_eq!(args.if_none_match(), Some("other-etag"));
1557 assert_eq!(args.if_version_match(), Some("version-match"));
1558 assert_eq!(args.if_version_not_match(), Some("version-not-match"));
1559 assert_eq!(args.if_modified_since(), Some(modified));
1560 assert_eq!(args.if_unmodified_since(), Some(unmodified));
1561 assert_eq!(args.content_length_hint(), Some(42));
1562 assert_eq!(args.override_content_type(), Some("text/plain"));
1563 assert_eq!(args.override_cache_control(), Some("no-cache"));
1564 assert_eq!(args.override_content_disposition(), Some("attachment"));
1565 assert!(args.is_conditional());
1566 }
1567
1568 #[test]
1569 fn write_options_preserve_owned_views() {
1570 let (args, _) = OpWrite::from_options(
1571 &Capability::default(),
1572 options::WriteOptions {
1573 append: true,
1574 concurrent: 4,
1575 content_type: Some("text/plain".to_owned()),
1576 content_disposition: Some("attachment".to_owned()),
1577 content_encoding: Some("gzip".to_owned()),
1578 cache_control: Some("no-cache".to_owned()),
1579 if_match: Some("etag".to_owned()),
1580 if_none_match: Some("other-etag".to_owned()),
1581 if_version_match: Some("version-match".to_owned()),
1582 if_version_not_match: Some("version-not-match".to_owned()),
1583 if_not_exists: true,
1584 user_metadata: Some(HashMap::from([("owner".to_owned(), "opendal".to_owned())])),
1585 ..Default::default()
1586 },
1587 )
1588 .unwrap();
1589
1590 assert!(args.append());
1591 assert_eq!(args.concurrent(), 4);
1592 assert_eq!(args.content_type(), Some("text/plain"));
1593 assert_eq!(args.content_disposition(), Some("attachment"));
1594 assert_eq!(args.content_encoding(), Some("gzip"));
1595 assert_eq!(args.cache_control(), Some("no-cache"));
1596 assert_eq!(args.if_match(), Some("etag"));
1597 assert_eq!(args.if_none_match(), Some("other-etag"));
1598 assert_eq!(args.if_version_match(), Some("version-match"));
1599 assert_eq!(args.if_version_not_match(), Some("version-not-match"));
1600 assert!(args.if_not_exists());
1601 assert_eq!(args.user_metadata().unwrap().get("owner"), Some("opendal"));
1602 assert!(args.is_conditional());
1603 }
1604
1605 #[test]
1606 fn compose_options_preserve_owned_views() {
1607 let args = OpCompose::from_options(
1608 &Capability::default(),
1609 options::ComposeOptions {
1610 concurrent: 4,
1611 content_type: Some("text/plain".to_owned()),
1612 content_disposition: Some("attachment".to_owned()),
1613 content_encoding: Some("gzip".to_owned()),
1614 cache_control: Some("no-cache".to_owned()),
1615 if_match: Some("etag".to_owned()),
1616 if_none_match: Some("other-etag".to_owned()),
1617 if_version_match: Some("version-match".to_owned()),
1618 if_version_not_match: Some("version-not-match".to_owned()),
1619 if_not_exists: true,
1620 user_metadata: Some(HashMap::from([("owner".to_owned(), "opendal".to_owned())])),
1621 ..Default::default()
1622 },
1623 )
1624 .unwrap();
1625
1626 assert_eq!(args.concurrent(), 4);
1627 assert_eq!(args.content_type(), Some("text/plain"));
1628 assert_eq!(args.content_disposition(), Some("attachment"));
1629 assert_eq!(args.content_encoding(), Some("gzip"));
1630 assert_eq!(args.cache_control(), Some("no-cache"));
1631 assert_eq!(args.if_match(), Some("etag"));
1632 assert_eq!(args.if_none_match(), Some("other-etag"));
1633 assert_eq!(args.if_version_match(), Some("version-match"));
1634 assert_eq!(args.if_version_not_match(), Some("version-not-match"));
1635 assert!(args.if_not_exists());
1636 assert_eq!(args.user_metadata().unwrap().get("owner"), Some("opendal"));
1637 assert!(args.is_conditional());
1638 }
1639
1640 #[test]
1641 fn options_lower_if_not_changed_before_freeze() {
1642 let condition = condition_metadata();
1643 let capability = Capability {
1644 write_with_if_match: true,
1645 write_with_if_version_match: true,
1646 delete_with_if_match: true,
1647 copy_with_if_match: true,
1648 ..Default::default()
1649 };
1650 let (write, _) = OpWrite::from_options(
1651 &capability,
1652 options::WriteOptions {
1653 if_not_changed: Some(condition.clone()),
1654 ..Default::default()
1655 },
1656 )
1657 .unwrap();
1658 assert_eq!(write.if_match(), None);
1659 assert_eq!(write.if_version_match(), Some("version"));
1660
1661 let delete = OpDelete::from_options(
1662 &capability,
1663 options::DeleteOptions {
1664 if_not_changed: Some(condition.clone()),
1665 ..Default::default()
1666 },
1667 )
1668 .unwrap();
1669 assert_eq!(delete.if_match(), Some("etag"));
1670 assert_eq!(delete.if_version_match(), None);
1671
1672 let copy = OpCopy::from_options(
1673 &capability,
1674 options::CopyOptions {
1675 if_not_changed: Some(condition.clone()),
1676 ..Default::default()
1677 },
1678 )
1679 .unwrap();
1680 assert_eq!(copy.if_match(), Some("etag"));
1681 assert_eq!(copy.if_version_match(), None);
1682
1683 let compose = OpCompose::from_options(
1684 &capability,
1685 options::ComposeOptions {
1686 if_not_changed: Some(condition.clone()),
1687 ..Default::default()
1688 },
1689 )
1690 .unwrap();
1691 assert_eq!(compose.if_match(), Some("etag"));
1692 assert_eq!(compose.if_version_match(), None);
1693
1694 let compose_source = OpRead::from_compose_source_options(
1695 &capability,
1696 options::ComposeSourceOptions {
1697 if_not_changed: Some(condition.clone()),
1698 ..Default::default()
1699 },
1700 )
1701 .unwrap();
1702 assert_eq!(compose_source.if_match(), Some("etag"));
1703 assert_eq!(compose_source.version(), None);
1704
1705 let compose_source = OpRead::from_compose_source_options(
1706 &Capability {
1707 compose_with_source_version: true,
1708 ..Default::default()
1709 },
1710 options::ComposeSourceOptions {
1711 if_not_changed: Some(condition),
1712 ..Default::default()
1713 },
1714 )
1715 .unwrap();
1716 assert_eq!(compose_source.if_match(), None);
1717 assert_eq!(compose_source.version(), Some("version"));
1718 }
1719
1720 #[test]
1721 fn delete_copy_list_and_restore_options_roundtrip() {
1722 let delete = OpDelete::from_options(
1723 &Capability::default(),
1724 options::DeleteOptions {
1725 version: Some("version".to_owned()),
1726 recursive: true,
1727 if_match: Some("etag".to_owned()),
1728 ..Default::default()
1729 },
1730 )
1731 .unwrap();
1732 assert_eq!(delete.version(), Some("version"));
1733 assert!(delete.recursive());
1734 assert_eq!(delete.if_match(), Some("etag"));
1735 assert!(delete.is_conditional());
1736
1737 let copy = OpCopy::from_options(
1738 &Capability::default(),
1739 options::CopyOptions {
1740 if_not_exists: true,
1741 if_version_match: Some("destination-version".to_owned()),
1742 source_version: Some("source-version".to_owned()),
1743 ..Default::default()
1744 },
1745 )
1746 .unwrap();
1747 assert!(copy.if_not_exists());
1748 assert_eq!(copy.if_version_match(), Some("destination-version"));
1749 assert_eq!(copy.source_version(), Some("source-version"));
1750 assert!(copy.is_conditional());
1751
1752 let list: OpList = options::ListOptions {
1753 limit: Some(100),
1754 start_after: Some("marker".to_owned()),
1755 recursive: true,
1756 versions: true,
1757 deleted: true,
1758 }
1759 .into();
1760 assert_eq!(list.limit(), Some(100));
1761 assert_eq!(list.start_after(), Some("marker"));
1762 assert!(list.recursive());
1763 assert!(list.versions());
1764 assert!(list.deleted());
1765
1766 let restore: OpRestore = options::RestoreOptions {
1767 version: Some("version".to_owned()),
1768 if_not_exists: true,
1769 }
1770 .into();
1771 assert_eq!(restore.version(), Some("version"));
1772 assert!(restore.if_not_exists());
1773 assert!(restore.is_conditional());
1774 }
1775}