1use std::fmt;
148
149use crate::{
150 prelude::*,
151 account::Capability,
152 bucket::{
153 Bucket,
154 FileRetentionMode,
155 FileRetentionPolicy,
156 ServerSideEncryption,
157 },
158 client::{HeaderMap, HttpClient},
159 error::*,
160 types::ContentDisposition,
161 validate::{
162 validate_content_disposition,
163 validate_file_metadata_size,
164 validated_file_info,
165 validated_file_name,
166 },
167};
168
169pub use http_types::{
170 cache::{CacheControl, Expires},
171 content::ContentEncoding,
172 mime::Mime,
173};
174
175use serde::{Serialize, Deserialize};
176
177
178macro_rules! add_file_info {
180 ($map:ident, $name:literal, $value:expr) => {
181 if let Some(v) = $value {
182 $map.insert($name.into(), serde_json::Value::from(v));
183 }
184 };
185}
186
187macro_rules! percent_encode {
188 ($str:expr) => {
189 percent_encoding::utf8_percent_encode(
190 &$str,
191 &crate::types::QUERY_ENCODE_SET
192 ).to_string()
193 };
194}
195
196#[derive(Debug, Serialize, Deserialize)]
197#[serde(rename_all = "lowercase")]
198enum LegalHoldValue {
199 On,
200 Off,
201}
202
203impl fmt::Display for LegalHoldValue {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 match self {
206 Self::On => write!(f, "on"),
207 Self::Off => write!(f, "off"),
208 }
209 }
210}
211
212#[derive(Debug, Deserialize)]
214pub struct FileLegalHold {
215 #[serde(rename = "isClientAuthorizedToRead")]
216 can_read: bool,
217 value: Option<LegalHoldValue>,
218}
219
220#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
222#[serde(rename_all = "camelCase")]
223pub enum FileAction {
224 Start,
226 Upload,
228 Copy,
230 Hide,
232 Folder,
234}
235
236#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
242#[allow(dead_code)]
243pub struct FileRetentionSetting {
244 mode: Option<FileRetentionMode>,
245 #[serde(rename = "retainUntilTimestamp")]
246 retain_until: Option<i64>,
247}
248
249impl FileRetentionSetting {
250 pub fn new(
251 mode: FileRetentionMode,
252 retain_until: chrono::DateTime<chrono::Utc>
253 ) -> Result<Self, BadData<chrono::DateTime<chrono::Utc>>> {
254 if retain_until > chrono::Utc::now() {
255 Ok(Self {
256 mode: Some(mode),
257 retain_until: Some(retain_until.timestamp_millis()),
258 })
259 } else {
260 Err(BadData {
261 value: retain_until,
262 msg: "retain_until must be in the future".into(),
263 })
264 }
265 }
266}
267
268#[derive(Debug, Deserialize)]
271pub struct FileRetention {
272 #[serde(rename = "isClientAuthorizedToRead")]
273 can_read: bool,
274 value: FileRetentionSetting,
275}
276
277impl FileRetention {
278 pub fn settings(&self) -> Option<FileRetentionSetting> {
282 if self.can_read {
283 Some(self.value)
284 } else {
285 None
286 }
287 }
288}
289
290#[derive(Debug, Deserialize)]
293#[serde(rename_all = "camelCase")]
294#[allow(dead_code)]
295pub struct File {
296 account_id: Option<String>,
297 action: FileAction,
298 bucket_id: String,
299 content_length: u64,
301 content_sha1: Option<String>, content_md5: Option<String>, content_type: Option<String>,
305 file_id: String,
306 file_info: serde_json::Value,
307 file_name: String,
308 file_retention: Option<FileRetention>,
309 legal_hold: Option<FileLegalHold>,
310 server_side_encryption: Option<ServerSideEncryption>,
311 upload_timestamp: i64,
314}
315
316impl File {
317 pub fn action(&self) -> FileAction { self.action }
319
320 pub fn bucket_id(&self) -> &str { &self.bucket_id }
322
323 pub fn content_length(&self) -> Option<u64> {
328 match self.action {
329 FileAction::Upload | FileAction::Copy => Some(self.content_length),
330 _ => None,
331 }
332 }
333
334 pub fn sha1_checksum(&self) -> Option<&String> {
339 match &self.content_sha1 {
340 Some(v) => if v == "none" { None } else { Some(v) }
341 None => None,
342 }
343 }
344
345 pub fn md5_checksum(&self) -> Option<&String> {
350 self.content_md5.as_ref()
351 }
352
353 pub fn content_type(&self) -> Option<&String> {
356 self.content_type.as_ref()
357 }
358
359 pub fn file_id(&self) -> &str { &self.file_id }
361
362 pub fn file_info(&self) -> &serde_json::Value {
364 &self.file_info
365 }
366
367 pub fn file_name(&self) -> &str { &self.file_name }
369
370 pub fn file_retention(&self) -> Option<&FileRetention> {
372 self.file_retention.as_ref()
373 }
374
375 pub fn has_legal_hold<E>(&self) -> Result<Option<bool>, Error<E>>
383 where E: fmt::Debug + fmt::Display,
384 {
385 if let Some(hold) = &self.legal_hold {
386 if ! hold.can_read {
387 Err(Error::Unauthorized(Capability::ReadFileLegalHolds))
388 } else if let Some(val) = &hold.value {
389 match val {
390 LegalHoldValue::On => Ok(Some(true)),
391 LegalHoldValue::Off => Ok(Some(false)),
392 }
393 } else {
394 Ok(None)
395 }
396 } else {
397 Ok(None)
398 }
399 }
400
401 pub fn encryption_settings(&self) -> Option<&ServerSideEncryption> {
403 self.server_side_encryption.as_ref()
404 }
405
406 pub fn upload_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
410 use chrono::{TimeZone as _, Utc};
411
412 match self.action {
413 FileAction::Folder => None,
414 _ => Some(Utc.timestamp_millis(self.upload_timestamp)),
415 }
416 }
417}
418
419#[derive(Debug, Deserialize)]
421#[serde(rename_all = "camelCase")]
422pub struct FilePart {
423 file_id: String,
424 part_number: u16,
425 content_length: u64,
426 content_sha1: String,
427 content_md5: Option<String>,
428 server_side_encryption: Option<ServerSideEncryption>,
429 upload_timestamp: i64,
430}
431
432impl FilePart {
433 pub fn file_id(&self) -> &str { &self.file_id }
434 pub fn part_number(&self) -> u16 { self.part_number }
435 pub fn content_length(&self) -> u64 { self.content_length }
436 pub fn sha1_checksum(&self) -> &str { &self.content_sha1 }
437 pub fn md5_checksum(&self) -> Option<&String> { self.content_md5.as_ref() }
438
439 pub fn encryption_settings(&self) -> Option<&ServerSideEncryption> {
440 self.server_side_encryption.as_ref()
441 }
442
443 pub fn upload_timestamp(&self) -> chrono::DateTime<chrono::Utc> {
444 use chrono::{TimeZone as _, Utc};
445
446 Utc.timestamp_millis(self.upload_timestamp)
447 }
448}
449
450#[derive(Debug, Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct CancelledFileUpload {
454 pub file_id: String,
456 pub account_id: String,
458 pub bucket_id: String,
460 pub file_name: String,
462}
463
464#[derive(Debug, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct DeletedFile {
467 pub file_id: String,
468 pub file_name: String,
469}
470
471pub async fn cancel_large_file<C, E>(auth: &mut Authorization<C>, file: File)
473-> Result<CancelledFileUpload, Error<E>>
474 where C: HttpClient<Error=Error<E>>,
475 E: fmt::Debug + fmt::Display,
476{
477 cancel_large_file_by_id(auth, file.file_id).await
478}
479
480pub async fn cancel_large_file_by_id<C, E>(
484 auth: &mut Authorization<C>,
485 id: impl AsRef<str>
486) -> Result<CancelledFileUpload, Error<E>>
487 where C: HttpClient<Error=Error<E>>,
488 E: fmt::Debug + fmt::Display,
489{
490 require_capability!(auth, Capability::WriteFiles);
491
492 let res = auth.client.post(auth.api_url("b2_cancel_large_file"))
493 .expect("Invalid URL")
494 .with_header("Authorization", &auth.authorization_token).unwrap()
495 .with_body_json(serde_json::json!({ "fileId": id.as_ref() }))
496 .send().await?;
497
498 let info: B2Result<CancelledFileUpload> = serde_json::from_slice(&res)?;
499 info.into()
500}
501
502#[derive(Debug, Clone, Serialize)]
506#[serde(into = "String")]
507pub struct ByteRange { start: u64, end: u64 }
508
509impl From<ByteRange> for String {
510 fn from(r: ByteRange) -> String {
511 format!("bytes={}-{}", r.start, r.end)
512 }
513}
514
515impl fmt::Display for ByteRange {
516 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517 write!(f, "bytes={}-{}", self.start, self.end)
518 }
519}
520
521impl ByteRange {
522 pub fn new(start: u64, end: u64) -> Result<Self, ValidationError> {
528 if start <= end {
529 Ok(Self { start, end })
530 } else {
531 Err(ValidationError::Incompatible(format!(
532 "Invalid start and end for range: {} to {}", start, end
533 )))
534 }
535 }
536
537 pub fn start(&self) -> u64 { self.start }
538 pub fn end(&self) -> u64 { self.end }
539}
540
541#[derive(Debug, Eq, PartialEq, Serialize)]
543#[serde(rename_all = "UPPERCASE")]
544pub enum MetadataDirective {
545 Copy,
546 Replace,
547}
548
549#[derive(Debug, Serialize)]
553#[serde(rename_all = "camelCase")]
554pub struct CopyFile<'a> {
555 source_file_id: String,
556 #[serde(skip_serializing_if = "Option::is_none")]
557 destination_bucket_id: Option<String>,
558 file_name: &'a str,
559 #[serde(skip_serializing_if = "Option::is_none")]
560 range: Option<ByteRange>,
561 metadata_directive: MetadataDirective,
562 #[serde(skip_serializing_if = "Option::is_none")]
563 content_type: Option<String>,
564 #[serde(skip_serializing_if = "Option::is_none")]
565 file_info: Option<serde_json::Value>,
566 #[serde(skip_serializing_if = "Option::is_none")]
567 file_retention: Option<FileRetentionPolicy>,
568 #[serde(skip_serializing_if = "Option::is_none")]
569 legal_hold: Option<LegalHoldValue>,
570 #[serde(rename = "sourceServerSideEncryption")]
571 #[serde(skip_serializing_if = "Option::is_none")]
572 source_encryption: Option<ServerSideEncryption>,
573 #[serde(rename = "destinationServerSideEncryption")]
574 #[serde(skip_serializing_if = "Option::is_none")]
575 dest_encryption: Option<ServerSideEncryption>,
576}
577
578impl<'a> CopyFile<'a> {
579 pub fn builder() -> CopyFileBuilder<'a> {
580 CopyFileBuilder::default()
581 }
582}
583
584#[derive(Default)]
589pub struct CopyFileBuilder<'a> {
590 source_file_id: Option<String>,
591 destination_bucket_id: Option<String>,
592 file_name: Option<&'a str>,
593 range: Option<ByteRange>,
594 metadata_directive: Option<MetadataDirective>,
595 content_type: Option<String>,
596 file_info: Option<serde_json::Value>,
597 file_retention: Option<FileRetentionPolicy>,
598 legal_hold: Option<LegalHoldValue>,
599 source_encryption: Option<ServerSideEncryption>,
600 dest_encryption: Option<ServerSideEncryption>,
601
602 last_modified: Option<i64>,
604 sha1_checksum: Option<&'a str>,
605 content_disposition: Option<String>,
606 content_language: Option<String>,
607 expires: Option<String>,
608 cache_control: Option<String>,
609 content_encoding: Option<String>,
610}
611
612impl<'a> CopyFileBuilder<'a> {
613 pub fn source_file(mut self, file: &File) -> Self {
616 self.source_encryption = file.server_side_encryption.clone();
617 self.source_file_id(&file.file_id)
618 }
619
620 pub fn source_file_id(mut self, file: impl Into<String>) -> Self {
622 self.source_file_id = Some(file.into());
623 self
624 }
625
626 pub fn destination_bucket_id(mut self, bucket: impl Into<String>) -> Self {
632 self.destination_bucket_id = Some(bucket.into());
633 self
634 }
635
636 pub fn destination_file_name(mut self, name: &'a str)
638 -> Result<Self, FileNameValidationError> {
639 self.file_name = Some(validated_file_name(name)?);
640 Ok(self)
641 }
642
643 pub fn range(mut self, range: ByteRange) -> Self {
645 self.range = Some(range);
646 self
647 }
648
649 pub fn metadata_directive(mut self, directive: MetadataDirective) -> Self {
658 self.metadata_directive = Some(directive);
659 self
660 }
661
662 pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
668 self.content_type = Some(content_type.into());
669 self
670 }
671
672 pub fn file_info(mut self, info: serde_json::Value)
693 -> Result<Self, ValidationError> {
694 self.file_info = Some(validated_file_info(info)?);
695 Ok(self)
696 }
697
698 pub fn file_retention(mut self, retention: FileRetentionPolicy) -> Self {
702 self.file_retention = Some(retention);
703 self
704 }
705
706 pub fn with_legal_hold(mut self) -> Self {
708 self.legal_hold = Some(LegalHoldValue::On);
709 self
710 }
711
712 pub fn without_legal_hold(mut self) -> Self {
714 self.legal_hold = Some(LegalHoldValue::Off);
715 self
716 }
717
718 pub fn source_encryption_settings(mut self, settings: ServerSideEncryption)
723 -> Self {
724 self.source_encryption = Some(settings);
725 self
726 }
727
728 pub fn destination_encryption_settings(
732 mut self,
733 settings: ServerSideEncryption
734 ) -> Self {
735 self.dest_encryption = Some(settings);
736 self
737 }
738
739 pub fn last_modified(mut self, time: chrono::DateTime<chrono::Utc>) -> Self
741 {
742 self.last_modified = Some(time.timestamp_millis());
743 self
744 }
745
746 pub fn sha1_checksum(mut self, checksum: &'a str) -> Self {
751 self.sha1_checksum = Some(checksum);
752 self
753 }
754
755 pub fn content_disposition(mut self, disposition: ContentDisposition)
760 -> Result<Self, ValidationError> {
761 validate_content_disposition(&disposition.0, false)?;
762
763 self.content_disposition = Some(percent_encode!(disposition.0));
764 Ok(self)
765 }
766
767 pub fn content_language(mut self, language: impl Into<String>) -> Self {
772 self.content_language = Some(percent_encode!(language.into()));
774 self
775 }
776
777 pub fn expiration(mut self, expiration: Expires) -> Self {
781 let expires = percent_encode!(expiration.value().to_string());
782
783 self.expires = Some(expires);
784 self
785 }
786
787 pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
793 self.cache_control = Some(cache_control.value().to_string());
794 self
795 }
796
797 pub fn content_encoding(mut self, encoding: ContentEncoding) -> Self {
802 let encoding = percent_encode!(format!("{}", encoding.encoding()));
803 self.content_encoding = Some(encoding);
804 self
805 }
806
807 pub fn build(self) -> Result<CopyFile<'a>, ValidationError> {
820 let source_file_id = self.source_file_id.ok_or_else(||
821 ValidationError::MissingData(
822 "The source file ID is required".into()
823 )
824 )?;
825
826 let file_name = self.file_name.ok_or_else(||
827 ValidationError::MissingData(
828 "The new file name must be specified".into()
829 )
830 )?;
831
832 let metadata_directive = self.metadata_directive
833 .unwrap_or(MetadataDirective::Copy);
834
835 if matches!(metadata_directive, MetadataDirective::Copy) {
836 if self.content_type.is_some() {
837 return Err(ValidationError::Incompatible(
838 "When copying a file, a new content-type cannot be set"
839 .into()
840 ));
841 } else if self.file_info.is_some() {
842 return Err(ValidationError::Incompatible(
843 "When copying a file, setting new file info is invalid"
844 .into()
845 ));
846 }
847 }
848
849 let file_info = if let Some(mut file_info) = self.file_info {
850 let info_map = file_info.as_object_mut()
851 .expect("file_info is not a JSON object");
852
853 add_file_info!(info_map, "src_last_modified_millis",
854 self.last_modified.map(|v| v.to_string()));
855 add_file_info!(info_map, "large_file_sha1", self.sha1_checksum);
856 add_file_info!(info_map, "b2-content-disposition",
857 self.content_disposition);
858 add_file_info!(info_map, "b2-content-language",
859 self.content_language);
860 add_file_info!(info_map, "b2-expires", self.expires);
861 add_file_info!(info_map, "b2-content-encoding",
862 self.content_encoding);
863
864 Some(file_info)
865 } else {
866 None
867 };
868
869 validate_file_metadata_size(
870 file_name,
871 file_info.as_ref(),
872 self.dest_encryption.as_ref()
873 )?;
874
875 Ok(CopyFile {
876 source_file_id,
877 destination_bucket_id: self.destination_bucket_id,
878 file_name,
879 range: self.range,
880 metadata_directive,
881 content_type: self.content_type,
882 file_info,
883 file_retention: self.file_retention,
884 legal_hold: self.legal_hold,
885 source_encryption: self.source_encryption,
886 dest_encryption: self.dest_encryption,
887 })
888 }
889}
890
891pub async fn copy_file<'a, C, E>(
899 auth: &mut Authorization<C>,
900 file: CopyFile<'_>
901) -> Result<File, Error<E>>
902 where C: HttpClient<Error=Error<E>>,
903 E: fmt::Debug + fmt::Display,
904{
905 require_capability!(auth, Capability::WriteFiles);
906 if file.file_retention.is_some() {
907 require_capability!(auth, Capability::WriteFileRetentions);
908 }
909 if file.legal_hold.is_some() {
910 require_capability!(auth, Capability::WriteFileLegalHolds);
911 }
912 if file.dest_encryption.is_some() {
913 require_capability!(auth, Capability::WriteBucketEncryption);
914 }
915
916 let res = auth.client.post(auth.api_url("b2_copy_file"))
917 .expect("Invalid URL")
918 .with_header("Authorization", &auth.authorization_token).unwrap()
919 .with_body_json(serde_json::to_value(file)?)
920 .send().await?;
921
922 let file: B2Result<File> = serde_json::from_slice(&res)?;
923 file.into()
924}
925
926#[derive(Debug, Serialize)]
928#[serde(rename_all = "camelCase")]
929pub struct CopyFilePart<'a> {
930 source_file_id: &'a str,
931 large_file_id: &'a str,
932 part_number: u16,
933 #[serde(skip_serializing_if = "Option::is_none")]
934 range: Option<ByteRange>,
935 #[serde(skip_serializing_if = "Option::is_none")]
936 source_server_side_encryption: Option<&'a ServerSideEncryption>,
937 #[serde(skip_serializing_if = "Option::is_none")]
938 destination_server_side_encryption: Option<&'a ServerSideEncryption>,
939}
940
941impl<'a> CopyFilePart<'a> {
942 pub fn builder() -> CopyFilePartBuilder<'a> {
943 CopyFilePartBuilder::default()
944 }
945}
946
947#[derive(Default)]
949pub struct CopyFilePartBuilder<'a> {
950 source_file: Option<&'a str>,
951 large_file: Option<&'a str>,
952 part_number: Option<u16>,
953 range: Option<ByteRange>,
954 source_encryption: Option<&'a ServerSideEncryption>,
955 dest_encryption: Option<&'a ServerSideEncryption>,
956}
957
958impl<'a> CopyFilePartBuilder<'a> {
959 pub fn source_file(mut self, file: &'a File) -> Self {
961 self.source_file = Some(&file.file_id);
962 self.source_encryption = file.server_side_encryption.as_ref();
963 self
964 }
965
966 pub fn source_file_id(mut self, file: &'a str) -> Self {
968 self.source_file = Some(file);
969 self
970 }
971
972 pub fn destination_large_file(mut self, file: &'a File) -> Self {
974 self.large_file = Some(&file.file_id);
975
976 if let Some(enc) = self.dest_encryption {
977 if ! matches!(enc, ServerSideEncryption::NoEncryption) {
978 self.dest_encryption = Some(enc);
979 }
980 }
981
982 self
983 }
984
985 pub fn destination_large_file_id(mut self, file: &'a str) -> Self {
987 self.large_file = Some(file);
988 self
989 }
990
991 pub fn part_number(mut self, part_num: u16) -> Result<Self, ValidationError>
995 {
996 #[allow(clippy::manual_range_contains)]
997 if part_num < 1 || part_num > 10000 {
998 return Err(ValidationError::OutOfBounds(format!(
999 "part_num must be between 1 and 10,000 inclusive. Was {}",
1000 part_num
1001 )));
1002 }
1003
1004 self.part_number = Some(part_num);
1005 Ok(self)
1006 }
1007
1008 pub fn range(mut self, range: ByteRange) -> Self {
1012 self.range = Some(range);
1013 self
1014 }
1015
1016 pub fn source_encryption_settings(mut self, enc: &'a ServerSideEncryption)
1020 -> Self {
1021 self.source_encryption = Some(enc);
1022 self
1023 }
1024
1025 pub fn destination_encryption_settings(
1029 mut self,
1030 enc: &'a ServerSideEncryption
1031 ) -> Self {
1032 self.dest_encryption = Some(enc);
1033 self
1034 }
1035
1036 pub fn build(self) -> Result<CopyFilePart<'a>, ValidationError> {
1038 let source_file_id = self.source_file.ok_or_else(||
1039 ValidationError::MissingData("source_file is required".into())
1040 )?;
1041
1042 let large_file_id = self.large_file.ok_or_else(||
1043 ValidationError::MissingData(
1044 "destination_large_file is required".into()
1045 )
1046 )?;
1047
1048 let part_number = self.part_number.ok_or_else(||
1049 ValidationError::MissingData("part_number is required".into())
1050 )?;
1051
1052 Ok(CopyFilePart {
1053 source_file_id,
1054 large_file_id,
1055 part_number,
1056 range: self.range,
1057 source_server_side_encryption: self.source_encryption,
1058 destination_server_side_encryption: self.dest_encryption,
1059 })
1060 }
1061}
1062
1063pub async fn copy_file_part<C, E>(
1068 auth: &mut Authorization<C>,
1069 file_part: CopyFilePart<'_>
1070) -> Result<FilePart, Error<E>>
1071 where C: HttpClient<Error=Error<E>>,
1072 E: fmt::Debug + fmt::Display,
1073{
1074 require_capability!(auth, Capability::WriteFiles);
1075
1076 let res = auth.client.post(auth.api_url("b2_copy_part"))
1077 .expect("Invalid URL")
1078 .with_header("Authorization", &auth.authorization_token).unwrap()
1079 .with_body_json(serde_json::to_value(file_part)?)
1080 .send().await?;
1081
1082 let part: B2Result<FilePart> = serde_json::from_slice(&res)?;
1083 part.into()
1084}
1085
1086#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1091#[serde(rename_all = "camelCase")]
1092pub enum BypassGovernance { Yes, No }
1093
1094pub async fn delete_file_version<C, E>(
1102 auth: &mut Authorization<C>,
1103 file: File,
1104 bypass_governance: BypassGovernance,
1105) -> Result<DeletedFile, Error<E>>
1106 where C: HttpClient<Error=Error<E>>,
1107 E: fmt::Debug + fmt::Display,
1108{
1109 delete_file_version_by_name_id(
1110 auth,
1111 &file.file_name,
1112 &file.file_id,
1113 bypass_governance
1114 ).await
1115}
1116
1117pub async fn download_file_headers<C, E>(
1123 auth: &mut Authorization<C>,
1124 file: &File
1125) -> Result<HeaderMap, Error<E>>
1126 where C: HttpClient<Error=Error<E>>,
1127 E: fmt::Debug + fmt::Display,
1128{
1129 download_file_headers_by_id(auth, &file.file_id).await
1130}
1131
1132pub async fn download_file_headers_by_id<C, E>(
1138 auth: &mut Authorization<C>,
1139 file_id: impl AsRef<str>
1140) -> Result<HeaderMap, Error<E>>
1141 where C: HttpClient<Error=Error<E>>,
1142 E: fmt::Debug + fmt::Display,
1143{
1144 require_capability!(auth, Capability::ReadFiles);
1149
1150 let res = auth.client.head(
1151 format!("{}?fileId={}",
1152 auth.download_url("b2_download_file_by_id"),
1153 file_id.as_ref()
1154 )
1155 )
1156 .expect("Invalid URL")
1157 .with_header("Authorization", &auth.authorization_token).unwrap()
1158 .send_keep_headers().await?;
1159
1160 Ok(res.1)
1161}
1162
1163#[derive(Debug)]
1164enum FileHandle<'a> {
1165 Id(&'a str),
1166 Name((String, &'a str)), }
1168
1169#[derive(Debug, Serialize)]
1182#[serde(rename_all = "camelCase")]
1183pub struct DownloadFile<'a> {
1184 #[serde(skip_serializing)]
1185 file: FileHandle<'a>,
1186 #[serde(skip_serializing)]
1187 range: Option<ByteRange>,
1188 #[serde(skip_serializing_if = "Option::is_none")]
1189 b2_content_disposition: Option<&'a str>,
1190 #[serde(skip_serializing_if = "Option::is_none")]
1191 b2_content_language: Option<&'a str>,
1192 #[serde(skip_serializing_if = "Option::is_none")]
1193 b2_expires: Option<String>,
1194 #[serde(skip_serializing_if = "Option::is_none")]
1195 b2_cache_control: Option<String>,
1196 #[serde(skip_serializing_if = "Option::is_none")]
1197 b2_content_encoding: Option<String>,
1198 #[serde(skip_serializing_if = "Option::is_none")]
1199 b2_content_type: Option<String>,
1200 #[serde(skip_serializing)]
1201 encryption: Option<ServerSideEncryption>,
1202}
1203
1204impl<'a> DownloadFile<'a> {
1205 pub fn with_id(id: &'a str) -> Self {
1207 Self {
1208 file: FileHandle::Id(id),
1209 range: None,
1210 b2_content_disposition: None,
1211 b2_content_language: None,
1212 b2_expires: None,
1213 b2_cache_control: None,
1214 b2_content_encoding: None,
1215 b2_content_type: None,
1216 encryption: None,
1217 }
1218 }
1219
1220 pub fn with_name(name: &str, bucket: &'a str) -> Self {
1224 Self {
1225 file: FileHandle::Name((percent_encode!(name), bucket)),
1226 range: None,
1227 b2_content_disposition: None,
1228 b2_content_language: None,
1229 b2_expires: None,
1230 b2_cache_control: None,
1231 b2_content_encoding: None,
1232 b2_content_type: None,
1233 encryption: None,
1234 }
1235 }
1236
1237 pub fn builder() -> DownloadFileBuilder<'a> {
1238 DownloadFileBuilder::default()
1239 }
1240
1241 pub fn public_url<'b, C, D, E>(&self, auth: D) -> String
1250 where C: HttpClient<Error=Error<E>> + 'b,
1251 D: Into<&'b DownloadAuth<'b, C>>,
1252 E: fmt::Debug + fmt::Display,
1253 {
1254 let auth = auth.into();
1255
1256 match &self.file {
1257 FileHandle::Id(id) => format!(
1258 "{}?fileId={}",
1259 auth.download_url("b2_download_file_by_id"),
1260 id
1261 ),
1262 FileHandle::Name((name, bucket)) => format!(
1263 "{}/file/{}/{}?",
1264 auth.download_get_url(),
1265 bucket,
1266 name
1267 ),
1268 }
1269 }
1270}
1271
1272#[derive(Default)]
1273pub struct DownloadFileBuilder<'a> {
1274 file: Option<FileHandle<'a>>,
1275 range: Option<ByteRange>,
1276 content_disposition: Option<&'a str>,
1277 content_language: Option<&'a str>,
1278 expires: Option<String>,
1279 cache_control: Option<String>,
1280 content_encoding: Option<String>,
1281 content_type: Option<String>,
1282 encryption: Option<ServerSideEncryption>,
1283}
1284
1285impl<'a> DownloadFileBuilder<'a> {
1286 pub fn file_name(mut self, name: &str, bucket: &'a str) -> Self {
1293 self.file = Some(FileHandle::Name((percent_encode!(name), bucket)));
1294 self
1295 }
1296
1297 pub fn file_id(mut self, id: &'a str) -> Self {
1302 self.file = Some(FileHandle::Id(id));
1303 self
1304 }
1305
1306 pub fn range(mut self, range: ByteRange) -> Self {
1314 self.range = Some(range);
1315 self
1316 }
1317
1318 pub fn content_disposition(mut self, disposition: &'a ContentDisposition)
1324 -> Result<Self, ValidationError> {
1325 validate_content_disposition(&disposition.0, false)?;
1326 self.content_disposition = Some(&disposition.0);
1327 Ok(self)
1328 }
1329
1330 pub fn content_language(mut self, language: &'a str) -> Self {
1336 self.content_language = Some(language);
1338 self
1339 }
1340
1341 pub fn expiration(mut self, expiration: Expires) -> Self {
1346 self.expires = Some(expiration.value().to_string());
1347 self
1348 }
1349
1350 pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
1355 self.cache_control = Some(cache_control.value().to_string());
1356 self
1357 }
1358
1359 pub fn content_encoding(mut self, encoding: ContentEncoding) -> Self {
1365 self.content_encoding = Some(format!("{}", encoding.encoding()));
1366 self
1367 }
1368
1369 pub fn content_type(mut self, content_type: impl Into<Mime>) -> Self {
1374 self.content_type = Some(content_type.into().to_string());
1375 self
1376 }
1377
1378 pub fn encryption_settings(mut self, settings: ServerSideEncryption)
1382 -> Self {
1383 self.encryption = Some(settings);
1384 self
1385 }
1386
1387 pub fn build(self) -> Result<DownloadFile<'a>, ValidationError> {
1389 let file = self.file.ok_or_else(|| ValidationError::MissingData(
1390 "Must specify the file to download".into()
1391 ))?;
1392
1393 Ok(DownloadFile {
1394 file,
1395 range: self.range,
1396 b2_content_disposition: self.content_disposition,
1397 b2_content_language: self.content_language,
1398 b2_expires: self.expires,
1399 b2_cache_control: self.cache_control,
1400 b2_content_encoding: self.content_encoding,
1401 b2_content_type: self.content_type,
1402 encryption: self.encryption,
1403 })
1404 }
1405}
1406
1407pub enum DownloadAuth<'a, C>
1411 where C: HttpClient
1412{
1413 Auth(&'a mut Authorization<C>),
1414 Download(&'a mut DownloadAuthorization<C>),
1415}
1416
1417impl<'a, C> DownloadAuth<'a, C>
1418 where C: HttpClient,
1419{
1420 fn download_get_url(&self) -> &str {
1421 match self {
1422 Self::Auth(auth) => auth.download_get_url(),
1423 Self::Download(auth) => &auth.download_url,
1424 }
1425 }
1426
1427 fn download_url(&self, endpoint: impl AsRef<str>) -> String {
1428 match self {
1429 Self::Auth(auth) => auth.download_url(endpoint),
1430 Self::Download(auth) =>
1431 format!("{}/b2api/v2/{}", auth.download_url, endpoint.as_ref())
1432 }
1433 }
1434
1435 fn authorization_token(&self) -> &str {
1436 match self {
1437 Self::Auth(auth) => &auth.authorization_token,
1438 Self::Download(auth) => &auth.authorization_token,
1439 }
1440 }
1441
1442 fn has_capability(&self, cap: Capability) -> bool {
1443 match self {
1444 Self::Auth(auth) => auth.has_capability(cap),
1445 _ => true,
1446 }
1447 }
1448}
1449
1450impl<'a, C> From<&'a mut Authorization<C>> for DownloadAuth<'a, C>
1451 where C: HttpClient,
1452{
1453 fn from(auth: &'a mut Authorization<C>) -> Self {
1454 Self::Auth(auth)
1455 }
1456}
1457
1458impl<'a, C> From<&'a mut DownloadAuthorization<C>> for DownloadAuth<'a, C>
1459 where C: HttpClient,
1460{
1461 fn from(auth: &'a mut DownloadAuthorization<C>) -> Self {
1462 Self::Download(auth)
1463 }
1464}
1465
1466pub async fn download_file<'a, C, E>(
1474 auth: impl Into<DownloadAuth<'a, C>>,
1475 file: DownloadFile<'_>
1476) -> Result<(Vec<u8>, HeaderMap), Error<E>>
1477 where C: HttpClient<Error=Error<E>> + 'a,
1478 E: fmt::Debug + fmt::Display,
1479{
1480 match file.file {
1481 FileHandle::Id(_) => {
1482 match auth.into() {
1483 DownloadAuth::Auth(auth) => download_file_by_id(auth, file)
1484 .await,
1485 _ => Err(Error::MissingAuthorization),
1486 }
1487 },
1488 FileHandle::Name(_) => download_file_by_name(auth, file).await
1489 }
1490}
1491
1492async fn download_file_by_id<C, E>(
1493 auth: &mut Authorization<C>,
1494 file: DownloadFile<'_>
1495) -> Result<(Vec<u8>, HeaderMap), Error<E>>
1496 where C: HttpClient<Error=Error<E>>,
1497 E: fmt::Debug + fmt::Display,
1498{
1499 require_capability!(auth, Capability::ReadFiles);
1504
1505 let file_id = match file.file {
1506 FileHandle::Id(id) => id,
1507 FileHandle::Name(_) => panic!("Call download_file_by_name() instead"),
1508 };
1509
1510 let mut file_req = serde_json::to_value(&file)?;
1511 file_req["fileId"] = serde_json::Value::String(file_id.into());
1512
1513 let mut req = auth.client.post(auth.download_url("b2_download_file_by_id"))
1514 .expect("Invalid URL")
1515 .with_header("Authorization", &auth.authorization_token).unwrap()
1516 .with_body_json(file_req);
1517
1518 if let Some(range) = file.range {
1519 req = req.with_header("Range", &range.to_string())?;
1520 }
1521
1522 if let Some(ServerSideEncryption::SelfManaged(enc)) = file.encryption {
1523 req = req
1524 .with_header(
1525 "X-Bz-Server-Side-Encryption-Customer-Algorithm",
1526 &enc.algorithm.to_string()
1527 )?
1528 .with_header(
1529 "X-Bz-Server-Side-Encryption-Customer-Key",
1530 &enc.key
1531 )?
1532 .with_header(
1533 "X-Bz-Server-Side-Encryption-Customer-Key-Md5",
1534 &enc.digest
1535 )?;
1536 }
1537
1538 let (body, headers) = req.send_keep_headers().await?;
1539
1540 let res: Result<B2Error, _> = serde_json::from_slice(&body);
1543 match res {
1544 Ok(e) => Err(e.into()),
1545 Err(_) => Ok((body, headers)),
1546 }
1547}
1548
1549async fn download_file_by_name<'a, C, E>(
1550 auth: impl Into<DownloadAuth<'a, C>>,
1551 file: DownloadFile<'_>
1552) -> Result<(Vec<u8>, HeaderMap), Error<E>>
1553 where C: HttpClient<Error=Error<E>> + 'a,
1554 E: fmt::Debug + fmt::Display,
1555{
1556 let mut auth = auth.into();
1557
1558 require_capability!(auth, Capability::ReadFiles);
1563 assert!(matches!(file.file, FileHandle::Name(_)));
1564
1565 let mut url = file.public_url(&auth).to_owned();
1566
1567 macro_rules! add_param {
1568 ($str:ident, $name:literal, $obj:expr) => {
1569 $str.push_str($name);
1570 $str.push('=');
1571 $str.push_str($obj);
1572 $str.push('&'); };
1574 }
1575
1576 macro_rules! add_opt_param {
1577 ($str:ident, $name:literal, $obj:expr) => {
1578 if let Some(s) = $obj {
1579 add_param!($str, $name, &s);
1580 }
1581 };
1582 }
1583
1584 add_opt_param!(url, "b2ContentDisposition", file.b2_content_disposition);
1585 add_opt_param!(url, "b2ContentLanguage", file.b2_content_language);
1586 add_opt_param!(url, "b2Expires", file.b2_expires);
1587 add_opt_param!(url, "b2CacheControl", file.b2_cache_control);
1588 add_opt_param!(url, "b2ContentEncoding", file.b2_content_encoding);
1589 add_opt_param!(url, "b2ContentType", file.b2_content_type);
1590
1591 if let Some(ServerSideEncryption::SelfManaged(enc)) = file.encryption {
1592 add_param!(url,
1593 "X-Bz-Server-Side-Encryption-Customer-Algorithm",
1594 &enc.algorithm.to_string()
1595 );
1596 add_param!(url,
1597 "X-Bz-Server-Side-Encryption-Customer-Key",
1598 &enc.key
1599 );
1600 add_param!(url,
1601 "X-Bz-Server-Side-Encryption-Customer-Key-Md5",
1602 &enc.digest
1603 );
1604 }
1605
1606 let auth_token = auth.authorization_token().to_owned();
1607
1608 let client = match auth {
1609 DownloadAuth::Auth(ref mut auth) => &mut auth.client,
1610 DownloadAuth::Download(ref mut auth) => &mut auth.client,
1611 };
1612
1613 let mut req = client.get(url)
1614 .expect("Invalid URL")
1615 .with_header("Authorization", &auth_token).unwrap();
1616
1617 if let Some(range) = file.range {
1618 req = req.with_header("Range", &range.to_string())?
1619 }
1620
1621 let (body, headers) = req.send_keep_headers().await?;
1622
1623 let res: Result<B2Error, _> = serde_json::from_slice(&body);
1626 match res {
1627 Ok(e) => Err(e.into()),
1628 Err(_) => Ok((body, headers)),
1629 }
1630}
1631
1632pub async fn delete_file_version_by_name_id<C, E>(
1640 auth: &mut Authorization<C>,
1641 file_name: impl AsRef<str>,
1642 file_id: impl AsRef<str>,
1643 bypass_governance: BypassGovernance,
1644) -> Result<DeletedFile, Error<E>>
1645 where C: HttpClient<Error=Error<E>>,
1646 E: fmt::Debug + fmt::Display,
1647{
1648 require_capability!(auth, Capability::DeleteFiles);
1649
1650 let mut body = serde_json::json!({
1651 "fileName": &file_name.as_ref(),
1652 "fileId": &file_id.as_ref(),
1653 });
1654
1655 if matches!(bypass_governance, BypassGovernance::Yes) {
1656 require_capability!(auth, Capability::BypassGovernance);
1657 body["bypassGovernance"] = serde_json::Value::Bool(true);
1658 }
1659
1660 let res = auth.client.post(auth.api_url("b2_delete_file_version"))
1661 .expect("Invalid URL")
1662 .with_header("Authorization", &auth.authorization_token).unwrap()
1663 .with_body_json(body)
1664 .send().await?;
1665
1666 let file: B2Result<DeletedFile> = serde_json::from_slice(&res)?;
1667 file.into()
1668}
1669
1670pub async fn finish_large_file_upload<C, E>(
1680 auth: &mut Authorization<C>,
1681 file: &File,
1682 sha1_checksums: &[String],
1683) -> Result<File, Error<E>>
1684 where C: HttpClient<Error=Error<E>>,
1685 E: fmt::Debug + fmt::Display,
1686{
1687 finish_large_file_upload_by_id(auth, &file.file_id, sha1_checksums).await
1688}
1689
1690pub async fn finish_large_file_upload_by_id<C, E>(
1694 auth: &mut Authorization<C>,
1695 file_id: impl AsRef<str>,
1696 sha1_checksums: &[String],
1697) -> Result<File, Error<E>>
1698 where C: HttpClient<Error=Error<E>>,
1699 E: fmt::Debug + fmt::Display,
1700{
1701 use serde_json::json;
1702
1703 require_capability!(auth, Capability::WriteFiles);
1704
1705 let res = auth.client.post(auth.api_url("b2_finish_large_file"))
1706 .expect("Invalid URL")
1707 .with_header("Authorization", &auth.authorization_token).unwrap()
1708 .with_body_json(json!( {
1709 "fileId": file_id.as_ref(),
1710 "partSha1Array": &sha1_checksums,
1711 }))
1712 .send().await?;
1713
1714 let file: B2Result<File> = serde_json::from_slice(&res)?;
1715 file.into()
1716}
1717
1718pub async fn get_file_info<C, E>(
1728 auth: &mut Authorization<C>,
1729 file_id: impl AsRef<str>
1730) -> Result<File, Error<E>>
1731 where C: HttpClient<Error=Error<E>>,
1732 E: fmt::Debug + fmt::Display,
1733{
1734 use serde_json::json;
1735
1736 require_capability!(auth, Capability::ReadFiles);
1737
1738 let res = auth.client.post(auth.api_url("b2_get_file_info"))
1739 .expect("Invalid URL")
1740 .with_header("Authorization", &auth.authorization_token).unwrap()
1741 .with_body_json(json!({
1742 "fileId": file_id.as_ref(),
1743 }))
1744 .send().await?;
1745
1746 let file_info: B2Result<File> = serde_json::from_slice(&res)?;
1747 match file_info {
1748 B2Result::Ok(mut info) => {
1749 if let Some(sha1) = &info.content_sha1 {
1750 if sha1 == "none" {
1751 info.content_sha1 = None;
1752 }
1753 }
1754
1755 Ok(info)
1756 },
1757 B2Result::Err(e) => Err(e.into()),
1758 }
1759}
1760
1761#[derive(Debug, Serialize)]
1767#[serde(rename_all = "camelCase")]
1768pub struct DownloadAuthorizationRequest<'a> {
1769 bucket_id: &'a str,
1770 file_name_prefix: &'a str,
1771 valid_duration_in_seconds: Duration,
1772 #[serde(skip_serializing_if = "Option::is_none")]
1773 b2_content_disposition: Option<String>,
1774 #[serde(skip_serializing_if = "Option::is_none")]
1775 b2_content_language: Option<String>,
1776 #[serde(skip_serializing_if = "Option::is_none")]
1777 b2_expires: Option<String>,
1778 #[serde(skip_serializing_if = "Option::is_none")]
1779 b2_cache_control: Option<String>,
1780 #[serde(skip_serializing_if = "Option::is_none")]
1781 b2_content_encoding: Option<String>,
1782 #[serde(skip_serializing_if = "Option::is_none")]
1783 b2_content_type: Option<String>,
1784}
1785
1786impl<'a> DownloadAuthorizationRequest<'a> {
1787 pub fn builder() -> DownloadAuthorizationRequestBuilder<'a> {
1788 DownloadAuthorizationRequestBuilder::default()
1789 }
1790}
1791
1792#[derive(Default)]
1802pub struct DownloadAuthorizationRequestBuilder<'a> {
1803 bucket_id: Option<&'a str>,
1805 file_name_prefix: Option<&'a str>,
1806 valid_duration_in_seconds: Option<Duration>,
1807 b2_content_disposition: Option<String>,
1809 b2_content_language: Option<String>,
1810 b2_expires: Option<String>,
1811 b2_cache_control: Option<String>,
1812 b2_content_encoding: Option<String>,
1813 b2_content_type: Option<String>,
1814}
1815
1816impl<'a> DownloadAuthorizationRequestBuilder<'a> {
1817 pub fn bucket_id(mut self, id: &'a str) -> Self {
1819 self.bucket_id = Some(id);
1820 self
1821 }
1822
1823 pub fn file_name_prefix(mut self, name: &'a str)
1826 -> Result<Self, FileNameValidationError> {
1827
1828 self.file_name_prefix = Some(validated_file_name(name)?);
1829 Ok(self)
1830 }
1831
1832 pub fn duration(mut self, dur: chrono::Duration)
1837 -> Result<Self, ValidationError> {
1838 if dur < chrono::Duration::seconds(1)
1839 || dur > chrono::Duration::weeks(1)
1840 {
1841 return Err(ValidationError::OutOfBounds(
1842 "Duration must be between 1 and 604,800 seconds, inclusive"
1843 .into()
1844 ));
1845 }
1846
1847 self.valid_duration_in_seconds = Some(Duration(dur));
1848 Ok(self)
1849 }
1850
1851 pub fn content_disposition(mut self, disposition: ContentDisposition)
1855 -> Self {
1856 self.b2_content_disposition = Some(disposition.0);
1857 self
1858 }
1859
1860 pub fn content_language<S: Into<String>>(mut self, lang: S) -> Self {
1863 self.b2_content_language = Some(lang.into());
1865 self
1866 }
1867
1868 pub fn expiration(mut self, expiration: Expires) -> Self {
1870 self.b2_expires = Some(expiration.value().to_string());
1871 self
1872 }
1873
1874 pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
1876 self.b2_cache_control = Some(cache_control.value().to_string());
1877 self
1878 }
1879
1880 pub fn content_encoding(mut self, encoding: ContentEncoding) -> Self {
1882 self.b2_content_encoding = Some(format!("{}", encoding.encoding()));
1883 self
1884 }
1885
1886 pub fn content_type(mut self, content_type: impl Into<Mime>) -> Self {
1888 self.b2_content_type = Some(content_type.into().to_string());
1889 self
1890 }
1891
1892 pub fn build(self)
1894 -> Result<DownloadAuthorizationRequest<'a>, ValidationError> {
1895 let bucket_id = self.bucket_id
1896 .ok_or_else(|| ValidationError::MissingData(
1897 "A bucket ID must be provided".into()
1898 ))?;
1899 let file_name_prefix = self.file_name_prefix
1900 .ok_or_else(|| ValidationError::MissingData(
1901 "A filename prefix must be provided".into()
1902 ))?;
1903 let valid_duration_in_seconds = self.valid_duration_in_seconds
1904 .ok_or_else(|| ValidationError::MissingData(
1905 "The duration of the authorization token must be set".into()
1906 ))?;
1907
1908 Ok(DownloadAuthorizationRequest {
1909 bucket_id,
1910 file_name_prefix,
1911 valid_duration_in_seconds,
1912 b2_content_disposition: self.b2_content_disposition,
1913 b2_content_language: self.b2_content_language,
1914 b2_expires: self.b2_expires,
1915 b2_cache_control: self.b2_cache_control,
1916 b2_content_encoding: self.b2_content_encoding,
1917 b2_content_type: self.b2_content_type,
1918 })
1919 }
1920}
1921
1922#[derive(Debug)]
1924#[allow(dead_code)]
1925pub struct DownloadAuthorization<C>
1926 where C: HttpClient,
1927{
1928 client: C,
1929 api_url: String,
1930 download_url: String,
1931
1932 bucket_id: String,
1933 file_name_prefix: String,
1934 authorization_token: String,
1935}
1936
1937impl<C> DownloadAuthorization<C>
1938 where C: HttpClient + Clone,
1939{
1940 pub fn bucket_id(&self) -> &str { &self.bucket_id }
1942 pub fn file_name_prefix(&self) -> &str { &self.file_name_prefix }
1945
1946 fn from_proto(
1947 proto: ProtoDownloadAuthorization,
1948 auth: &Authorization<C>,
1949 ) -> Self {
1950 Self {
1951 client: auth.client.clone(),
1952 api_url: auth.api_url.clone(),
1953 download_url: auth.download_url.clone(),
1954 bucket_id: proto.bucket_id,
1955 file_name_prefix: proto.file_name_prefix,
1956 authorization_token: proto.authorization_token,
1957 }
1958 }
1959}
1960
1961#[derive(Debug, Deserialize)]
1962#[serde(rename_all = "camelCase")]
1963struct ProtoDownloadAuthorization {
1964 bucket_id: String,
1965 file_name_prefix: String,
1966 authorization_token: String,
1967}
1968
1969pub async fn get_download_authorization<'a, C, E>(
2009 auth: &mut Authorization<C>,
2010 download_req: DownloadAuthorizationRequest<'_>
2011) -> Result<DownloadAuthorization<C>, Error<E>>
2012 where C: HttpClient<Error=Error<E>>,
2013 E: fmt::Debug + fmt::Display,
2014{
2015 require_capability!(auth, Capability::ShareFiles);
2016
2017 let res = auth.client.post(auth.api_url("b2_get_download_authorization"))
2018 .expect("Invalid URL")
2019 .with_header("Authorization", &auth.authorization_token).unwrap()
2020 .with_body_json(serde_json::to_value(download_req)?)
2021 .send().await?;
2022
2023 let proto_auth: B2Result<ProtoDownloadAuthorization> =
2024 serde_json::from_slice(&res)?;
2025
2026 proto_auth.map(|a| DownloadAuthorization::from_proto(a, auth)).into()
2027}
2028
2029#[derive(Deserialize)]
2031#[allow(dead_code)]
2032#[serde(rename_all = "camelCase")]
2033pub struct UploadPartAuthorization<'a, 'b, C, E>
2034 where C: HttpClient<Error=Error<E>>,
2035 E: fmt::Debug + fmt::Display,
2036{
2037 #[serde(skip_deserializing)]
2038 #[serde(default = "make_none")]
2039 auth: Option<&'a mut Authorization<C>>,
2040 #[serde(skip_deserializing)]
2041 #[serde(default = "make_none")]
2042 encryption: Option<&'b ServerSideEncryption>,
2043 file_id: String,
2044 upload_url: String,
2045 authorization_token: String,
2046}
2047
2048fn make_none<T>() -> Option<T> { None }
2049
2050pub async fn get_upload_part_authorization<'a, 'b, C, E>(
2067 auth: &'a mut Authorization<C>,
2068 file: &'b File,
2069) -> Result<UploadPartAuthorization<'a, 'b, C, E>, Error<E>>
2070 where C: HttpClient<Error=Error<E>>,
2071 E: fmt::Debug + fmt::Display,
2072{
2073 get_upload_part_authorization_by_id(
2074 auth,
2075 &file.file_id,
2076 file.server_side_encryption.as_ref()
2077 ).await
2078}
2079
2080pub async fn get_upload_part_authorization_by_id<'a, 'b, C, E>(
2085 auth: &'a mut Authorization<C>,
2086 file_id: impl AsRef<str>,
2087 encryption: Option<&'b ServerSideEncryption>,
2088) -> Result<UploadPartAuthorization<'a, 'b, C, E>, Error<E>>
2089 where C: HttpClient<Error=Error<E>>,
2090 E: fmt::Debug + fmt::Display,
2091{
2092 use serde_json::json;
2093
2094 require_capability!(auth, Capability::WriteFiles);
2095
2096 let res = auth.client.post(auth.api_url("b2_get_upload_part_url"))
2097 .expect("Invalid URL")
2098 .with_header("Authorization", &auth.authorization_token).unwrap()
2099 .with_body_json(json!({ "fileId": file_id.as_ref() }))
2100 .send().await?;
2101
2102 let upload_auth: B2Result<UploadPartAuthorization<'_, '_, _, _>> =
2103 serde_json::from_slice(&res)?;
2104
2105 upload_auth.map(move |mut a| {
2106 a.auth = Some(auth);
2107 a.encryption = encryption;
2108 a
2109 }).into()
2110}
2111
2112#[derive(Deserialize)]
2114#[allow(dead_code)]
2115#[serde(rename_all = "camelCase")]
2116pub struct UploadAuthorization<'a, C, E>
2117 where C: HttpClient<Error=Error<E>>,
2118 E: fmt::Debug + fmt::Display,
2119{
2120 #[serde(skip_deserializing)]
2121 #[serde(default = "make_none")]
2122 auth: Option<&'a mut Authorization<C>>,
2123 bucket_id: String,
2124 upload_url: String,
2125 authorization_token: String,
2126}
2127
2128impl<'a, C, E> UploadAuthorization<'a, C, E>
2129 where C: HttpClient<Error=Error<E>>,
2130 E: fmt::Debug + fmt::Display,
2131{
2132 pub fn bucket_id(&self) -> &str { &self.bucket_id }
2133}
2134
2135pub async fn get_upload_authorization<'a, 'b, C, E>(
2152 auth: &'a mut Authorization<C>,
2153 bucket: &'b Bucket,
2154) -> Result<UploadAuthorization<'a, C, E>, Error<E>>
2155 where C: HttpClient<Error=Error<E>>,
2156 E: fmt::Debug + fmt::Display,
2157{
2158 get_upload_authorization_by_id(auth, &bucket.bucket_id).await
2159}
2160
2161pub async fn get_upload_authorization_by_id<'a, 'b, C, E>(
2166 auth: &'a mut Authorization<C>,
2167 bucket_id: impl AsRef<str>,
2168) -> Result<UploadAuthorization<'a, C, E>, Error<E>>
2169 where C: HttpClient<Error=Error<E>>,
2170 E: fmt::Debug + fmt::Display,
2171{
2172 use serde_json::json;
2173
2174 require_capability!(auth, Capability::WriteFiles);
2175
2176 let res = auth.client.post(auth.api_url("b2_get_upload_url"))
2177 .expect("Invalid URL")
2178 .with_header("Authorization", &auth.authorization_token).unwrap()
2179 .with_body_json(json!({ "bucketId": bucket_id.as_ref() }))
2180 .send().await?;
2181
2182 let upload_auth: B2Result<UploadAuthorization<'_, _, _>> =
2183 serde_json::from_slice(&res)?;
2184
2185 upload_auth.map(move |mut a| { a.auth = Some(auth); a }).into()
2186}
2187
2188pub async fn hide_file<C, E>(auth: &mut Authorization<C>, file: &File)
2203-> Result<File, Error<E>>
2204 where C: HttpClient<Error=Error<E>>,
2205 E: fmt::Debug + fmt::Display,
2206{
2207 hide_file_by_name(auth, &file.bucket_id, &file.file_name).await
2208}
2209
2210pub async fn hide_file_by_name<C, E>(
2225 auth: &mut Authorization<C>,
2226 bucket_id: impl AsRef<str>,
2227 file_name: impl AsRef<str>,
2228) -> Result<File, Error<E>>
2229 where C: HttpClient<Error=Error<E>>,
2230 E: fmt::Debug + fmt::Display,
2231{
2232 use serde_json::json;
2233
2234 require_capability!(auth, Capability::WriteFiles);
2235
2236 let res = auth.client.post(auth.api_url("b2_hide_file"))
2237 .expect("Invalid URL")
2238 .with_header("Authorization", &auth.authorization_token).unwrap()
2239 .with_body_json(json!({
2240 "bucketId": bucket_id.as_ref(),
2241 "fileName": file_name.as_ref(),
2242 }))
2243 .send().await?;
2244
2245 let file: B2Result<File> = serde_json::from_slice(&res)?;
2246 file.into()
2247}
2248
2249#[derive(Debug, Serialize)]
2251#[serde(rename_all = "camelCase")]
2252#[allow(dead_code)]
2253pub struct ListFileNames<'a> {
2254 bucket_id: &'a str,
2255 start_file_name: Option<String>,
2256 max_file_count: Option<u16>,
2257 prefix: Option<&'a str>,
2258 delimiter: Option<char>,
2259}
2260
2261impl<'a> ListFileNames<'a> {
2262 pub fn builder() -> ListFileNamesBuilder<'a> {
2263 ListFileNamesBuilder::default()
2264 }
2265}
2266
2267#[derive(Default)]
2269pub struct ListFileNamesBuilder<'a> {
2270 bucket_id: Option<&'a str>,
2271 start_file_name: Option<String>,
2272 max_file_count: Option<u16>,
2273 prefix: Option<&'a str>,
2274 delimiter: Option<char>,
2275}
2276
2277impl<'a> ListFileNamesBuilder<'a> {
2278 pub fn bucket_id(mut self, id: &'a str) -> Self {
2280 self.bucket_id = Some(id);
2281 self
2282 }
2283
2284 pub fn start_file_name(mut self, file_name: impl Into<String>) -> Self {
2286 self.start_file_name = Some(file_name.into());
2287 self
2288 }
2289
2290 pub fn max_file_count(mut self, count: u16) -> Self {
2300 use std::cmp::Ord as _;
2301
2302 self.max_file_count = Some(count.clamp(1, 10_000));
2303 self
2304 }
2305
2306 pub fn prefix(mut self, prefix: &'a str)
2314 -> Result<Self, FileNameValidationError> {
2315 self.prefix = Some(validated_file_name(prefix)?);
2316 Ok(self)
2317 }
2318
2319 pub fn delimiter(mut self, delimiter: char)
2325 -> Result<Self, FileNameValidationError> {
2326 if delimiter.is_ascii_control() {
2330 Err(FileNameValidationError::InvalidChar(delimiter))
2331 } else {
2332 self.delimiter = Some(delimiter);
2333 Ok(self)
2334 }
2335 }
2336
2337 pub fn build(self) -> Result<ListFileNames<'a>, MissingData> {
2341 let bucket_id = self.bucket_id.ok_or_else(||
2342 MissingData::new("bucket_id")
2343 )?;
2344
2345 Ok(ListFileNames {
2346 bucket_id,
2347 start_file_name: self.start_file_name,
2348 max_file_count: self.max_file_count,
2349 prefix: self.prefix,
2350 delimiter: self.delimiter,
2351 })
2352 }
2353}
2354
2355#[derive(Deserialize)]
2356#[serde(rename_all = "camelCase")]
2357struct FileNameList {
2358 files: Vec<File>,
2359 next_file_name: Option<String>,
2360}
2361
2362#[allow(clippy::needless_lifetimes)] pub async fn list_file_names<'a, C, E>(
2369 auth: &mut Authorization<C>,
2370 request: ListFileNames<'a>,
2371) -> Result<(Vec<File>, Option<ListFileNames<'a>>), Error<E>>
2372 where C: HttpClient<Error=Error<E>>,
2373 E: fmt::Debug + fmt::Display,
2374{
2375 require_capability!(auth, Capability::ListFiles);
2376
2377 let res = auth.client.post(auth.api_url("b2_list_file_names"))
2378 .expect("Invalid URL")
2379 .with_header("Authorization", &auth.authorization_token).unwrap()
2380 .with_body_json(serde_json::to_value(&request)?)
2381 .send().await?;
2382
2383 let files: B2Result<FileNameList> = serde_json::from_slice(&res)?;
2384 match files {
2385 B2Result::Ok(files) => {
2386 if let Some(next_file) = files.next_file_name {
2387 let mut request = request;
2388 request.start_file_name = Some(next_file);
2389
2390 Ok((files.files, Some(request)))
2391 } else {
2392 Ok((files.files, None))
2393 }
2394 },
2395 B2Result::Err(e) => Err(e.into()),
2396 }
2397}
2398
2399#[derive(Debug, Serialize)]
2401#[serde(rename_all = "camelCase")]
2402#[allow(dead_code)]
2403pub struct ListFileVersions<'a> {
2404 bucket_id: &'a str,
2405 start_file_name: Option<String>,
2406 start_file_id: Option<String>,
2407 max_file_count: Option<u16>,
2408 prefix: Option<&'a str>,
2409 delimiter: Option<char>,
2410}
2411
2412impl<'a> ListFileVersions<'a> {
2413 pub fn builder() -> ListFileVersionsBuilder<'a> {
2414 ListFileVersionsBuilder::default()
2415 }
2416}
2417
2418#[derive(Default)]
2420pub struct ListFileVersionsBuilder<'a> {
2421 bucket_id: Option<&'a str>,
2422 start_file_name: Option<String>,
2423 start_file_id: Option<String>,
2424 max_file_count: Option<u16>,
2425 prefix: Option<&'a str>,
2426 delimiter: Option<char>,
2427}
2428
2429impl<'a> ListFileVersionsBuilder<'a> {
2430 pub fn bucket_id(mut self, id: &'a str) -> Self {
2432 self.bucket_id = Some(id);
2433 self
2434 }
2435
2436 pub fn start_file_name(mut self, file_name: impl Into<String>) -> Self {
2441 self.start_file_name = Some(file_name.into());
2442 self
2443 }
2444
2445 pub fn start_file_id(mut self, file_id: impl Into<String>) -> Self {
2449 self.start_file_id = Some(file_id.into());
2450 self
2451 }
2452
2453 pub fn max_file_count(mut self, count: u16) -> Self {
2463 use std::cmp::Ord as _;
2464
2465 self.max_file_count = Some(count.clamp(1, 10_000));
2466 self
2467 }
2468
2469 pub fn prefix(mut self, prefix: &'a str)
2477 -> Result<Self, FileNameValidationError> {
2478 self.prefix = Some(validated_file_name(prefix)?);
2479 Ok(self)
2480 }
2481
2482 pub fn delimiter(mut self, delimiter: char)
2488 -> Result<Self, FileNameValidationError> {
2489 if delimiter.is_ascii_control() {
2493 Err(FileNameValidationError::InvalidChar(delimiter))
2494 } else {
2495 self.delimiter = Some(delimiter);
2496 Ok(self)
2497 }
2498 }
2499
2500 pub fn build(self) -> Result<ListFileVersions<'a>, MissingData> {
2504 let bucket_id = self.bucket_id.ok_or_else(||
2505 MissingData::new("bucket_id")
2506 )?;
2507
2508 if self.start_file_id.is_some() && self.start_file_name.is_none() {
2509 return Err(MissingData::new("start_file_name")
2510 .with_message(
2511 "If start_file_id is specified, start_file_name is required"
2512 )
2513 );
2514 }
2515
2516 Ok(ListFileVersions {
2517 bucket_id,
2518 start_file_name: self.start_file_name,
2519 start_file_id: self.start_file_id,
2520 max_file_count: self.max_file_count,
2521 prefix: self.prefix,
2522 delimiter: self.delimiter,
2523 })
2524 }
2525}
2526
2527#[derive(Deserialize)]
2528#[serde(rename_all = "camelCase")]
2529struct FileVersionList {
2530 files: Vec<File>,
2531 next_file_name: Option<String>,
2532 next_file_id: Option<String>,
2533}
2534
2535#[allow(clippy::needless_lifetimes)] pub async fn list_file_versions<'a, C, E>(
2544 auth: &mut Authorization<C>,
2545 request: ListFileVersions<'a>,
2546) -> Result<(Vec<File>, Option<ListFileVersions<'a>>), Error<E>>
2547 where C: HttpClient<Error=Error<E>>,
2548 E: fmt::Debug + fmt::Display,
2549{
2550 require_capability!(auth, Capability::ListFiles);
2551
2552 let res = auth.client.post(auth.api_url("b2_list_file_versions"))
2553 .expect("Invalid URL")
2554 .with_header("Authorization", &auth.authorization_token).unwrap()
2555 .with_body_json(serde_json::to_value(&request)?)
2556 .send().await?;
2557
2558 let files: B2Result<FileVersionList> = serde_json::from_slice(&res)?;
2559 match files {
2560 B2Result::Ok(files) => {
2561 let mut request = request;
2562
2563 if files.next_file_name.is_some() {
2564 request.start_file_name = files.next_file_name;
2565 request.start_file_id = files.next_file_id;
2566
2567 Ok((files.files, Some(request)))
2568 } else {
2569 Ok((files.files, None))
2570 }
2571 },
2572 B2Result::Err(e) => Err(e.into()),
2573 }
2574}
2575
2576#[derive(Debug, Serialize)]
2577#[serde(rename_all = "camelCase")]
2578pub struct ListFileParts<'a> {
2579 file_id: &'a str,
2580 start_part_number: Option<u16>,
2581 max_part_count: Option<u16>,
2582}
2583
2584impl<'a> ListFileParts<'a> {
2585 pub fn builder() -> ListFilePartsBuilder<'a> {
2586 ListFilePartsBuilder::default()
2587 }
2588}
2589
2590#[derive(Default)]
2591pub struct ListFilePartsBuilder<'a> {
2592 file_id: Option<&'a str>,
2593 start_part_number: Option<u16>,
2594 max_part_count: Option<u16>,
2595}
2596
2597impl<'a> ListFilePartsBuilder<'a> {
2598 pub fn file(mut self, file: &'a File) -> Self {
2600 self.file_id = Some(&file.file_id);
2601 self
2602 }
2603
2604 pub fn file_id(mut self, id: &'a str) -> Self {
2606 self.file_id = Some(id);
2607 self
2608 }
2609
2610 pub fn start_part_number(mut self, num: u16) -> Self {
2612 self.start_part_number = Some(num);
2613 self
2614 }
2615
2616 pub fn max_part_count(mut self, count: u16) -> Self {
2623 use std::cmp::Ord as _;
2624
2625 self.max_part_count = Some(count.clamp(1, 1_000));
2626 self
2627 }
2628
2629 pub fn build(self) -> Result<ListFileParts<'a>, MissingData> {
2630 let file_id = self.file_id.ok_or_else(||
2631 MissingData::new("file_id")
2632 )?;
2633
2634 Ok(ListFileParts {
2635 file_id,
2636 start_part_number: self.start_part_number,
2637 max_part_count: self.max_part_count,
2638 })
2639 }
2640}
2641
2642#[derive(Deserialize)]
2643#[serde(rename_all = "camelCase")]
2644struct FilePartList {
2645 parts: Vec<FilePart>,
2646 next_part_number: Option<u16>,
2647}
2648
2649#[allow(clippy::needless_lifetimes)] pub async fn list_file_parts<'a, C, E>(
2652 auth: &mut Authorization<C>,
2653 request: ListFileParts<'a>,
2654) -> Result<(Vec<FilePart>, Option<ListFileParts<'a>>), Error<E>>
2655 where C: HttpClient<Error=Error<E>>,
2656 E: fmt::Debug + fmt::Display,
2657{
2658 require_capability!(auth, Capability::WriteFiles);
2659
2660 let res = auth.client.post(auth.api_url("b2_list_parts"))
2661 .expect("Invalid URL")
2662 .with_header("Authorization", &auth.authorization_token).unwrap()
2663 .with_body_json(serde_json::to_value(&request)?)
2664 .send().await?;
2665
2666 let parts: B2Result<FilePartList> = serde_json::from_slice(&res)?;
2667 match parts {
2668 B2Result::Ok(parts) => {
2669 if let Some(next_part) = parts.next_part_number {
2670 let mut request = request;
2671 request.start_part_number = Some(next_part);
2672
2673 Ok((parts.parts, Some(request)))
2674 } else {
2675 Ok((parts.parts, None))
2676 }
2677 },
2678 B2Result::Err(e) => Err(e.into()),
2679 }
2680}
2681
2682#[derive(Debug, Serialize)]
2683#[serde(rename_all = "camelCase")]
2684#[allow(dead_code)]
2685pub struct ListUnfinishedLargeFiles<'a> {
2686 bucket_id: &'a str,
2687 name_prefix: Option<&'a str>,
2688 start_file_id: Option<String>,
2689 max_file_count: Option<u16>,
2690}
2691
2692impl<'a> ListUnfinishedLargeFiles<'a> {
2693 pub fn builder() -> ListUnfinishedLargeFilesBuilder<'a> {
2694 ListUnfinishedLargeFilesBuilder::default()
2695 }
2696}
2697
2698#[derive(Default)]
2699pub struct ListUnfinishedLargeFilesBuilder<'a> {
2700 bucket_id: Option<&'a str>,
2701 name_prefix: Option<&'a str>,
2702 start_file_id: Option<String>,
2703 max_file_count: Option<u16>,
2704}
2705
2706impl<'a> ListUnfinishedLargeFilesBuilder<'a> {
2707 pub fn bucket_id(mut self, id: &'a str) -> Self {
2709 self.bucket_id = Some(id);
2710 self
2711 }
2712
2713 pub fn prefix(mut self, prefix: &'a str)
2717 -> Result<Self, FileNameValidationError> {
2718 self.name_prefix = Some(validated_file_name(prefix)?);
2719 Ok(self)
2720 }
2721
2722 pub fn start_file_id(mut self, file_id: impl Into<String>) -> Self {
2724 self.start_file_id = Some(file_id.into());
2725 self
2726 }
2727
2728 pub fn max_file_count(mut self, count: u16) -> Self {
2735 use std::cmp::Ord as _;
2736
2737 self.max_file_count = Some(count.clamp(1, 10_000));
2738 self
2739 }
2740
2741 pub fn build(self) -> Result<ListUnfinishedLargeFiles<'a>, MissingData> {
2743 let bucket_id = self.bucket_id.ok_or_else(||
2744 MissingData::new("bucket_id")
2745 )?;
2746
2747 Ok(ListUnfinishedLargeFiles {
2748 bucket_id,
2749 name_prefix: self.name_prefix,
2750 start_file_id: self.start_file_id,
2751 max_file_count: self.max_file_count,
2752 })
2753 }
2754}
2755
2756#[derive(Deserialize)]
2757#[serde(rename_all = "camelCase")]
2758struct FileIdList {
2759 files: Vec<File>,
2760 next_file_id: Option<String>,
2761}
2762
2763#[allow(clippy::needless_lifetimes)] pub async fn list_unfinished_large_files<'a, C, E>(
2772 auth: &mut Authorization<C>,
2773 request: ListUnfinishedLargeFiles<'a>
2774) -> Result<(Vec<File>, Option<ListUnfinishedLargeFiles<'a>>), Error<E>>
2775 where C: HttpClient<Error=Error<E>>,
2776 E: fmt::Debug + fmt::Display,
2777{
2778 require_capability!(auth, Capability::ListFiles);
2779
2780 let res = auth.client.post(auth.api_url("b2_list_unfinished_large_files"))
2781 .expect("Invalid URL")
2782 .with_header("Authorization", &auth.authorization_token).unwrap()
2783 .with_body_json(serde_json::to_value(&request)?)
2784 .send().await?;
2785
2786 let files: B2Result<FileIdList> = serde_json::from_slice(&res)?;
2787 match files {
2788 B2Result::Ok(files) => {
2789 if let Some(next_file_id) = files.next_file_id {
2790 let mut request = request;
2791 request.start_file_id = Some(next_file_id);
2792
2793 Ok((files.files, Some(request)))
2794 } else {
2795 Ok((files.files, None))
2796 }
2797 },
2798 B2Result::Err(e) => Err(e.into()),
2799 }
2800}
2801
2802#[derive(Debug, Serialize)]
2804#[serde(rename_all = "camelCase")]
2805pub struct StartLargeFile<'a> {
2806 bucket_id: &'a str,
2807 file_name: String,
2808 content_type: String,
2809 #[serde(skip_serializing_if = "Option::is_none")]
2810 file_info: Option<serde_json::Value>,
2811 #[serde(skip_serializing_if = "Option::is_none")]
2812 file_retention: Option<FileRetentionPolicy>,
2813 #[serde(skip_serializing_if = "Option::is_none")]
2814 legal_hold: Option<LegalHoldValue>,
2815 #[serde(skip_serializing_if = "Option::is_none")]
2816 server_side_encryption: Option<ServerSideEncryption>,
2817}
2818
2819impl<'a> StartLargeFile<'a> {
2820 pub fn builder() -> StartLargeFileBuilder<'a> {
2821 StartLargeFileBuilder::default()
2822 }
2823}
2824
2825#[derive(Debug, Default)]
2827pub struct StartLargeFileBuilder<'a> {
2828 bucket_id: Option<&'a str>,
2829 file_name: Option<String>,
2830 content_type: Option<String>,
2831 file_info: Option<serde_json::Value>,
2832 file_retention: Option<FileRetentionPolicy>,
2833 legal_hold: Option<LegalHoldValue>,
2834 server_side_encryption: Option<ServerSideEncryption>,
2835
2836 last_modified: Option<i64>,
2838 sha1_checksum: Option<&'a str>,
2839 content_disposition: Option<String>,
2840 content_language: Option<String>,
2841 expires: Option<String>,
2842 cache_control: Option<String>,
2843 content_encoding: Option<String>,
2844}
2845
2846impl<'a> StartLargeFileBuilder<'a> {
2847 pub fn bucket_id(mut self, id: &'a str) -> Self {
2849 self.bucket_id = Some(id);
2850 self
2851 }
2852
2853 pub fn file_name(mut self, name: impl AsRef<str>)
2857 -> Result<Self, FileNameValidationError> {
2858 let name = validated_file_name(name.as_ref())?;
2859
2860 self.file_name = Some(percent_encode!(name));
2861 Ok(self)
2862 }
2863
2864 pub fn content_type(mut self, mime: impl Into<String>) -> Self {
2868 self.content_type = Some(mime.into());
2872 self
2873 }
2874
2875 pub fn file_info(mut self, info: serde_json::Value)
2892 -> Result<Self, ValidationError> {
2893 self.file_info = Some(validated_file_info(info)?);
2894 Ok(self)
2895 }
2896
2897 pub fn file_retention(mut self, policy: FileRetentionPolicy) -> Self {
2899 self.file_retention = Some(policy);
2900 self
2901 }
2902
2903 pub fn with_legal_hold(mut self) -> Self {
2905 self.legal_hold = Some(LegalHoldValue::On);
2906 self
2907 }
2908
2909 pub fn without_legal_hold(mut self) -> Self {
2911 self.legal_hold = Some(LegalHoldValue::Off);
2912 self
2913 }
2914
2915 pub fn encryption_settings(mut self, settings: ServerSideEncryption) -> Self
2917 {
2918 self.server_side_encryption = Some(settings);
2919 self
2920 }
2921
2922 pub fn last_modified(mut self, time: chrono::DateTime<chrono::Utc>) -> Self
2924 {
2925 self.last_modified = Some(time.timestamp_millis());
2926 self
2927 }
2928
2929 pub fn sha1_checksum(mut self, checksum: &'a str) -> Self {
2934 self.sha1_checksum = Some(checksum);
2935 self
2936 }
2937
2938 pub fn content_disposition(mut self, disposition: ContentDisposition)
2945 -> Result<Self, ValidationError> {
2946 validate_content_disposition(&disposition.0, false)?;
2947
2948 self.content_disposition = Some(percent_encode!(disposition.0));
2949 Ok(self)
2950 }
2951
2952 pub fn content_language(mut self, language: impl Into<String>) -> Self {
2957 self.content_language = Some(percent_encode!(language.into()));
2959 self
2960 }
2961
2962 pub fn expiration(mut self, expiration: Expires) -> Self {
2966 let expires = percent_encode!(expiration.value().to_string());
2967
2968 self.expires = Some(expires);
2969 self
2970 }
2971
2972 pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
2978 self.cache_control = Some(cache_control.value().to_string());
2979 self
2980 }
2981
2982 pub fn content_encoding(mut self, encoding: ContentEncoding) -> Self {
2987 let encoding = percent_encode!(format!("{}", encoding.encoding()));
2988 self.content_encoding = Some(encoding);
2989 self
2990 }
2991
2992 pub fn build(self) -> Result<StartLargeFile<'a>, ValidationError> {
2993 let bucket_id = self.bucket_id.ok_or_else(||
2994 ValidationError::MissingData(
2995 "The bucket ID in which to store the file must be present"
2996 .into()
2997 )
2998 )?;
2999
3000 let file_name = self.file_name.ok_or_else(||
3001 ValidationError::MissingData(
3002 "The file name must be specified".into()
3003 )
3004 )?;
3005
3006 let content_type = self.content_type
3007 .unwrap_or_else(|| "b2/x-auto".into());
3008
3009 let file_info = if let Some(mut file_info) = self.file_info {
3010 let info_map = file_info.as_object_mut()
3011 .expect("file_info is not a JSON object");
3012
3013 add_file_info!(info_map, "src_last_modified_millis",
3014 self.last_modified.map(|v| v.to_string()));
3015 add_file_info!(info_map, "large_file_sha1", self.sha1_checksum);
3016 add_file_info!(info_map, "b2-content-disposition",
3017 self.content_disposition);
3018 add_file_info!(info_map, "b2-content-language",
3019 self.content_language);
3020 add_file_info!(info_map, "b2-expires", self.expires);
3021 add_file_info!(info_map, "b2-cache-control", self.cache_control);
3022 add_file_info!(info_map, "b2-content-encoding",
3023 self.content_encoding);
3024
3025 Some(file_info)
3026 } else {
3027 None
3028 };
3029
3030 validate_file_metadata_size(
3031 &file_name,
3032 file_info.as_ref(),
3033 self.server_side_encryption.as_ref()
3034 )?;
3035
3036 Ok(StartLargeFile {
3037 bucket_id,
3038 file_name,
3039 content_type,
3040 file_info,
3041 file_retention: self.file_retention,
3042 legal_hold: self.legal_hold,
3043 server_side_encryption: self.server_side_encryption,
3044 })
3045 }
3046}
3047
3048pub async fn start_large_file<'a, C, E>(
3072 auth: &mut Authorization<C>,
3073 file: StartLargeFile<'_>
3074) -> Result<File, Error<E>>
3075 where C: HttpClient<Error=Error<E>>,
3076 E: fmt::Debug + fmt::Display,
3077{
3078 require_capability!(auth, Capability::WriteFiles);
3079 if file.file_retention.is_some() {
3080 require_capability!(auth, Capability::WriteFileRetentions);
3081 }
3082 if file.legal_hold.is_some() {
3083 require_capability!(auth, Capability::WriteFileLegalHolds);
3084 }
3085 if file.server_side_encryption.is_some() {
3086 require_capability!(auth, Capability::WriteBucketEncryption);
3087 }
3088
3089 let res = auth.client.post(auth.api_url("b2_start_large_file"))
3090 .expect("Invalid URL")
3091 .with_header("Authorization", &auth.authorization_token).unwrap()
3092 .with_body_json(serde_json::to_value(file)?)
3093 .send().await?;
3094
3095 let file: B2Result<File> = serde_json::from_slice(&res)?;
3096 file.into()
3097}
3098
3099#[derive(Serialize, Deserialize)]
3101#[serde(rename_all = "camelCase")]
3102pub struct UpdateFileLegalHold<'a> {
3103 file_name: &'a str,
3104 file_id: &'a str,
3105 legal_hold: LegalHoldValue,
3106}
3107
3108impl<'a> UpdateFileLegalHold<'a> {
3109 pub fn enable_for(file: &'a File) -> Self {
3111 Self {
3112 file_name: &file.file_name,
3113 file_id: &file.file_id,
3114 legal_hold: LegalHoldValue::On,
3115 }
3116 }
3117
3118 pub fn disable_for(file: &'a File) -> Self {
3120 Self {
3121 file_name: &file.file_name,
3122 file_id: &file.file_id,
3123 legal_hold: LegalHoldValue::Off,
3124 }
3125 }
3126
3127 pub fn builder() -> UpdateFileLegalHoldBuilder<'a> {
3129 UpdateFileLegalHoldBuilder::default()
3130 }
3131}
3132
3133#[derive(Default)]
3135pub struct UpdateFileLegalHoldBuilder<'a> {
3136 file_name: Option<&'a str>,
3137 file_id: Option<&'a str>,
3138 legal_hold: Option<LegalHoldValue>,
3139}
3140
3141impl<'a> UpdateFileLegalHoldBuilder<'a> {
3142 pub fn file(mut self, file: &'a File) -> Self {
3144 self.file_name = Some(&file.file_name);
3145 self.file_id = Some(&file.file_id);
3146 self
3147 }
3148
3149 pub fn file_name(mut self, file_name: &'a str)
3153 -> Result<Self, FileNameValidationError> {
3154 self.file_name = Some(validated_file_name(file_name)?);
3155 Ok(self)
3156 }
3157
3158 pub fn file_id(mut self, file_id: &'a str) -> Self {
3162 self.file_id = Some(file_id);
3163 self
3164 }
3165
3166 pub fn with_legal_hold(mut self) -> Self {
3168 self.legal_hold = Some(LegalHoldValue::On);
3169 self
3170 }
3171
3172 pub fn without_legal_hold(mut self) -> Self {
3174 self.legal_hold = Some(LegalHoldValue::Off);
3175 self
3176 }
3177
3178 pub fn build(self) -> Result<UpdateFileLegalHold<'a>, MissingData> {
3183 let file_name = self.file_name.ok_or_else(||
3184 MissingData::new("file_name")
3185 )?;
3186 let file_id = self.file_id.ok_or_else(||
3187 MissingData::new("file_id")
3188 )?;
3189 let legal_hold = self.legal_hold.ok_or_else(||
3190 MissingData::new("legal_hold")
3191 )?;
3192
3193 Ok(UpdateFileLegalHold {
3194 file_name,
3195 file_id,
3196 legal_hold,
3197 })
3198 }
3199}
3200
3201pub async fn update_file_legal_hold<C, E>(
3205 auth: &mut Authorization<C>,
3206 file_update: UpdateFileLegalHold<'_>
3207) -> Result<(), Error<E>>
3208 where C: HttpClient<Error=Error<E>>,
3209 E: fmt::Debug + fmt::Display,
3210{
3211 require_capability!(auth, Capability::WriteFileLegalHolds);
3212
3213 let res = auth.client.post(auth.api_url("b2_update_file_legal_hold"))
3214 .expect("Invalid URL")
3215 .with_header("Authorization", &auth.authorization_token).unwrap()
3216 .with_body_json(serde_json::to_value(file_update)?)
3217 .send().await?;
3218
3219 let res: B2Result<UpdateFileLegalHold> = serde_json::from_slice(&res)?;
3220 res.map(|_| ()).into()
3221}
3222
3223#[derive(Debug, Serialize, Deserialize)]
3225#[serde(rename_all = "camelCase")]
3226pub struct UpdateFileRetention<'a> {
3227 file_name: &'a str,
3228 file_id: &'a str,
3229 file_retention: FileRetentionSetting,
3230 #[serde(skip_serializing_if = "Option::is_none")]
3231 bypass_governance: Option<BypassGovernance>,
3232}
3233
3234impl<'a> UpdateFileRetention<'a> {
3235 pub fn builder() -> UpdateFileRetentionBuilder<'a> {
3236 UpdateFileRetentionBuilder::default()
3237 }
3238}
3239
3240#[derive(Default)]
3242pub struct UpdateFileRetentionBuilder<'a> {
3243 file_name: Option<&'a str>,
3244 file_id: Option<&'a str>,
3245 file_retention: Option<FileRetentionSetting>,
3246 bypass_governance: Option<BypassGovernance>,
3247}
3248
3249impl<'a> UpdateFileRetentionBuilder<'a> {
3250 pub fn file(mut self, file: &'a File) -> Self {
3252 self.file_name = Some(&file.file_name);
3253 self.file_id = Some(&file.file_id);
3254 self
3255 }
3256
3257 pub fn file_name(mut self, file_name: &'a str)
3261 -> Result<Self, FileNameValidationError> {
3262 self.file_name = Some(validated_file_name(file_name)?);
3263 Ok(self)
3264 }
3265
3266 pub fn file_id(mut self, file_id: &'a str) -> Self {
3270 self.file_id = Some(file_id);
3271 self
3272 }
3273
3274 pub fn file_retention(mut self, retention: FileRetentionSetting) -> Self {
3280 self.file_retention = Some(retention);
3281 self
3282 }
3283
3284 pub fn bypass_governance(mut self) -> Self {
3289 self.bypass_governance = Some(BypassGovernance::Yes);
3290 self
3291 }
3292
3293 pub fn build(self) -> Result<UpdateFileRetention<'a>, MissingData> {
3295 let file_name = self.file_name.ok_or_else(||
3296 MissingData::new("file_name")
3297 )?;
3298 let file_id = self.file_id.ok_or_else(||
3299 MissingData::new("file_id")
3300 )?;
3301 let file_retention = self.file_retention.ok_or_else(||
3302 MissingData::new("file_retention")
3303 )?;
3304
3305 Ok(UpdateFileRetention {
3306 file_name,
3307 file_id,
3308 file_retention,
3309 bypass_governance: self.bypass_governance,
3310 })
3311 }
3312}
3313
3314pub async fn update_file_retention<C, E>(
3330 auth: &mut Authorization<C>,
3331 retention_update: UpdateFileRetention<'_>,
3332) -> Result<(), Error<E>>
3333 where C: HttpClient<Error=Error<E>>,
3334 E: fmt::Debug + fmt::Display,
3335{
3336 require_capability!(auth, Capability::WriteFileRetentions);
3337 if matches!(retention_update.bypass_governance, Some(BypassGovernance::Yes))
3338 {
3339 require_capability!(auth, Capability::BypassGovernance);
3340 }
3341
3342 let res = auth.client.post(auth.api_url("b2_update_file_retention"))
3343 .expect("Invalid URL")
3344 .with_header("Authorization", &auth.authorization_token).unwrap()
3345 .with_body_json(serde_json::to_value(retention_update)?)
3346 .send().await?;
3347
3348 let res: B2Result<UpdateFileRetention> = serde_json::from_slice(&res)?;
3349 res.map(|_| ()).into()
3350}
3351
3352pub struct UploadFile<'a> {
3356 file_name: String,
3357 content_type: String,
3358 sha1_checksum: &'a str,
3359 file_info: Option<serde_json::Value>,
3360 legal_hold: Option<LegalHoldValue>,
3361 file_retention: Option<(FileRetentionMode, i64)>,
3362 encryption: Option<ServerSideEncryption>,
3363}
3364
3365impl<'a> UploadFile<'a> {
3366 pub fn builder() -> UploadFileBuilder<'a> {
3367 UploadFileBuilder::default()
3368 }
3369}
3370
3371#[derive(Default)]
3384pub struct UploadFileBuilder<'a> {
3385 file_name: Option<String>,
3386 content_type: Option<String>,
3387 sha1_checksum: Option<&'a str>,
3388 last_modified: Option<i64>,
3389 file_info: Option<serde_json::Value>,
3390
3391 content_disposition: Option<String>,
3393 content_language: Option<String>,
3394 expires: Option<String>,
3395 cache_control: Option<String>,
3396 content_encoding: Option<String>,
3397
3398 legal_hold: Option<LegalHoldValue>,
3399 file_retention_mode: Option<FileRetentionMode>,
3400 file_retention_time: Option<i64>,
3401 encryption: Option<ServerSideEncryption>,
3402}
3403
3404impl<'a> UploadFileBuilder<'a> {
3405 pub fn file_name(mut self, name: impl AsRef<str>)
3409 -> Result<Self, FileNameValidationError> {
3410 let name = validated_file_name(name.as_ref())?;
3411
3412 self.file_name = Some(percent_encode!(name));
3413 Ok(self)
3414 }
3415
3416 pub fn content_type(mut self, content_type: impl Into<Mime>) -> Self {
3428 self.content_type = Some(content_type.into().to_string());
3429 self
3430 }
3431
3432 pub fn sha1_checksum(mut self, checksum: &'a str) -> Self {
3437 self.sha1_checksum = Some(checksum);
3438 self
3439 }
3440
3441 pub fn last_modified(mut self, time: chrono::DateTime<chrono::Utc>) -> Self
3443 {
3444 self.last_modified = Some(time.timestamp_millis());
3445 self
3446 }
3447
3448 pub fn content_disposition(mut self, disposition: ContentDisposition)
3453 -> Result<Self, ValidationError> {
3454 validate_content_disposition(&disposition.0, false)?;
3455
3456 self.content_disposition = Some(percent_encode!(disposition.0));
3457 Ok(self)
3458 }
3459
3460 pub fn content_language(mut self, language: impl Into<String>) -> Self {
3465 self.content_language = Some(percent_encode!(language.into()));
3467 self
3468 }
3469
3470 pub fn expiration(mut self, expiration: Expires) -> Self {
3474 let expires = percent_encode!(expiration.value().to_string());
3475
3476 self.expires = Some(expires);
3477 self
3478 }
3479
3480 pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
3486 self.cache_control = Some(cache_control.value().to_string());
3487 self
3488 }
3489
3490 pub fn content_encoding(mut self, encoding: ContentEncoding) -> Self {
3495 let encoding = percent_encode!(format!("{}", encoding.encoding()));
3496 self.content_encoding = Some(encoding);
3497 self
3498 }
3499
3500 pub fn file_info(mut self, info: serde_json::Value)
3520 -> Result<Self, ValidationError> {
3521 let mut file_info = validated_file_info(info)?;
3522
3523 if let Some(map) = file_info.as_object_mut() {
3524 let mut key_updates = vec![];
3525
3526 for key in map.keys() {
3527 if ! key.starts_with("X-Bz-Info-") {
3528 key_updates.push(key.to_owned());
3529 }
3530 }
3531
3532 for old_key in key_updates.into_iter() {
3533 let val = map.remove(&old_key).unwrap();
3534 let mut new_key = String::from("X-Bz-Info-");
3535 new_key.push_str(&old_key);
3536
3537 map.insert(new_key, val);
3538 }
3539 }
3540
3541 self.file_info = Some(file_info);
3542 Ok(self)
3543 }
3544
3545 pub fn with_legal_hold(mut self) -> Self {
3547 self.legal_hold = Some(LegalHoldValue::On);
3548 self
3549 }
3550
3551 pub fn without_legal_hold(mut self) -> Self {
3553 self.legal_hold = Some(LegalHoldValue::Off);
3554 self
3555 }
3556
3557 pub fn file_retention_mode(mut self, mode: FileRetentionMode) -> Self {
3562 self.file_retention_mode = Some(mode);
3563 self
3564 }
3565
3566 pub fn retain_until(mut self, time: chrono::DateTime<chrono::Utc>)
3571 -> Self {
3572 self.file_retention_time = Some(time.timestamp_millis());
3573 self
3574 }
3575
3576 pub fn encryption_settings(mut self, settings: ServerSideEncryption)
3578 -> Self {
3579 self.encryption = Some(settings);
3580 self
3581 }
3582
3583 pub fn build(self) -> Result<UploadFile<'a>, ValidationError> {
3585 let file_name = self.file_name.ok_or_else(||
3586 ValidationError::MissingData("Filename is required".into())
3587 )?;
3588
3589 let content_type = self.content_type
3590 .unwrap_or_else(|| "b2/x-auto".into());
3591
3592 let sha1_checksum = self.sha1_checksum.unwrap_or("do_not_verify");
3593
3594 if self.file_retention_mode.is_some()
3595 ^ self.file_retention_time.is_some()
3596 {
3597 return Err(ValidationError::BadFormat(
3598 "File retention policy is not fully configured".into()
3599 ));
3600 }
3601
3602 let file_info = if let Some(mut file_info) = self.file_info {
3603 let info_map = file_info.as_object_mut()
3604 .expect("file_info is not a JSON object");
3605
3606 add_file_info!(info_map, "X-Bz-info-src_last_modified_millis",
3607 self.last_modified.map(|v| v.to_string()));
3608 add_file_info!(info_map, "X-Bz-info-b2-content-disposition",
3609 self.content_disposition);
3610 add_file_info!(info_map, "X-Bz-info-b2-content-language",
3611 self.content_language);
3612 add_file_info!(info_map, "X-Bz-info-b2-expires", self.expires);
3613 add_file_info!(info_map, "X-Bz-info-b2-content-encoding",
3614 self.content_encoding);
3615
3616 Some(file_info)
3617 } else {
3618 None
3619 };
3620
3621 validate_file_metadata_size(
3622 &file_name,
3623 file_info.as_ref(),
3624 self.encryption.as_ref()
3625 )?;
3626
3627 let file_retention = self.file_retention_mode
3628 .zip(self.file_retention_time);
3629
3630 Ok(UploadFile {
3631 file_name,
3632 content_type,
3633 sha1_checksum,
3634 file_info,
3635 legal_hold: self.legal_hold,
3636 file_retention,
3637 encryption: self.encryption,
3638 })
3639 }
3640}
3641
3642pub async fn upload_file<C, E>(
3647 auth: &mut UploadAuthorization<'_, C, E>,
3648 upload: UploadFile<'_>,
3649 data: &[u8],
3650) -> Result<File, Error<E>>
3651 where C: HttpClient<Error=Error<E>>,
3652 E: fmt::Debug + fmt::Display,
3653{
3654 let inner_auth = auth.auth.as_mut().unwrap();
3658
3659 require_capability!(inner_auth, Capability::WriteFiles);
3660
3661 if upload.file_retention.is_some() {
3662 require_capability!(inner_auth, Capability::WriteFileRetentions);
3665 }
3666
3667 let mut req = inner_auth.client.post(&auth.upload_url)
3668 .expect("Invalid URL")
3669 .with_header("Authorization", &auth.authorization_token)?
3670 .with_header("X-Bz-File-Name", &upload.file_name)?
3671 .with_header("Content-Type", &upload.content_type)?
3672 .with_header("Content-Length", &data.len().to_string())?
3673 .with_header("X-Bz-Content-Sha1", upload.sha1_checksum)?;
3674
3675 if let Some(mut file_info) = upload.file_info {
3676 let info_map = file_info.as_object_mut()
3677 .expect("file_info is not a JSON object");
3678
3679 macro_rules! add_metadata_header {
3680 ($header_name:literal) => {
3681 if let Some(val) = info_map.remove($header_name) {
3682 req = req.with_header($header_name, val.as_str().unwrap())?
3683 }
3684 };
3685 }
3686
3687 add_metadata_header!("X-Bz-Info-src_last_modified_millis");
3688 add_metadata_header!("X-Bz-Info-b2-content-disposition");
3689 add_metadata_header!("X-Bz-Info-b2-content-language");
3690 add_metadata_header!("X-Bz-Info-b2-expires");
3691 add_metadata_header!("X-Bz-Info-b2-cache-control");
3692 add_metadata_header!("X-Bz-Info-content-encoding");
3693
3694 for (key, val) in info_map.into_iter() {
3695 req = req.with_header(key, &val.to_string())?;
3696 }
3697 }
3698
3699 if let Some(legal_hold) = upload.legal_hold {
3700 req = req.with_header("X-Bz-File-Legal-Hold", &legal_hold.to_string())?;
3701 }
3702
3703 if let Some((mode, timestamp)) = upload.file_retention {
3704 req = req
3705 .with_header("X-Bz-File-Retention-Mode", &mode.to_string())?
3706 .with_header("X-Bz-File-Retention-Retain-Until-Timestamp",
3707 ×tamp.to_string())?;
3708 }
3709
3710 if let Some(enc) = upload.encryption {
3711 if let Some(headers) = enc.to_headers() {
3712 for (header, value) in headers.into_iter() {
3713 req = req.with_header(header, &value)?;
3714 }
3715 }
3716 }
3717
3718 let res = req.with_body(data).send().await?;
3719
3720 let file: B2Result<File> = serde_json::from_slice(&res)?;
3721 file.into()
3722}
3723
3724#[derive(Clone)]
3726pub struct UploadFilePart<'a> {
3727 part_number: u16,
3728 content_sha1: &'a str,
3729 encryption: Option<ServerSideEncryption>,
3730}
3731
3732impl<'a> UploadFilePart<'a> {
3733 pub fn builder() -> UploadFilePartBuilder<'a> {
3734 UploadFilePartBuilder::default()
3735 }
3736
3737 pub fn create_next_part(mut self, sha1_checksum: Option<&'a str>)
3739 -> Result<Self, ValidationError> {
3740 self.content_sha1 = sha1_checksum.unwrap_or("do_not_verify");
3741
3742 if self.part_number < 10_000 {
3743 self.part_number += 1;
3744 Ok(self)
3745 } else {
3746 Err(ValidationError::OutOfBounds(
3747 "The maximum part number is 10,000.".into()
3748 ))
3749 }
3750 }
3751}
3752
3753pub struct UploadFilePartBuilder<'a> {
3755 part_number: u16,
3756 content_sha1: &'a str,
3757 encryption: Option<ServerSideEncryption>,
3758}
3759
3760impl<'a> Default for UploadFilePartBuilder<'a> {
3761 fn default() -> Self {
3762 Self {
3763 part_number: 1,
3764 content_sha1: "do_not_verify",
3765 encryption: None,
3766 }
3767 }
3768}
3769
3770impl<'a> UploadFilePartBuilder<'a> {
3771 pub fn part_number(mut self, num: u16) -> Self {
3776 use std::cmp::Ord as _;
3777
3778 self.part_number = num.clamp(1, 10_000);
3779 self
3780 }
3781
3782 pub fn part_sha1_checksum(mut self, sha1: &'a str) -> Self {
3789 self.content_sha1 = sha1;
3790 self
3791 }
3792
3793 pub fn server_side_encryption(mut self, encryption: ServerSideEncryption)
3797 -> Self {
3798 self.encryption = Some(encryption);
3799 self
3800 }
3801
3802 pub fn build(self) -> UploadFilePart<'a> {
3804 UploadFilePart {
3805 part_number: self.part_number,
3806 content_sha1: self.content_sha1,
3807 encryption: self.encryption,
3808 }
3809 }
3810}
3811
3812pub async fn upload_file_part<C, E>(
3849 auth: &mut UploadPartAuthorization<'_, '_, C, E>,
3850 upload: &UploadFilePart<'_>,
3851 data: &[u8],
3852) -> Result<FilePart, Error<E>>
3853 where C: HttpClient<Error=Error<E>>,
3854 E: fmt::Debug + fmt::Display,
3855{
3856 let inner_auth = auth.auth.as_mut().unwrap();
3860
3861 require_capability!(inner_auth, Capability::WriteFiles);
3862
3863 let mut req = inner_auth.client.post(&auth.upload_url)
3864 .expect("Invalid URL")
3865 .with_header("Authorization", &auth.authorization_token).unwrap()
3866 .with_header("X-Bz-Part-Number", &upload.part_number.to_string())?
3867 .with_header("Content-Length", &data.len().to_string())?
3868 .with_header("X-Bz-Content-Sha1", upload.content_sha1)?;
3869
3870 if let Some(enc) = &upload.encryption {
3871 if let Some(headers) = enc.to_headers() {
3872 for (header, value) in headers.into_iter() {
3873 req = req.with_header(header, &value)?;
3874 }
3875 }
3876 }
3877
3878 let res = req.with_body(data).send().await?;
3879
3880 let part: B2Result<FilePart> = serde_json::from_slice(&res)?;
3881 part.into()
3882}
3883
3884#[cfg(all(test, feature = "with_surf"))]
3885mod tests_mocked {
3886 use super::*;
3887 use crate::{
3888 account::Capability,
3889 error::ErrorCode,
3890 test_utils::{create_test_auth, create_test_client},
3891 };
3892 use surf_vcr::VcrMode;
3893
3894
3895 #[async_std::test]
3896 async fn start_large_file_upload_success() -> anyhow::Result<()> {
3897 let client = create_test_client(
3898 VcrMode::Replay,
3899 "test_sessions/large_file.yaml",
3900 None, None
3901 ).await?;
3902
3903 let mut auth = create_test_auth(client, vec![Capability::WriteFiles])
3904 .await;
3905
3906 let req = StartLargeFile::builder()
3907 .bucket_id("8d625eb63be2775577c70e1a")
3908 .file_name("test-large-file")?
3909 .build()?;
3910
3911 let file = start_large_file(&mut auth, req).await?;
3912 assert_eq!(file.file_name(), "test-large-file");
3913 assert_eq!(file.action(), FileAction::Start);
3914
3915 Ok(())
3916 }
3917
3918 #[async_std::test]
3919 async fn cancel_large_file_upload_success() -> anyhow::Result<()> {
3920 let client = create_test_client(
3921 VcrMode::Replay,
3922 "test_sessions/large_file.yaml",
3923 None, None
3924 ).await?;
3925
3926 let mut auth = create_test_auth(client, vec![Capability::WriteFiles])
3927 .await;
3928
3929 let file_info = cancel_large_file_by_id(
3930 &mut auth,
3931 concat!(
3932 "4_z8d625eb63be2775577c70e1a_f204261ca2ea2c4e1_d20211112",
3933 "_m211109_c002_v0001114_t0054"
3934 )
3935 ).await?;
3936
3937 assert_eq!(file_info.file_name, "test-large-file");
3938
3939 Ok(())
3940 }
3941
3942 #[async_std::test]
3943 async fn cancel_large_file_upload_doesnt_exist() -> anyhow::Result<()> {
3944 let client = create_test_client(
3945 VcrMode::Replay,
3946 "test_sessions/large_file.yaml",
3947 None, None
3948 ).await?;
3949
3950 let mut auth = create_test_auth(client, vec![Capability::WriteFiles])
3951 .await;
3952
3953 match cancel_large_file_by_id(&mut auth, "bad-id").await.unwrap_err() {
3954 Error::B2(e) => assert_eq!(e.code(), ErrorCode::BadRequest),
3955 _ => panic!("Unexpected error type"),
3956 }
3957
3958 Ok(())
3959 }
3960
3961 #[async_std::test]
3962 async fn test_get_download_authorization() -> Result<(), anyhow::Error> {
3963 use http_types::cache::CacheDirective;
3964
3965 let (expires1, expires2) = {
3968 use http_types::Trailers;
3969
3970 let mut header = Trailers::new();
3971 header.insert("Expires", "Fri, 21 Jan 2022 14:10:49 GMT");
3972
3973 let e1 = Expires::from_headers(header.as_ref())
3974 .unwrap().unwrap().value().to_string();
3975 let e2 = Expires::from_headers(header.as_ref()).unwrap().unwrap();
3976
3977 (e1, e2)
3978 };
3979
3980 let client = create_test_client(
3981 VcrMode::Replay,
3982 "test_sessions/auth_account.yaml",
3983 #[allow(clippy::option_map_unit_fn)]
3984 Some(Box::new(move |req| {
3985 use surf_vcr::Body;
3986
3987 if let Body::Str(body) = &mut req.body {
3988 let body_json: Result<serde_json::Value, _> =
3989 serde_json::from_str(body);
3990
3991 if let Ok(mut body) = body_json {
3992 body.get_mut("b2Expires")
3993 .map(|v| *v = serde_json::json!(expires1));
3994
3995 req.body = Body::Str(body.to_string());
3996 }
3997 }
3998 })),
3999 None
4000 ).await?;
4001
4002 let mut auth = create_test_auth(client, vec![Capability::ShareFiles])
4003 .await;
4004
4005 let mut cache_control = CacheControl::new();
4006 cache_control.push(CacheDirective::MustRevalidate);
4007
4008 let req = DownloadAuthorizationRequest::builder()
4009 .bucket_id("8d625eb63be2775577c70e1a")
4010 .file_name_prefix("files/")?
4011 .duration(chrono::Duration::seconds(30))?
4012 .content_disposition(
4013 ContentDisposition("Attachment; filename=example.html".into())
4014 )
4015 .expiration(expires2)
4016 .cache_control(cache_control)
4017 .build()?;
4018
4019 let download_auth = get_download_authorization(&mut auth, req).await?;
4020 assert_eq!(download_auth.bucket_id(), "8d625eb63be2775577c70e1a");
4021
4022 Ok(())
4023 }
4024
4025 #[async_std::test]
4026 async fn test_get_download_authorization_with_only_required_data()
4027 -> Result<(), anyhow::Error> {
4028 let client = create_test_client(
4029 VcrMode::Replay,
4030 "test_sessions/auth_account.yaml",
4031 None, None
4032 ).await?;
4033
4034 let mut auth = create_test_auth(client, vec![Capability::ShareFiles])
4035 .await;
4036
4037 let req = DownloadAuthorizationRequest::builder()
4038 .bucket_id("8d625eb63be2775577c70e1a")
4039 .file_name_prefix("files/")?
4040 .duration(chrono::Duration::seconds(30))?
4041 .build()?;
4042
4043 let download_auth = get_download_authorization(&mut auth, req).await?;
4044 assert_eq!(download_auth.bucket_id(), "8d625eb63be2775577c70e1a");
4045
4046 Ok(())
4047 }
4048
4049 #[async_std::test]
4050 async fn obtain_part_upload_authorization() -> anyhow::Result<()> {
4051 let client = create_test_client(
4052 VcrMode::Replay,
4053 "test_sessions/large_file.yaml",
4054 None, None
4055 ).await?;
4056
4057 let mut auth = create_test_auth(client, vec![Capability::WriteFiles])
4058 .await;
4059
4060 let file = StartLargeFile::builder()
4061 .bucket_id("8d625eb63be2775577c70e1a")
4062 .file_name("Test-large-file.txt")?
4063 .content_type("text/plain")
4064 .build()?;
4065
4066 let file = start_large_file(&mut auth, file).await?;
4067 let upload_auth = get_upload_part_authorization(&mut auth, &file)
4068 .await?;
4069
4070 assert_eq!(upload_auth.file_id, file.file_id);
4071
4072 Ok(())
4073 }
4074
4075 #[async_std::test]
4076 async fn obtain_upload_authorization() -> anyhow::Result<()> {
4077 let client = create_test_client(
4078 VcrMode::Replay,
4079 "test_sessions/file.yaml",
4080 None, None
4081 ).await?;
4082
4083 let mut auth = create_test_auth(client, vec![Capability::WriteFiles])
4084 .await;
4085
4086 let upload_auth = get_upload_authorization_by_id(
4087 &mut auth,
4088 "8d625eb63be2775577c70e1a"
4089 ).await?;
4090
4091 assert_eq!(upload_auth.bucket_id, "8d625eb63be2775577c70e1a");
4092
4093 Ok(())
4094 }
4095
4096 #[async_std::test]
4097 async fn upload_file_success() -> anyhow::Result<()> {
4098 let client = create_test_client(
4099 VcrMode::Replay,
4100 "test_sessions/file.yaml",
4101 None, None
4102 ).await?;
4103
4104 let mut auth = create_test_auth(client, vec![Capability::WriteFiles])
4105 .await;
4106
4107 let mut upload_auth = get_upload_authorization_by_id(
4108 &mut auth,
4109 "8d625eb63be2775577c70e1a"
4110 ).await?;
4111
4112 let file = UploadFile::builder()
4113 .file_name("test-file-upload.txt")?
4114 .sha1_checksum("81fe8bfe87576c3ecb22426f8e57847382917acf")
4115 .build()?;
4116
4117 let file = upload_file(&mut upload_auth, file, b"abcd").await?;
4118
4119 assert_eq!(file.action, FileAction::Upload);
4120
4121 Ok(())
4122 }
4123
4124 #[async_std::test]
4125 async fn copy_file_success() -> anyhow::Result<()> {
4126 let client = create_test_client(
4127 VcrMode::Replay,
4128 "test_sessions/large_file.yaml",
4129 None, None
4130 ).await?;
4131
4132 let mut auth = create_test_auth(
4133 client,
4134 vec![Capability::WriteFiles, Capability::ReadFiles]
4135 ).await;
4136
4137 let file = CopyFile::builder()
4138 .source_file_id(concat!(
4139 "4_z8d625eb63be2775577c70e1a_f111954e3108ff3f6_d20211118_",
4140 "m151810_c002_v0001168_t0010"
4141 ))
4142 .destination_file_name("new-file.txt")?
4143 .build()?;
4144
4145 let new_file = copy_file(&mut auth, file).await?;
4146 assert_eq!(new_file.file_name, "new-file.txt");
4147 assert_eq!(new_file.action, FileAction::Copy);
4148
4149 Ok(())
4150 }
4151
4152 #[async_std::test]
4155 async fn copy_file_part_success() -> anyhow::Result<()> {
4156 let client = create_test_client(
4157 VcrMode::Replay,
4158 "test_sessions/large_file.yaml",
4159 None, None
4160 ).await?;
4161
4162 let mut auth = create_test_auth(
4163 client,
4164 vec![Capability::WriteFiles, Capability::ReadFiles]
4165 ).await;
4166
4167 let file = StartLargeFile::builder()
4168 .bucket_id("8d625eb63be2775577c70e1a")
4169 .file_name("Test-large-file2.txt")?
4170 .content_type("text/plain")
4171 .build()?;
4172
4173 let file = start_large_file(&mut auth, file).await?;
4174
4175 let part1 = CopyFilePart::builder()
4176 .source_file_id(concat!(
4177 "4_z8d625eb63be2775577c70e1a_f111954e3108ff3f6_d20211118_",
4178 "m151810_c002_v0001168_t0010"
4179 ))
4180 .destination_large_file(&file)
4181 .part_number(1)?
4182 .build()?;
4183
4184 let part2 = CopyFilePart::builder()
4185 .source_file_id(concat!(
4186 "4_z8d625eb63be2775577c70e1a_f111954e3108ff3f6_d20211118_",
4187 "m151810_c002_v0001168_t0010"
4188 ))
4189 .destination_large_file(&file)
4190 .part_number(2)?
4191 .range(ByteRange::new(0, 3)?)
4192 .build()?;
4193
4194 let part1 = copy_file_part(&mut auth, part1).await?;
4195 let part2 = copy_file_part(&mut auth, part2).await?;
4196
4197 assert_eq!(part1.part_number, 1);
4198 assert_eq!(part2.part_number, 2);
4199
4200 let _file = cancel_large_file(&mut auth, file).await?;
4201 Ok(())
4202 }
4203
4204 #[async_std::test]
4207 async fn download_file_by_id_success() -> anyhow::Result<()> {
4208 let client = create_test_client(
4209 VcrMode::Replay,
4210 "test_sessions/file.yaml",
4211 None, None
4212 ).await?;
4213
4214 let mut auth = create_test_auth(client, vec![Capability::ReadFiles])
4215 .await;
4216
4217 let req = DownloadFile::with_id(concat!("4_z8d625eb63be2775577c70e1a_f",
4218 "111954e3108ff3f6_d20211118_m151810_c002_v0001168_t0010"));
4219
4220 let (file, _headers) = download_file(&mut auth, req).await?;
4221 assert_eq!(file, b"Some text\n");
4222
4223 Ok(())
4224 }
4225
4226 #[async_std::test]
4227 async fn download_file_by_name_success() -> anyhow::Result<()> {
4228 let client = create_test_client(
4229 VcrMode::Replay,
4230 "test_sessions/file.yaml",
4231 None, None
4232 ).await?;
4233
4234 let mut auth = create_test_auth(client, vec![Capability::ReadFiles])
4235 .await;
4236
4237 let req = DownloadFile::with_name("test-file.txt", "testing-b2-client");
4238
4239 let (file, _headers) = download_file(&mut auth, req).await?;
4240 assert_eq!(file, b"Some text\n");
4241
4242 Ok(())
4243 }
4244
4245 #[async_std::test]
4246 async fn download_file_by_name_via_download_authorization_success()
4247 -> anyhow::Result<()> {
4248 let client = create_test_client(
4249 VcrMode::Replay,
4250 "test_sessions/file.yaml",
4251 None, None
4252 ).await?;
4253
4254 let mut auth = create_test_auth(
4255 client,
4256 vec![Capability::ReadFiles, Capability::ShareFiles]
4257 ).await;
4258
4259 let req = DownloadAuthorizationRequest::builder()
4260 .bucket_id("8d625eb63be2775577c70e1a")
4261 .file_name_prefix("test")?
4262 .duration(chrono::Duration::seconds(30))?
4263 .build()?;
4264
4265 let mut download_auth = get_download_authorization(
4266 &mut auth,
4267 req
4268 ).await?;
4269
4270 let req = DownloadFile::with_name("test-file.txt", "testing-b2-client");
4271
4272 let (file, _headers) = download_file(&mut download_auth, req).await?;
4273 assert_eq!(file, b"Some text\n");
4274
4275 Ok(())
4276 }
4277
4278 #[async_std::test]
4291 async fn download_file_range_success() -> anyhow::Result<()> {
4292 let client = create_test_client(
4293 VcrMode::Replay,
4294 "test_sessions/file.yaml",
4295 None, None
4296 ).await?;
4297
4298 let mut auth = create_test_auth(client, vec![Capability::ReadFiles])
4299 .await;
4300
4301 let req = DownloadFile::builder()
4302 .file_name("test-file.txt", "testing-b2-client")
4303 .range(ByteRange::new(5, 8)?)
4304 .build()?;
4305
4306 let (file, _headers) = download_file(&mut auth, req).await?;
4307 assert_eq!(file, b"text");
4308
4309 Ok(())
4310 }
4311
4312 #[async_std::test]
4315 async fn delete_file_success() -> anyhow::Result<()> {
4316 let client = create_test_client(
4317 VcrMode::Replay,
4318 "test_sessions/delete_file.yaml",
4319 None, None
4320 ).await?;
4321
4322 let mut auth = create_test_auth(
4323 client,
4324 vec![Capability::DeleteFiles, Capability::WriteFiles]
4325 ).await;
4326
4327 let mut upload_auth = get_upload_authorization_by_id(
4328 &mut auth,
4329 "8d625eb63be2775577c70e1a"
4330 ).await?;
4331
4332 let file = UploadFile::builder()
4333 .file_name("test-file-upload.txt")?
4334 .sha1_checksum("81fe8bfe87576c3ecb22426f8e57847382917acf")
4335 .build()?;
4336
4337 let file = upload_file(&mut upload_auth, file, b"abcd").await?;
4338
4339
4340 let _ = delete_file_version(&mut auth, file, BypassGovernance::No)
4341 .await?;
4342
4343 Ok(())
4344 }
4345
4346 #[async_std::test]
4347 async fn upload_large_file_full_process() -> anyhow::Result<()> {
4348 let client = create_test_client(
4349 VcrMode::Replay,
4350 "test_sessions/large_file.yaml",
4351 Some(Box::new(|req| {
4352 use surf_vcr::Body;
4353
4354 if let Body::Str(body) = &mut req.body {
4355 if body.starts_with("aaaaa") {
4356 req.body = Body::Str("aaaaa for 5 MB of data".into());
4358 }
4359 }
4360 })),
4361 None
4362 ).await?;
4363
4364 let mut auth = create_test_auth(client, vec![Capability::WriteFiles])
4365 .await;
4366
4367 let file = StartLargeFile::builder()
4368 .bucket_id("8d625eb63be2775577c70e1a")
4369 .file_name("Test-large-file.txt")?
4370 .content_type("text/plain")
4371 .build()?;
4372
4373 let file = start_large_file(&mut auth, file).await?;
4374 let mut upload_auth = get_upload_part_authorization(&mut auth, &file)
4375 .await?;
4376
4377 let data1: Vec<u8> = [b'a'].iter().cycle().take(5*1024*1024)
4379 .cloned().collect();
4380
4381 let upload = UploadFilePart::builder()
4382 .part_number(1)
4383 .part_sha1_checksum("61b8d6600ac94d912874f569a9341120f680c9f8")
4384 .build();
4385
4386
4387 let _part1 = upload_file_part(&mut upload_auth, &upload, &data1).await?;
4388
4389 let upload = upload.create_next_part(
4390 Some("924f61661a3472da74307a35f2c8d22e07e84a4d")
4391 )?;
4392
4393 let _part2 = upload_file_part(&mut upload_auth, &upload, b"bcd").await?;
4394
4395 let file = finish_large_file_upload(
4396 &mut auth,
4397 &file,
4398 &[
4399 "61b8d6600ac94d912874f569a9341120f680c9f8".into(),
4400 "924f61661a3472da74307a35f2c8d22e07e84a4d".into(),
4401 ]
4402 ).await?;
4403
4404 assert_eq!(file.action, FileAction::Upload);
4405
4406 Ok(())
4407 }
4408
4409 #[async_std::test]
4410 async fn test_get_file_info() -> anyhow::Result<()> {
4411 let client = create_test_client(
4412 VcrMode::Replay,
4413 "test_sessions/file.yaml",
4414 None, None
4415 ).await?;
4416
4417 let mut auth = create_test_auth(client, vec![Capability::ReadFiles])
4418 .await;
4419
4420 let file_info = get_file_info(
4421 &mut auth,
4422 concat!("4_z8d625eb63be2775577c70e1a_f1187926dea44b322_d20211230",
4423 "_m171512_c002_v0001110_t0055")
4424 ).await?;
4425
4426 assert_eq!(
4427 file_info.content_sha1,
4428 Some(String::from("81fe8bfe87576c3ecb22426f8e57847382917acf"))
4429 );
4430
4431 Ok(())
4432 }
4433
4434 #[async_std::test]
4435 async fn test_hide_file() -> anyhow::Result<()> {
4436 let client = create_test_client(
4437 VcrMode::Replay,
4438 "test_sessions/file.yaml",
4439 None, None
4440 ).await?;
4441
4442 let mut auth = create_test_auth(client, vec![Capability::WriteFiles])
4443 .await;
4444
4445 let file = hide_file_by_name(
4446 &mut auth,
4447 "8d625eb63be2775577c70e1a",
4448 "test-file.txt"
4449 ).await?;
4450
4451 assert_eq!(file.action, FileAction::Hide);
4452
4453 Ok(())
4454 }
4455
4456 #[async_std::test]
4457 async fn test_list_file_names() -> anyhow::Result<()> {
4458 let client = create_test_client(
4459 VcrMode::Replay,
4460 "test_sessions/file.yaml",
4461 None,
4462 Some(std::boxed::Box::new(move |res| {
4463 use surf_vcr::Body;
4464
4465 if let Body::Str(body) = &mut res.body {
4466 let body_json: Result<serde_json::Value, _> =
4467 serde_json::from_str(body);
4468
4469 if let Ok(mut body) = body_json {
4470 if let Some(files) = body.get_mut("files") {
4471 let files = files.as_array_mut().unwrap();
4472
4473 for file in files.iter_mut() {
4474 file["accountId"] = serde_json::Value::String(
4475 "hidden account id".into()
4476 );
4477 }
4478 }
4479
4480 res.body = Body::Str(body.to_string());
4481 }
4482 }
4483 }))
4484 ).await?;
4485
4486 let mut auth = create_test_auth(client, vec![Capability::ListFiles])
4487 .await;
4488
4489 let req = ListFileNames::builder()
4490 .bucket_id("8d625eb63be2775577c70e1a")
4491 .max_file_count(5)
4492 .build().unwrap();
4493
4494 let (files, next_req) = list_file_names(&mut auth, req).await?;
4495
4496 assert_eq!(files.len(), 2);
4497 assert!(next_req.is_none());
4498
4499 Ok(())
4500 }
4501
4502 #[async_std::test]
4503 async fn test_list_file_versions() -> anyhow::Result<()> {
4504 let client = create_test_client(
4505 VcrMode::Replay,
4506 "test_sessions/file.yaml",
4507 None,
4508 Some(std::boxed::Box::new(move |res| {
4509 use surf_vcr::Body;
4510
4511 if let Body::Str(body) = &mut res.body {
4512 let body_json: Result<serde_json::Value, _> =
4513 serde_json::from_str(body);
4514
4515 if let Ok(mut body) = body_json {
4516 if let Some(files) = body.get_mut("files") {
4517 let files = files.as_array_mut().unwrap();
4518
4519 for file in files.iter_mut() {
4520 file["accountId"] = serde_json::Value::String(
4521 "hidden account id".into()
4522 );
4523 }
4524 }
4525
4526 res.body = Body::Str(body.to_string());
4527 }
4528 }
4529 }))
4530 ).await?;
4531
4532 let mut auth = create_test_auth(client, vec![Capability::ListFiles])
4533 .await;
4534
4535 let req = ListFileVersions::builder()
4536 .bucket_id("8d625eb63be2775577c70e1a")
4537 .max_file_count(5)
4538 .build().unwrap();
4539
4540 let (files, next_req) = list_file_versions(&mut auth, req).await?;
4541
4542 assert_eq!(files.len(), 4);
4543 assert!(next_req.is_none());
4544
4545 Ok(())
4546 }
4547
4548 #[async_std::test]
4549 async fn test_list_file_parts() -> anyhow::Result<()> {
4550 let client = create_test_client(
4551 VcrMode::Replay,
4552 "test_sessions/large_file.yaml",
4553 Some(Box::new(|req| {
4554 use surf_vcr::Body;
4555
4556 if let Body::Str(body) = &mut req.body {
4557 if body.starts_with("aaaaa") {
4558 req.body = Body::Str("aaaaa for 5 MB of data".into());
4560 }
4561 }
4562 })),
4563 None
4564 ).await?;
4565
4566 let mut auth = create_test_auth(client, vec![Capability::WriteFiles])
4567 .await;
4568
4569 let file = {
4571 let file = StartLargeFile::builder()
4572 .bucket_id("8d625eb63be2775577c70e1a")
4573 .file_name("unfinished-file.txt")?
4574 .content_type("text/plain")
4575 .build()?;
4576
4577 let file = start_large_file(&mut auth, file).await?;
4578 let mut upload_auth = get_upload_part_authorization(
4579 &mut auth,
4580 &file
4581 ).await?;
4582
4583 let data1: Vec<u8> = [b'a'].iter().cycle().take(5*1024*1024)
4585 .cloned().collect();
4586
4587 let upload = UploadFilePart::builder()
4588 .part_sha1_checksum("61b8d6600ac94d912874f569a9341120f680c9f8")
4589 .build();
4590
4591 let _part1 = upload_file_part(&mut upload_auth, &upload, &data1)
4592 .await?;
4593
4594 let upload = upload.create_next_part(
4595 Some("924f61661a3472da74307a35f2c8d22e07e84a4d")
4596 )?;
4597
4598 let _part2 = upload_file_part(&mut upload_auth, &upload, b"bcd")
4599 .await?;
4600
4601 file
4602 };
4603
4604 let req = ListFileParts::builder()
4605 .file_id(&file.file_id)
4606 .max_part_count(5)
4607 .build().unwrap();
4608
4609 let (parts, next_req) = list_file_parts(&mut auth, req).await?;
4610
4611 assert_eq!(parts.len(), 2);
4612 assert!(next_req.is_none());
4613
4614 let _ = cancel_large_file(&mut auth, file).await?;
4615
4616 Ok(())
4617 }
4618
4619 #[async_std::test]
4620 async fn test_list_unfinished_files() -> anyhow::Result<()> {
4621 let client = create_test_client(
4622 VcrMode::Replay,
4623 "test_sessions/large_file.yaml",
4624 None,
4625 Some(std::boxed::Box::new(move |res| {
4626 use surf_vcr::Body;
4627
4628 if let Body::Str(body) = &mut res.body {
4629 let body_json: Result<serde_json::Value, _> =
4630 serde_json::from_str(body);
4631
4632 if let Ok(mut body) = body_json {
4633 if let Some(files) = body.get_mut("files") {
4634 let files = files.as_array_mut().unwrap();
4635
4636 for file in files.iter_mut() {
4637 file["accountId"] = serde_json::Value::String(
4638 "hidden account id".into()
4639 );
4640 }
4641 }
4642
4643 res.body = Body::Str(body.to_string());
4644 }
4645 }
4646 }))
4647 ).await?;
4648
4649 let mut auth = create_test_auth(client, vec![Capability::ListFiles])
4650 .await;
4651
4652 let list_files = ListUnfinishedLargeFiles::builder()
4653 .bucket_id("8d625eb63be2775577c70e1a")
4654 .build()?;
4655
4656 let (files, next_req) = list_unfinished_large_files(
4657 &mut auth,
4658 list_files
4659 ).await?;
4660
4661 assert_eq!(files.len(), 2);
4662 assert!(next_req.is_none());
4663
4664 Ok(())
4665 }
4666
4667 #[async_std::test]
4668 async fn test_update_legal_hold() -> anyhow::Result<()> {
4669 let client = create_test_client(
4670 VcrMode::Replay,
4671 "test_sessions/file.yaml",
4672 None, None
4673 ).await?;
4674
4675 let mut auth = create_test_auth(
4676 client,
4677 vec![Capability::WriteFileLegalHolds]
4678 ).await;
4679
4680 let update = UpdateFileLegalHold::builder()
4681 .file_name("test-file.txt")?
4682 .file_id(concat!("4_zcd120e962b02c7a577e70e1a_f100e7b2902e23bf1",
4683 "_d20220205_m134630_c002_v0001141_t0007"))
4684 .with_legal_hold()
4685 .build()?;
4686
4687 update_file_legal_hold(&mut auth, update).await?;
4688
4689 Ok(())
4690 }
4691
4692 #[async_std::test]
4693 async fn test_update_legal_hold_fails_when_not_allowed_by_bucket()
4694 -> anyhow::Result<()> {
4695 let client = create_test_client(
4696 VcrMode::Replay,
4697 "test_sessions/file.yaml",
4698 None, None
4699 ).await?;
4700
4701 let mut auth = create_test_auth(
4702 client,
4703 vec![Capability::WriteFileLegalHolds]
4704 ).await;
4705
4706 let update = UpdateFileLegalHold::builder()
4707 .file_name("test-file.txt")?
4708 .file_id(concat!("4_z8d625eb63be2775577c70e1a_f107f7b2843696d21",
4709 "_d20220201_m191409_c002_v0001094_t0020"))
4710 .with_legal_hold()
4711 .build()?;
4712
4713 let res = update_file_legal_hold(&mut auth, update).await;
4714 assert!(res.is_err());
4715
4716 Ok(())
4717 }
4718
4719 #[async_std::test]
4720 async fn test_update_file_retention_settings()
4721 -> anyhow::Result<()> {
4722 use chrono::{Utc, TimeZone as _};
4723
4724 let client = create_test_client(
4725 VcrMode::Replay,
4726 "test_sessions/file.yaml",
4727 None, None
4728 ).await?;
4729
4730 let mut auth = create_test_auth(
4731 client,
4732 vec![Capability::WriteFileRetentions]
4733 ).await;
4734
4735 let retain_until = Utc.ymd(3000, 1, 1).and_hms(0, 0, 0);
4736
4737 let update = UpdateFileRetention::builder()
4738 .file_name("test-file.txt")?
4739 .file_id(concat!("4_zcd120e962b02c7a577e70e1a_f100e7b2902e23bf1",
4740 "_d20220205_m134630_c002_v0001141_t0007"))
4741 .file_retention(FileRetentionSetting::new(
4742 FileRetentionMode::Governance,
4743 retain_until
4744 )?)
4745 .build()?;
4746
4747 update_file_retention(&mut auth, update).await?;
4748
4749 Ok(())
4750 }
4751
4752 #[async_std::test]
4753 async fn test_update_file_retention_settings_fails_when_bucket_disallows()
4754 -> anyhow::Result<()> {
4755 use chrono::{Utc, TimeZone as _};
4756
4757 let client = create_test_client(
4758 VcrMode::Replay,
4759 "test_sessions/file.yaml",
4760 None, None
4761 ).await?;
4762
4763 let mut auth = create_test_auth(
4764 client,
4765 vec![Capability::WriteFileRetentions]
4766 ).await;
4767
4768 let retain_until = Utc.ymd(3000, 1, 1).and_hms(0, 0, 0);
4769
4770 let update = UpdateFileRetention::builder()
4771 .file_name("test-file.txt")?
4772 .file_id(concat!("4_z8d625eb63be2775577c70e1a_f107f7b2843696d21",
4773 "_d20220201_m191409_c002_v0001094_t0020"))
4774 .file_retention(FileRetentionSetting::new(
4775 FileRetentionMode::Governance,
4776 retain_until
4777 )?)
4778 .build()?;
4779
4780 let res = update_file_retention(&mut auth, update).await;
4781 assert!(res.is_err());
4782
4783 Ok(())
4784 }
4785}
4786
4787#[cfg(test)]
4788mod tests {
4789 use super::*;
4790
4791
4792 #[async_std::test]
4793 async fn copy_file_bad_req_content_type() -> anyhow::Result<()> {
4794 let file = CopyFile::builder()
4795 .source_file_id(concat!(
4796 "4_z8d625eb63be2775577c70e1a_f111954e3108ff3f6_d20211118_",
4797 "m151810_c002_v0001168_t0010"
4798 ))
4799 .destination_file_name("new-file.txt")?
4800 .content_type("text/plain");
4801
4802 match file.build().unwrap_err() {
4803 ValidationError::Incompatible(_) => {},
4804 e => panic!("Unexpected error type: {}", e),
4805 }
4806
4807 Ok(())
4808 }
4809}