b2_client/
file.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2   License, v. 2.0. If a copy of the MPL was not distributed with this
3   file, You can obtain one at http://mozilla.org/MPL/2.0/.
4*/
5
6//! Backblaze B2 API calls for working with files.
7//!
8//! See [the B2 documentation on uploading
9//! files](https://www.backblaze.com/b2/docs/uploading.html) for an overview of
10//! the process.
11//!
12//!
13//! # Uploading Files
14//!
15//! For files larger than 5 GB, see [Uploading Large
16//! Files](#uploading-large-files).
17//!
18//! To upload a file:
19//!
20//! 1. Authenticate with B2 to obtain an [Authorization] object.
21//! 2. Create an [UploadFile] request.
22//! 3. Call [get_upload_authorization] to get an [UploadAuthorization] for a
23//!    bucket.
24//! 4. Call [upload_file] with your `UploadAuthorization`, `UploadFile` request,
25//!    and file data.
26//!
27//! You can upload multiple files with a single `UploadAuthorization`, but only
28//! one at a time. To upload multiple files in parallel, each thread or task
29//! needs to obtain its own `UploadAuthorization`.
30//!
31//!
32//! # Uploading Large Files
33//!
34//! To upload a large file:
35//!
36//! 1. Authenticate with B2 to obtain an [Authorization] object.
37//! 2. Create a [StartLargeFile] object with the destination bucket and new
38//!    file's information, then pass it to [start_large_file]. You will receive
39//!    a [File] object.
40//! 3. Pass the [File] to [get_upload_part_authorization] to receive an
41//!    [UploadPartAuthorization].
42//!     * You can upload parts in separate threads for better performance; each
43//!       thread must call [get_upload_part_authorization] and use its
44//!       respective authorization when uploading data.
45//! 4. Create an [UploadFilePart] object via the [UploadFilePartBuilder].
46//! 5. Use the `UploadPartAuthorization` and `UploadFilePart` to call
47//!    [upload_file_part] with the file data to upload.
48//! 6. Call [UploadFilePart::create_next_part] to create a new upload part
49//!    request.
50//! 7. Repeat steps 5 and 6 until all parts have been uploaded.
51//! 8. Call [finish_large_file_upload] to merge the file parts into a single
52//!    [File]. After finishing the file, it can be treated like any other
53//!    uploaded file.
54//!
55//! # Examples
56//!
57//! ```no_run
58//! # fn calculate_sha1(data: &[u8]) -> String { String::default() }
59//! use std::env;
60//! use anyhow;
61//! use b2_client::{self as b2, HttpClient as _};
62//!
63//! # #[cfg(feature = "with_surf")]
64//! async fn upload_file(name: &str, bucket: b2::Bucket, data: &[u8])
65//! -> anyhow::Result<b2::File> {
66//!     let key = env::var("B2_KEY").ok().unwrap();
67//!     let key_id = env::var("B2_KEY_ID").ok().unwrap();
68//!
69//!     let client = b2::client::SurfClient::default();
70//!     let mut auth = b2::authorize_account(client, &key, &key_id).await?;
71//!
72//!     let mut upload_auth = b2::get_upload_authorization(&mut auth, &bucket)
73//!         .await?;
74//!
75//!     let checksum = calculate_sha1(&data);
76//!
77//!     let file = b2::UploadFile::builder()
78//!         .file_name(name)?
79//!         .sha1_checksum(&checksum)
80//!         .build()?;
81//!
82//!     Ok(b2::upload_file(&mut upload_auth, file, &data).await?)
83//! }
84//! ```
85//!
86//! ```no_run
87//! # fn calculate_sha1(data: &[u8]) -> String { String::default() }
88//! use std::env;
89//! use anyhow;
90//! use b2_client::{self as b2, HttpClient as _};
91//!
92//! # #[cfg(feature = "with_surf")]
93//! async fn upload_large_file(
94//!     name: &str,
95//!     data_part1: &[u8],
96//!     data_part2: &[u8],
97//! ) -> anyhow::Result<b2::File> {
98//!     let key = env::var("B2_KEY").ok().unwrap();
99//!     let key_id = env::var("B2_KEY_ID").ok().unwrap();
100//!
101//!     let client = b2::client::SurfClient::default();
102//!     let mut auth = b2::authorize_account(client, &key, &key_id).await?;
103//!
104//!     let file = b2::StartLargeFile::builder()
105//!         .bucket_id("some-bucket-id")
106//!         .file_name(name)?
107//!         .content_type("text/plain")
108//!         .build()?;
109//!
110//!     let file = b2::start_large_file(&mut auth, file).await?;
111//!
112//!     let mut upload_auth = b2::get_upload_part_authorization(
113//!         &mut auth,
114//!         &file
115//!     ).await?;
116//!
117//!     // Assuming a `calculate_sha1` function is defined:
118//!     let sha1 = calculate_sha1(&data_part1);
119//!     let sha2 = calculate_sha1(&data_part2);
120//!
121//!     let upload_req = b2::UploadFilePart::builder()
122//!         .part_sha1_checksum(&sha1)
123//!         .build();
124//!
125//!     let _part1 = b2::upload_file_part(
126//!         &mut upload_auth,
127//!         &upload_req,
128//!         &data_part1,
129//!     ).await?;
130//!
131//!     let upload_req = upload_req.create_next_part(Some(&sha2))?;
132//!
133//!     let _part2 = b2::upload_file_part(
134//!         &mut upload_auth,
135//!         &upload_req,
136//!         &data_part2,
137//!     ).await?;
138//!
139//!     Ok(b2::finish_large_file_upload(
140//!         &mut auth,
141//!         &file,
142//!         &[sha1, sha2],
143//!     ).await?)
144//! }
145//! ```
146
147use 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
178// Add a standard header to a serde_json::Map.
179macro_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/// Determines whether there is a legal hold on a file.
213#[derive(Debug, Deserialize)]
214pub struct FileLegalHold {
215    #[serde(rename = "isClientAuthorizedToRead")]
216    can_read: bool,
217    value: Option<LegalHoldValue>,
218}
219
220/// The action taken that resulted in a [File] object returned by the B2 API.
221#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
222#[serde(rename_all = "camelCase")]
223pub enum FileAction {
224    /// A large file upload has been started and is still in progress.
225    Start,
226    /// A file was uploaded.
227    Upload,
228    /// A file was copied from another file.
229    Copy,
230    /// The file (file version) has been marked as hidden.
231    Hide,
232    /// The file is a virtual folder.
233    Folder,
234}
235
236// TODO: I may want to rename this to FileRetention since it's one of
237// update_file_retention's parameters.
238// This is different than but very similar to bucket::FileRetentionPolicy.
239/// Sets the file retention mode and date/time during which the retention rule
240/// applies.
241#[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// This is different than but very similar to bucket::FileLockConfiguration.
269/// The retention settings for a file.
270#[derive(Debug, Deserialize)]
271pub struct FileRetention {
272    #[serde(rename = "isClientAuthorizedToRead")]
273    can_read: bool,
274    value: FileRetentionSetting,
275}
276
277impl FileRetention {
278    /// Get the file retention settings.
279    ///
280    /// If not authorized to read the settings, returns `None`.
281    pub fn settings(&self) -> Option<FileRetentionSetting> {
282        if self.can_read {
283            Some(self.value)
284        } else {
285            None
286        }
287    }
288}
289
290// TODO: Rename to FileMetadata?
291/// Metadata of a file stored in B2.
292#[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    // Only relevant when action is "upload", will be 0 otherwise.
300    content_length: u64,
301    // Value is "none" for large files.
302    content_sha1: Option<String>, // Max 64 elements
303    content_md5: Option<String>, // Max 32 elements
304    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    // Milliseconds since midnight, 1970-1-1
312    // If action is `Folder`, this will be 0.
313    upload_timestamp: i64,
314}
315
316impl File {
317    /// The action taken to result in this [File].
318    pub fn action(&self) -> FileAction { self.action }
319
320    /// The ID of the bucket containing the file.
321    pub fn bucket_id(&self) -> &str { &self.bucket_id }
322
323    /// The number of bytes stored in the file.
324    ///
325    /// Only meaningful when the [action](Self::action) is [FileAction::Upload]
326    /// or [FileAction::Copy]; otherwise the value is `None`.
327    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    /// The SHA-1 checksum of the bytes in the file.
335    ///
336    /// There is no checksum for large files or when the [action](Self::action)
337    /// is [FileAction::Hide] or [FileAction::Folder].
338    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    /// The MD5 checksum of the bytes in the file.
346    ///
347    /// There is no checksum for large files or when the [action](Self::action)
348    /// is [FileAction::Hide] or [FileAction::Folder].
349    pub fn md5_checksum(&self) -> Option<&String> {
350        self.content_md5.as_ref()
351    }
352
353    /// When [action](Self::action) is [FileAction::Upload],
354    /// [FileAction::Start], or [FileAction::Copy], the file's MIME type.
355    pub fn content_type(&self) -> Option<&String> {
356        self.content_type.as_ref()
357    }
358
359    /// The B2 ID of the file.
360    pub fn file_id(&self) -> &str { &self.file_id }
361
362    /// User-specified and other file metadata.
363    pub fn file_info(&self) -> &serde_json::Value {
364        &self.file_info
365    }
366
367    /// The name of the file.
368    pub fn file_name(&self) -> &str { &self.file_name }
369
370    /// The file's retention policy.
371    pub fn file_retention(&self) -> Option<&FileRetention> {
372        self.file_retention.as_ref()
373    }
374
375    /// See if there is a legal hold on this file.
376    ///
377    /// Returns an error if the [Authorization] does not have
378    /// [Capability::ReadFileLegalHolds].
379    ///
380    /// Returns `None` if a legal hold is not valid for the file type (e.g., the
381    /// [action](Self::action) is `hide` or `folder`).
382    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    /// The encryption settings for the file.
402    pub fn encryption_settings(&self) -> Option<&ServerSideEncryption> {
403        self.server_side_encryption.as_ref()
404    }
405
406    /// The date and time at which the file was uploaded.
407    ///
408    /// If the [action](Self::action) is `Folder`, returns `None`.
409    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/// A part of a large file currently being uploaded.
420#[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/// A large file that was cancelled prior to upload completion.
451#[derive(Debug, Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct CancelledFileUpload {
454    /// The ID of the cancelled file.
455    pub file_id: String,
456    /// The account that owns the file.
457    pub account_id: String,
458    /// The bucket the file was being uploaded to.
459    pub bucket_id: String,
460    /// The file's name.
461    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
471/// Cancel the uploading of a large file and delete any parts already uploaded.
472pub 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
480/// Cancel the uploading of a large file and delete any parts already uploaded.
481///
482/// See [cancel_large_file] for documentation on use.
483pub 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/// A byte-range to retrieve a portion of a file.
503///
504/// Both `start` and `end` are inclusive.
505#[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    // TODO: It would be reasonable to misremember/assume that the range is
523    // exclusive of the end byte; should we use `new_inclusive` and
524    // `new_exclusive` (bounded/unbounded?) functions instead? This forces
525    // explicitly choosing one or the other. Name them `new_end_xxx` to be truly
526    // clear?
527    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/// Describe the action to take with file metadata when copying a file.
542#[derive(Debug, Eq, PartialEq, Serialize)]
543#[serde(rename_all = "UPPERCASE")]
544pub enum MetadataDirective {
545    Copy,
546    Replace,
547}
548
549/// A request to copy a file from a bucket, potentially to a different bucket.
550///
551/// Use [CopyFileBuilder] to create a `CopyFile`, then pass it to [copy_file].
552#[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/// A builder to create a [CopyFile] request.
585///
586/// See <https://www.backblaze.com/b2/docs/b2_copy_file.html> for further
587/// information.
588#[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    // To merge into file_info on build if metadata_directive is Replace:
603    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    /// Obtain the source file ID and encryption settings for the copy
614    /// operation.
615    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    /// Set the source file ID of the file to copy.
621    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    /// Set the destination bucket for the new file.
627    ///
628    /// If not provided, the same bucket ID as the source file is used.
629    ///
630    /// Both buckets must belong to the same account.
631    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    /// Set the filename to use for the new file.
637    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    /// If provided, only copy the specified byte range of the source file.
644    pub fn range(mut self, range: ByteRange) -> Self {
645        self.range = Some(range);
646        self
647    }
648
649    /// Determine whether to copy the source metadata to the new file.
650    ///
651    /// If [MetadataDirective::Copy] (the default), the source metadata will be
652    /// copied to the new file.
653    ///
654    /// If [MetadataDirective::Replace], the new file's metadata will be empty
655    /// or determined by the information provided via
656    /// [content_type](Self::content_type) and [file_info](Self::file_info).
657    pub fn metadata_directive(mut self, directive: MetadataDirective) -> Self {
658        self.metadata_directive = Some(directive);
659        self
660    }
661
662    /// Set the content-type of the file.
663    ///
664    /// The content-type can only be set if
665    /// [metadata_directive](Self::metadata_directive) is
666    /// [MetadataDirective::Replace].
667    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    /// Set user-specified file metadata.
673    ///
674    /// The file information can only be set if
675    /// [metadata_directive](Self::metadata_directive) is
676    /// [MetadataDirective::Replace].
677    ///
678    /// For the following headers, use their corresponding methods instead of
679    /// setting the values here:
680    ///
681    /// * X-Bz-Info-src_last_modified_millis:
682    ///   [last_modified](Self::last_modified)
683    /// * X-Bz-Info-large_file_sha1: [sha1_checksum](Self::sha1_checksum)
684    /// * Content-Disposition: [content_disposition](Self::content_disposition)
685    /// * Content-Language: [content_language](Self::content_language)
686    /// * Expires: [expiration](Self::expiration)
687    /// * Cache-Control: [cache_control](Self::cache_control)
688    /// * Content-Encoding: [content_encoding](Self::content_encoding)
689    ///
690    /// If any of the above are set here and via their methods, the value from
691    /// the method will override the value specified here.
692    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    /// Set the file-retention settings for the new file.
699    ///
700    /// Setting this requires [Capability::WriteFileRetentions].
701    pub fn file_retention(mut self, retention: FileRetentionPolicy) -> Self {
702        self.file_retention = Some(retention);
703        self
704    }
705
706    /// Enable legal hold status for the new file.
707    pub fn with_legal_hold(mut self) -> Self {
708        self.legal_hold = Some(LegalHoldValue::On);
709        self
710    }
711
712    /// Do not enable legal hold status for the new file.
713    pub fn without_legal_hold(mut self) -> Self {
714        self.legal_hold = Some(LegalHoldValue::Off);
715        self
716    }
717
718    /// Specify the server-side encryption settings on the source file.
719    ///
720    /// Calling [source_file](Self::source_file) will set this from the file
721    /// object.
722    pub fn source_encryption_settings(mut self, settings: ServerSideEncryption)
723    -> Self {
724        self.source_encryption = Some(settings);
725        self
726    }
727
728    /// Specify the server-side encryption settings for the destination file.
729    ///
730    /// If not provided, the bucket's default settings will be used.
731    pub fn destination_encryption_settings(
732        mut self,
733        settings: ServerSideEncryption
734    ) -> Self {
735        self.dest_encryption = Some(settings);
736        self
737    }
738
739    /// The time of the file's last modification.
740    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    /// The SHA1 checksum of the file's contents.
747    ///
748    /// B2 will use this to verify the accuracy of the file upload, and it will
749    /// be returned in the header `X-Bz-Content-Sha1` when downloading the file.
750    pub fn sha1_checksum(mut self, checksum: &'a str) -> Self {
751        self.sha1_checksum = Some(checksum);
752        self
753    }
754
755    /// The value to use for the `Content-Disposition` header when downloading
756    /// the file.
757    ///
758    /// Note that the download request can override this value.
759    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    /// The value to use for the `Content-Language` header when downloading the
768    /// file.
769    ///
770    /// Note that the download request can override this value.
771    pub fn content_language(mut self, language: impl Into<String>) -> Self {
772        // TODO: validate content_language
773        self.content_language = Some(percent_encode!(language.into()));
774        self
775    }
776
777    /// The value to use for the `Expires` header when the file is downloaded.
778    ///
779    /// Note that the download request can override this value.
780    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    /// The value to use for the `Cache-Control` header when the file is
788    /// downloaded.
789    ///
790    /// This would override the value set at the bucket level, and can be
791    /// overriden by a download request.
792    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    /// The value to use for the `Content-Encoding` header when the file is
798    /// downloaded.
799    ///
800    /// Note that this can be overriden by a download request.
801    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    /// Create a [CopyFile] object.
808    ///
809    /// # Returns
810    ///
811    /// Returns [ValidationError::MissingData] if the source file or destination
812    /// filename are not set.
813    ///
814    /// Returns [ValidationError::Incompatible] if the
815    /// [metadata_directive](Self::metadata_directive) is
816    /// [MetadataDirective::Copy] or was not provided AND
817    /// [content_type](Self::content_type) or [file_info](Self::file_info) were
818    /// set.
819    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
891/// Copy an existing file to a new file, possibly on a different bucket.
892///
893/// The new file must be less than 5 GB. Use [copy_file_part] to copy larger
894/// files.
895///
896/// If copying from one bucket to another, both buckets must belong to the same
897/// account.
898pub 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/// A request to copy from an existing file to a part of a large file.
927#[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/// A builder to create a [CopyFilePart] request.
948#[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    /// Set the source file to copy from and its encryption settings.
960    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    /// Set the source file to copy from.
967    pub fn source_file_id(mut self, file: &'a str) -> Self {
968        self.source_file = Some(file);
969        self
970    }
971
972    /// Set the large file to copy data to and its encryption settings.
973    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    /// Set  the large file to copy data to.
986    pub fn destination_large_file_id(mut self, file: &'a str) -> Self {
987        self.large_file = Some(file);
988        self
989    }
990
991    /// Set the number of this part.
992    ///
993    /// Part numbers increment from 1 to 10,000 inclusive.
994    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    /// Set the range of bytes from the source file to copy.
1009    ///
1010    /// If no range is specified, the entire file is copied.
1011    pub fn range(mut self, range: ByteRange) -> Self {
1012        self.range = Some(range);
1013        self
1014    }
1015
1016    /// Set the encryption settings of the source file.
1017    ///
1018    /// This must match the settings with which the file was encrypted.
1019    pub fn source_encryption_settings(mut self, enc: &'a ServerSideEncryption)
1020    -> Self {
1021        self.source_encryption = Some(enc);
1022        self
1023    }
1024
1025    /// Set the encryption settings of the destination file.
1026    ///
1027    /// This must match the settings passed to [start_large_file].
1028    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    /// Create a [CopyFilePart] request object.
1037    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
1063/// Copy from an existing file to a new large file.
1064///
1065/// The [Authorization] must have [Capability::WriteFiles], and if the bucket is
1066/// private, [Capability::ReadFiles].
1067pub 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/// Declare whether to bypass file lock restrictions when performing an action
1087/// on a [File].
1088///
1089/// Bypassing governance rules requires [Capability::BypassGovernance].
1090#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1091#[serde(rename_all = "camelCase")]
1092pub enum BypassGovernance { Yes, No }
1093
1094/// Delete a version of a file.
1095///
1096/// If the version is the file's latest version and there are older versions,
1097/// the most-recent older version will become the current version of the file.
1098///
1099/// If called on an unfinished large file, has the same effect as
1100/// [cancel_large_file].
1101pub 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
1117/// Retrieve the headers that will be returned when the specified file is
1118/// downloaded.
1119///
1120/// See <https://www.backblaze.com/b2/docs/b2_download_file_by_id.html> for a
1121/// list of headers that may be returned.
1122pub 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
1132/// Retrieve the headers that will be returned when the specified file is
1133/// downloaded.
1134///
1135/// See <https://www.backblaze.com/b2/docs/b2_download_file_by_id.html> for a
1136/// list of headers that may be returned.
1137pub 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    // TODO: This is probably only required for private buckets; public buckets
1145    // don't require an authorization token, but the docs read as if this is
1146    // necessary if provided. Need to test, and if necessary allow downloading
1147    // the file without passing the authorization token.
1148    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)), // (Percent-encoded file name, bucket name)
1167}
1168
1169/// A request to download a file or a portion of a file from the B2 API.
1170///
1171/// A simple file request can be created via [with_name](Self::with_name) or
1172/// [with_id](Self::with_id); for more complex requests use a
1173/// [DownloadFileBuilder].
1174///
1175/// If you use self-managed server-side encryption, you must use
1176/// [DownloadFileBuilder] to pass the encryption information.
1177///
1178/// See <https://www.backblaze.com/b2/docs/b2_download_file_by_id.html> for
1179/// information on downloading files, including the list of headers that may be
1180/// returned.
1181#[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    /// Download a file with the specified file ID.
1206    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    /// Download a file with the specified file name.
1221    ///
1222    /// The name will be percent-encoded.
1223    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    /// Generate the public URL for a GET request for a file in a public bucket.
1242    ///
1243    /// A file in a public bucket does not require an authorization token to
1244    /// access, making this link suitable for distribution (e.g., embedding in a
1245    /// web page).
1246    ///
1247    /// You must provide an [Authorization] or [DownloadAuthorization] that has
1248    /// access to the file.
1249    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    /// Download a file with the specified file name.
1287    ///
1288    /// The name will be percent-encoded.
1289    ///
1290    /// If both [file_name](Self::file_name) and [file_id](Self::file_id) are
1291    /// provided, the last one will be used.
1292    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    /// Download a file with the specified file ID.
1298    ///
1299    /// If both [file_name](Self::file_name) and [file_id](Self::file_id) are
1300    /// provided, the last one will be used.
1301    pub fn file_id(mut self, id: &'a str) -> Self {
1302        self.file = Some(FileHandle::Id(id));
1303        self
1304    }
1305
1306    /// Specify the byte range of the file to download.
1307    ///
1308    /// There will be a Content-Range header that specifies the bytes returned
1309    /// and the total number of bytes.
1310    ///
1311    /// The HTTP status code when a partial file is returned is `206 Partial
1312    /// Content` rather than `200 OK`.
1313    pub fn range(mut self, range: ByteRange) -> Self {
1314        self.range = Some(range);
1315        self
1316    }
1317
1318    /// Override the Content-Disposition header of the response with the one
1319    /// provided.
1320    ///
1321    /// If including this header will exceed the 7,000 byte header limit (2,048
1322    /// bytes if using server-side encryption), the request will be rejected.
1323    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    /// Override the Content-Language header of the response with the one
1331    /// provided.
1332    ///
1333    /// If including this header will exceed the 7,000 byte header limit (2,048
1334    /// bytes if using server-side encryption), the request will be rejected.
1335    pub fn content_language(mut self, language: &'a str) -> Self {
1336        // TODO: validate content_language
1337        self.content_language = Some(language);
1338        self
1339    }
1340
1341    /// Override the Expires header of the response with the one provided.
1342    ///
1343    /// If including this header will exceed the 7,000 byte header limit (2,048
1344    /// bytes if using server-side encryption), the request will be rejected.
1345    pub fn expiration(mut self, expiration: Expires) -> Self {
1346        self.expires = Some(expiration.value().to_string());
1347        self
1348    }
1349
1350    /// Override the Cache-Control header of the response with the one provided.
1351    ///
1352    /// If including this header will exceed the 7,000 byte header limit (2,048
1353    /// bytes if using server-side encryption), the request will be rejected.
1354    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    /// Override the Content-Encoding header of the response with the one
1360    /// provided.
1361    ///
1362    /// If including this header will exceed the 7,000 byte header limit (2,048
1363    /// bytes if using server-side encryption), the request will be rejected.
1364    pub fn content_encoding(mut self, encoding: ContentEncoding) -> Self {
1365        self.content_encoding = Some(format!("{}", encoding.encoding()));
1366        self
1367    }
1368
1369    /// Override the Content-Type header of the response with the one provided.
1370    ///
1371    /// If including this header will exceed the 7,000 byte header limit (2,048
1372    /// bytes if using server-side encryption), the request will be rejected.
1373    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    /// Set the encryption settings to use for the file.
1379    ///
1380    /// This is required if using self-managed server-side encryption.
1381    pub fn encryption_settings(mut self, settings: ServerSideEncryption)
1382    -> Self {
1383        self.encryption = Some(settings);
1384        self
1385    }
1386
1387    /// Build a [DownloadFile] request.
1388    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
1407/// Allow downloading files via an `Authorization` or `DownloadAuthorization`.
1408///
1409/// You do not need to use this type explicitly.
1410pub 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
1466/// Download a file from the B2 service.
1467///
1468/// If downloading a file by name, you may provide a mutable reference to either
1469/// an [Authorization] or a [DownloadAuthorization].
1470///
1471/// Downloading files by ID requires an `Authorization`. If provided with a
1472/// `DownloadAuthorization`, returns `Error::MissingAuthorization`.
1473pub 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    // TODO: This is probably only required for private buckets; public buckets
1500    // don't require an authorization token, but the docs read as if this is
1501    // necessary if provided. Need to test, and if necessary allow downloading
1502    // the file without passing the authorization token.
1503    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    // An error from Backblaze would successfully deserialize as Vec<u8>, so we
1541    // need to check for it specifically.
1542    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    // TODO: This is probably only required for private buckets; public buckets
1559    // don't require an authorization token, but the docs read as if this is
1560    // necessary if provided. Need to test, and if necessary allow downloading
1561    // the file without passing the authorization token.
1562    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('&'); // The trailing & will be ignored, so this is fine.
1573        };
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    // An error from Backblaze would successfully deserialize as Vec<u8>, so we
1624    // need to check for it specifically.
1625    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
1632/// Delete a version of a file.
1633///
1634/// If the version is the file's latest version and there are older versions,
1635/// the most-recent older version will become the current version of the file.
1636///
1637/// If called on an unfinished large file, has the same effect as
1638/// [cancel_large_file].
1639pub 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
1670/// Complete the upload of a large file, merging all parts into a single [File].
1671///
1672/// This is the final step to uploading a large file. If the request times out,
1673/// it is recommended to call [get_file_info] to see if the file succeeded and
1674/// only repeat the call to `finish_large_file_upload` if the file is missing.
1675///
1676/// The `sha1_checksums` must be sorted ascending by part number.
1677///
1678/// The [Authorization] must have [Capability::WriteFiles].
1679pub 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
1690/// Complete the upload of a large file, merging all parts into a single [File].
1691///
1692/// See [finish_large_file_upload] for documentation on use.
1693pub 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
1718/// Retrieve metadata about a file stored in B2.
1719///
1720/// See <https://www.backblaze.com/b2/docs/b2_get_file_info.html> for further
1721/// information.
1722///
1723/// # Errors
1724///
1725/// This function will return an error if the file ID does not exist or it is
1726/// for a large file that has not been finished yet.
1727pub 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/// A request to obtain a [DownloadAuthorization].
1762///
1763/// Use [DownloadAuthorizationRequestBuilder] to create a
1764/// `DownloadAuthorizationRequest`, then pass it to [get_download_authorization]
1765/// to obtain a [DownloadAuthorization].
1766#[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/// A builder to create a [DownloadAuthorizationRequest].
1793///
1794/// After building the `DownloadAuthorizationRequest`, pass it to
1795/// [get_download_authorization] to obtain a [DownloadAuthorization]
1796///
1797/// The bucket ID, file name prefix, and valid duration are required.
1798///
1799/// See <https://www.backblaze.com/b2/docs/b2_get_download_authorization.html>
1800/// for furter information.
1801#[derive(Default)]
1802pub struct DownloadAuthorizationRequestBuilder<'a> {
1803    // Required:
1804    bucket_id: Option<&'a str>,
1805    file_name_prefix: Option<&'a str>,
1806    valid_duration_in_seconds: Option<Duration>,
1807    // Optional:
1808    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    /// Create a download authorization for the specified bucket ID.
1818    pub fn bucket_id(mut self, id: &'a str) -> Self {
1819        self.bucket_id = Some(id);
1820        self
1821    }
1822
1823    /// Use the given file name prefix to determine what files the
1824    /// [DownloadAuthorization] will allow access to.
1825    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    /// Specify the amount of time for which the [DownloadAuthorization] will be
1833    /// valid.
1834    ///
1835    /// This must be between one second and one week, inclusive.
1836    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    /// If specified, download requests must have this content disposition. The
1852    /// grammar is specified in RFC 6266, except that parameter names containing
1853    /// a '*' are not allowed.
1854    pub fn content_disposition(mut self, disposition: ContentDisposition)
1855    -> Self {
1856        self.b2_content_disposition = Some(disposition.0);
1857        self
1858    }
1859
1860    /// If specified, download requests must have this content language. The
1861    /// grammar is specified in RFC 2616.
1862    pub fn content_language<S: Into<String>>(mut self, lang: S) -> Self {
1863        // TODO: Validate language.
1864        self.b2_content_language = Some(lang.into());
1865        self
1866    }
1867
1868    /// If specified, download requests must have this expiration.
1869    pub fn expiration(mut self, expiration: Expires) -> Self {
1870        self.b2_expires = Some(expiration.value().to_string());
1871        self
1872    }
1873
1874    /// If specified, download requests must have this cache control.
1875    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    /// If specified, download requests must have this content encoding.
1881    pub fn content_encoding(mut self, encoding: ContentEncoding) -> Self {
1882        self.b2_content_encoding = Some(format!("{}", encoding.encoding()));
1883        self
1884    }
1885
1886    /// If specified, download requests must have this content type.
1887    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    /// Build a [DownloadAuthorizationRequest].
1893    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/// A capability token that authorizes downloading files from a private bucket.
1923#[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    /// Get the ID of the bucket this `DownloadAuthorization` can access.
1941    pub fn bucket_id(&self) -> &str { &self.bucket_id }
1942    /// The file prefix that determines what files in the bucket are accessible
1943    /// via this `DownloadAuthorization`.
1944    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
1969/// Generate a download authorization token to download files with a specific
1970/// prefix from a private B2 bucket.
1971///
1972/// The [Authorization] token must have [Capability::ShareFiles].
1973///
1974/// The returned [DownloadAuthorization] can be passed to
1975/// [download_file](crate::file::download_file) in place of an [Authorization]
1976/// when downloading files by name.
1977///
1978/// See <https://www.backblaze.com/b2/docs/b2_get_download_authorization.html>
1979/// for further information.
1980///
1981/// # Examples
1982///
1983/// ```no_run
1984/// # #[cfg(feature = "with_surf")]
1985/// # use b2_client::{
1986/// #     client::{HttpClient, SurfClient},
1987/// #     account::authorize_account,
1988/// #     file::{DownloadAuthorizationRequest, get_download_authorization},
1989/// # };
1990/// # #[cfg(feature = "with_surf")]
1991/// # async fn f() -> anyhow::Result<()> {
1992/// let mut auth = authorize_account(
1993///     SurfClient::default(),
1994///     "MY KEY ID",
1995///     "MY KEY"
1996/// ).await?;
1997///
1998/// let download_req = DownloadAuthorizationRequest::builder()
1999///     .bucket_id("MY BUCKET ID")
2000///     .file_name_prefix("my/files/")?
2001///     .duration(chrono::Duration::seconds(60))?
2002///     .build()?;
2003///
2004/// let download_auth = get_download_authorization(&mut auth, download_req)
2005///     .await?;
2006/// # Ok(()) }
2007/// ```
2008pub 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/// An authorization to upload file contents to a B2 file.
2030#[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
2050/// Get an [UploadPartAuthorization] to upload data to a new B2 file.
2051///
2052/// Use the returned `UploadPartAuthorization` when calling [upload_file_part].
2053///
2054/// The `UploadPartAuthorization` is valid for 24 hours or until an endpoint
2055/// rejects an upload.
2056///
2057/// If uploading multiple parts concurrently, each thread or task needs its own
2058/// authorization.
2059///
2060/// The [Authorization] must have [Capability::WriteFiles].
2061///
2062/// # B2 API Difference
2063///
2064/// The equivalent B2 endpoint is called
2065/// [`b2_get_upload_url`](https://www.backblaze.com/b2/docs/b2_get_upload_part_url.html).
2066pub 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
2080/// Get an [UploadPartAuthorization] to upload data to a new B2 file.
2081///
2082/// See [get_upload_part_authorization] for documentation on retrieving the
2083/// authorization.
2084pub 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/// An authorization to upload a file to a B2 bucket.
2113#[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
2135/// Obtain an authorization to upload files to a bucket.
2136///
2137/// Use the returned [UploadAuthorization] when calling [upload_file].
2138///
2139/// For faster uploading, you can obtain multiple authorizations and upload
2140/// files concurrently.
2141///
2142/// The `UploadAuthorization` is valid for 24 hours or until an upload attempt
2143/// is rejected. You can make multiple file uploads with a single authorization.
2144///
2145/// The [Authorization] must have [Capability::WriteFiles].
2146///
2147/// # B2 API Difference
2148///
2149/// The equivalent B2 endpoint is called
2150/// [`b2_get_upload_url`](https://www.backblaze.com/b2/docs/b2_get_upload_url.html).
2151pub 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
2161/// Obtain an authorization to upload files to a bucket.
2162///
2163/// See [get_upload_authorization] for documentation on retrieving the
2164/// authorization.
2165pub 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
2188/// Hide a file so that it cannot be downloaded by name.
2189///
2190/// Previous versions of the file are still stored. See
2191/// <https://www.backblaze.com/b2/docs/file_versions.html> for information on
2192/// hiding files.
2193///
2194/// # Notes
2195///
2196/// Some  of the returned [File] fields are empty, `0`, or meaningless for
2197/// hidden files, such as [content_length](File::content_length) and
2198/// [sha1_checksum](File::sha1_checksum).
2199///
2200/// See <https://www.backblaze.com/b2/docs/b2_hide_file.html> for further
2201/// information.
2202pub 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
2210/// Hide a file so that it cannot be downloaded by name.
2211///
2212/// Previous versions of the file are still stored. See
2213/// <https://www.backblaze.com/b2/docs/file_versions.html> for information on
2214/// hiding files.
2215///
2216/// # Notes
2217///
2218/// Some  of the returned [File] fields are empty, `0`, or meaningless for
2219/// hidden files, such as [content_length](File::content_length) and
2220/// [sha1_checksum](File::sha1_checksum).
2221///
2222/// See <https://www.backblaze.com/b2/docs/b2_hide_file.html> for further
2223/// information.
2224pub 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/// A request to list the names of files stored in a bucket.
2250#[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/// A builder for a [ListFileNames] request.
2268#[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    /// The bucket ID from which to list files.
2279    pub fn bucket_id(mut self, id: &'a str) -> Self {
2280        self.bucket_id = Some(id);
2281        self
2282    }
2283
2284    /// The file name with which to start the listing.
2285    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    /// The maximum number of files to return.
2291    ///
2292    /// The default is 100. The provided `count` will be clamped to a value
2293    /// between 1 and 10,000 inclusive.
2294    ///
2295    /// A single transaction has a limit of 1,000 files; values greater than
2296    /// 1,000 will incur charges for multiple transactions.
2297    ///
2298    /// If more than 10,000 files are needed, a new request must be made.
2299    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    /// Set the filename prefix to filter the file listing.
2307    ///
2308    /// If not set, all files are matched.
2309    ///
2310    /// See <https://www.backblaze.com/b2/docs/b2_list_file_names.html> for
2311    /// information on file prefixes and delimiters, and their interaction with
2312    /// each other.
2313    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    /// Set the delimiter to use to simulate a hierarchical filesystem.
2320    ///
2321    /// See <https://www.backblaze.com/b2/docs/b2_list_file_names.html> for
2322    /// information on file prefixes and delimiters, and their interaction with
2323    /// each other.
2324    pub fn delimiter(mut self, delimiter: char)
2325    -> Result<Self, FileNameValidationError> {
2326        // Because this is for a filename, we're assuming no control characters
2327        // are allowed. B2 explicitly forbids ASCII control characters; not sure
2328        // of their UTF support...
2329        if delimiter.is_ascii_control() {
2330            Err(FileNameValidationError::InvalidChar(delimiter))
2331        } else {
2332            self.delimiter = Some(delimiter);
2333            Ok(self)
2334        }
2335    }
2336
2337    /// Build a [ListFileNames] request.
2338    ///
2339    /// Returns an error if the bucket ID has not been set.
2340    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/// Get a list of file names in a bucket.
2363///
2364/// See <https://www.backblaze.com/b2/docs/b2_list_file_names.html> for more
2365/// information, including setting filename prefixes for filtering and a
2366/// delimiter for working with virtual folders.
2367#[allow(clippy::needless_lifetimes)] // False positive.
2368pub 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/// A request to list the names of files stored in a bucket.
2400#[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/// A builder for a [ListFileVersions] request.
2419#[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    /// The bucket ID from which to list files.
2431    pub fn bucket_id(mut self, id: &'a str) -> Self {
2432        self.bucket_id = Some(id);
2433        self
2434    }
2435
2436    /// The file name with which to start the listing.
2437    ///
2438    /// If the file ID is also specified, the name and ID pair is the starting
2439    /// point of the listing.
2440    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    /// The first file ID to return in the listing.
2446    ///
2447    /// If a file ID is provided, then the corresponding filename is required.
2448    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    /// The maximum number of files to return.
2454    ///
2455    /// The default is 100. The provided `count` will be clamped to a value
2456    /// between 1 and 10,000 inclusive.
2457    ///
2458    /// A single transaction has a limit of 1,000 files; values greater than
2459    /// 1,000 will incur charges for multiple transactions.
2460    ///
2461    /// If more than 10,000 files are needed, a new request must be made.
2462    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    /// Set the filename prefix to filter the file listing.
2470    ///
2471    /// If not set, all files are matched.
2472    ///
2473    /// See <https://www.backblaze.com/b2/docs/b2_list_file_names.html> for
2474    /// information on file prefixes and delimiters, and their interaction with
2475    /// each other.
2476    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    /// Set the delimiter to use to simulate a hierarchical filesystem.
2483    ///
2484    /// See <https://www.backblaze.com/b2/docs/b2_list_file_names.html> for
2485    /// information on file prefixes and delimiters, and their interaction with
2486    /// each other.
2487    pub fn delimiter(mut self, delimiter: char)
2488    -> Result<Self, FileNameValidationError> {
2489        // Because this is for a filename, we're assuming no control characters
2490        // are allowed. B2 explicitly forbids ASCII control characters; not sure
2491        // of their UTF support...
2492        if delimiter.is_ascii_control() {
2493            Err(FileNameValidationError::InvalidChar(delimiter))
2494        } else {
2495            self.delimiter = Some(delimiter);
2496            Ok(self)
2497        }
2498    }
2499
2500    /// Build a [ListFileVersions] request.
2501    ///
2502    /// Returns an error if the bucket ID has not been set.
2503    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/// List all versions of the files contained in a bucket.
2536///
2537/// Files are listed in alphabetical order by filename, then by upload timestamp
2538/// sorted descending.
2539///
2540/// See <https://www.backblaze.com/b2/docs/b2_list_file_versions.html> for more
2541/// information.
2542#[allow(clippy::needless_lifetimes)] // False positive.
2543pub 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    /// A [File] returned by [start_large_file].
2599    pub fn file(mut self, file: &'a File) -> Self {
2600        self.file_id = Some(&file.file_id);
2601        self
2602    }
2603
2604    /// The ID of a [File] returned by [start_large_file].
2605    pub fn file_id(mut self, id: &'a str) -> Self {
2606        self.file_id = Some(id);
2607        self
2608    }
2609
2610    /// The first part to return in the listing.
2611    pub fn start_part_number(mut self, num: u16) -> Self {
2612        self.start_part_number = Some(num);
2613        self
2614    }
2615
2616    /// The maximum number of parts to return.
2617    ///
2618    /// The default is 100. The provided `count` will be clamped to a value
2619    /// between 1 and 1,000 inclusive.
2620    ///
2621    /// If more than 1,000 parts are needed, a new request must be made.
2622    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/// List the parts of a large file that has not yet been completed.
2650#[allow(clippy::needless_lifetimes)] // False positive.
2651pub 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    /// The bucket ID from which to list files.
2708    pub fn bucket_id(mut self, id: &'a str) -> Self {
2709        self.bucket_id = Some(id);
2710        self
2711    }
2712
2713    /// Set the filename prefix to filter the file listing.
2714    ///
2715    /// If not set, all files are matched.
2716    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    /// The file ID with which to start the listing.
2723    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    /// The maximum number of files to return.
2729    ///
2730    /// The default is 100. The provided `count` will be clamped to a value
2731    /// between 1 and 100 inclusive.
2732    ///
2733    /// If more than 100 files are needed, a new request must be made.
2734    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    /// Create a [ListUnfinishedLargeFiles] request.
2742    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/// Get a list of the unfinished large files stored by B2.
2764///
2765/// If there are more files, returns a [ListUnfinishedLargeFiles] request that
2766/// will begin with the next file.
2767///
2768/// See <https://www.backblaze.com/b2/docs/b2_list_unfinished_large_files.html>
2769/// for further information.
2770#[allow(clippy::needless_lifetimes)] // False positive.
2771pub 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/// A request to prepare to upload a large file.
2803#[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/// A builder for a [StartLargeFile] request.
2826#[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    // To merge into file_info on build:
2837    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    /// Specify the bucket in which to store the new file.
2848    pub fn bucket_id(mut self, id: &'a str) -> Self {
2849        self.bucket_id = Some(id);
2850        self
2851    }
2852
2853    /// Set the file's name.
2854    ///
2855    /// The provided name will be percent-encoded.
2856    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    /// Set the file's MIME type.
2865    ///
2866    /// If not specified, B2 will attempt to determine the file's type.
2867    pub fn content_type(mut self, mime: impl Into<String>) -> Self {
2868        // TODO: B2 has a map of auto-detected MIME types:
2869        // https://www.backblaze.com/b2/docs/content-types.html
2870        // How do we want to deal with that?
2871        self.content_type = Some(mime.into());
2872        self
2873    }
2874
2875    /// Set file metadata to be returned in headers when downloading the file.
2876    ///
2877    /// For the following headers, use their corresponding methods instead of
2878    /// setting the values here:
2879    ///
2880    /// * X-Bz-Info-src_last_modified_millis:
2881    ///   [last_modified](Self::last_modified)
2882    /// * X-Bz-Info-large_file_sha1: [sha1_checksum](Self::sha1_checksum)
2883    /// * Content-Disposition: [content_disposition](Self::content_disposition)
2884    /// * Content-Language: [content_language](Self::content_language)
2885    /// * Expires: [expiration](Self::expiration)
2886    /// * Cache-Control: [cache_control](Self::cache_control)
2887    /// * Content-Encoding: [content_encoding](Self::content_encoding)
2888    ///
2889    /// If any of the above are set here and via their methods, the value from
2890    /// the method will override the value specified here.
2891    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    /// Set the retention policy for the file.
2898    pub fn file_retention(mut self, policy: FileRetentionPolicy) -> Self {
2899        self.file_retention = Some(policy);
2900        self
2901    }
2902
2903    /// Enable a legal hold on the file.
2904    pub fn with_legal_hold(mut self) -> Self {
2905        self.legal_hold = Some(LegalHoldValue::On);
2906        self
2907    }
2908
2909    /// Disable a legal hold on the file.
2910    pub fn without_legal_hold(mut self) -> Self {
2911        self.legal_hold = Some(LegalHoldValue::Off);
2912        self
2913    }
2914
2915    /// Set the server-side encryption configuration for the file.
2916    pub fn encryption_settings(mut self, settings: ServerSideEncryption) -> Self
2917    {
2918        self.server_side_encryption = Some(settings);
2919        self
2920    }
2921
2922    /// The time of the file's last modification.
2923    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    /// The SHA1 checksum of the file's contents.
2930    ///
2931    /// B2 will use this to verify the accuracy of the file upload, and it will
2932    /// be returned in the header `X-Bz-Content-Sha1` when downloading the file.
2933    pub fn sha1_checksum(mut self, checksum: &'a str) -> Self {
2934        self.sha1_checksum = Some(checksum);
2935        self
2936    }
2937
2938    /// The value to use for the `Content-Disposition` header when downloading
2939    /// the file.
2940    ///
2941    /// Parameter continuations are not supported.
2942    ///
2943    /// Note that the download request can override this value.
2944    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    /// The value to use for the `Content-Language` header when downloading the
2953    /// file.
2954    ///
2955    /// Note that the download request can override this value.
2956    pub fn content_language(mut self, language: impl Into<String>) -> Self {
2957        // TODO: validate content_language
2958        self.content_language = Some(percent_encode!(language.into()));
2959        self
2960    }
2961
2962    /// The value to use for the `Expires` header when the file is downloaded.
2963    ///
2964    /// Note that the download request can override this value.
2965    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    /// The value to use for the `Cache-Control` header when the file is
2973    /// downloaded.
2974    ///
2975    /// This would override the value set at the bucket level, and can be
2976    /// overriden by a download request.
2977    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    /// The value to use for the `Content-Encoding` header when the file is
2983    /// downloaded.
2984    ///
2985    /// Note that this can be overriden by a download request.
2986    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
3048/// Prepare to upload a large file in multiple parts.
3049///
3050/// After calling `start_large_file`, each thread uploading a file part should
3051/// call [get_upload_part_authorization] to obtain an upload authorization.
3052/// Then call [upload_file_part] to upload the relevant file part.
3053///
3054/// File parts can be copied from an existing file via [copy_file_part].
3055///
3056/// A large file size can be 100 MB to 10 TB (inclusive). See
3057/// <https://www.backblaze.com/b2/docs/large_files.html> for more information on
3058/// working with large files.
3059///
3060/// There must be at least two parts to a large file, with each part between 5
3061/// MB to 5 GB inclusive; the final part can be less than 5 MB but must contain
3062/// at least one byte.
3063///
3064/// See <https://www.backblaze.com/b2/docs/b2_start_large_file.html> for the B2
3065/// documentation on starting large file uploads.
3066///
3067/// The [Authorization] must have [Capability::WriteFiles].
3068// TODO: Return a LargeFile or FileInProgress type? It would only matter for
3069// something like `copy_file_part` where it provides type-safety when passing
3070// both a source file and the large (destination) file IDs together.
3071pub 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/// A request to enable or disable a legal hold on a specific file.
3100#[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    /// Create a request to enable a legal hold for the specified file.
3110    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    /// Create a request to disable a legal hold for the specified file.
3119    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    /// Use a builder to create a legal hold update request.
3128    pub fn builder() -> UpdateFileLegalHoldBuilder<'a> {
3129        UpdateFileLegalHoldBuilder::default()
3130    }
3131}
3132
3133/// A builder for an [UpdateFileLegalHold] request.
3134#[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    /// Update the legal hold status for the specified file.
3143    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    /// Update the legal hold status for a file with the specified name.
3150    ///
3151    /// Setting the [file_id](Self::file_id) is also required.
3152    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    /// Update the legal hold status for a file with the specified ID.
3159    ///
3160    /// Setting the [file_name](Self::file_name) is also required.
3161    pub fn file_id(mut self, file_id: &'a str) -> Self {
3162        self.file_id = Some(file_id);
3163        self
3164    }
3165
3166    /// Enable a legal hold.
3167    pub fn with_legal_hold(mut self) -> Self {
3168        self.legal_hold = Some(LegalHoldValue::On);
3169        self
3170    }
3171
3172    /// Disable a legal hold.
3173    pub fn without_legal_hold(mut self) -> Self {
3174        self.legal_hold = Some(LegalHoldValue::Off);
3175        self
3176    }
3177
3178    /// Build an [UpdateFileLegalHold] request.
3179    ///
3180    /// Returns an error if any of the file name, file ID, or legal hold status
3181    /// are not specified.
3182    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
3201// TODO: B2 returns the same data we sent it. Not sure there's a reason to do
3202// the same - change or continue returning ()?
3203/// Enable or disable a legal hold on a file.
3204pub 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/// A request to update file retention settings on a file.
3224#[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/// A builder for an [UpdateFileRetention] request.
3241#[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    /// The file to update.
3251    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    /// The name of the file to update.
3258    ///
3259    /// The file ID is also required.
3260    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    /// The ID of the file to update.
3267    ///
3268    /// The file name is also required.
3269    pub fn file_id(mut self, file_id: &'a str) -> Self {
3270        self.file_id = Some(file_id);
3271        self
3272    }
3273
3274    /// The new file retention settings for the file.
3275    ///
3276    /// See
3277    /// <https://www.backblaze.com/b2/docs/file_lock.html#b2_api_file_lock_parameters>
3278    /// for more information.
3279    pub fn file_retention(mut self, retention: FileRetentionSetting) -> Self {
3280        self.file_retention = Some(retention);
3281        self
3282    }
3283
3284    /// Bypass governance rules to allow deleting or shortening an existing
3285    /// governance-mode retention setting.
3286    ///
3287    /// The authorization must include [Capability::BypassGovernance].
3288    pub fn bypass_governance(mut self) -> Self {
3289        self.bypass_governance = Some(BypassGovernance::Yes);
3290        self
3291    }
3292
3293    /// Create an [UpdateFileRetention] request.
3294    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
3314// TODO: B2 returns the same data we sent it. Not sure there's a reason to do
3315// the same - change or continue returning ()?
3316/// Modify the file lock retention settings for a file.
3317///
3318/// Any attempt to delete or modify a locked file during the retention period
3319/// will fail.
3320///
3321/// The retention settings for files locked with [FileRetentionMode::Governance]
3322/// can be deleted or shortened only by accounts with
3323/// [Capability::BypassGovernance].
3324///
3325/// The retention settings for files locked with [FileRetentionMode::Compliance]
3326/// cannot be removed or shortened, but their retention dates can be extended.
3327///
3328/// The bucket containing the file must have File Lock enabled.
3329pub 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
3352/// A request to upload a file to B2.
3353///
3354/// Use [UploadFileBuilder] to create an `UploadFile`.
3355pub 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/// A builder to create an [UploadFile] request.
3372///
3373/// The [file_name](Self::file_name) and [sha1_checksum](Self::sha1_checksum)
3374/// are required.
3375///
3376/// The combined length limit of
3377/// [content_disposition](Self::content_disposition),
3378/// [content_language](Self::content_language), [expiration](Self::expiration),
3379/// [cache_control](Self::cache_control),
3380/// [content_encoding](Self::content_encoding), and custom headers is 7,000
3381/// bytes, unless self-managed encryption and/or file locks are enabled, in
3382/// which case the limit is 2,048 bytes.
3383#[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    // To merge into file_info on build.
3392    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    /// The name of the file.
3406    ///
3407    /// The provided name will be percent-encoded.
3408    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    /// The MIME type of the file's contents.
3417    ///
3418    /// This will be returned in the `Content-Type` header when downloading the
3419    /// file.
3420    ///
3421    /// If not specified, B2 will attempt to automatically set the content-type,
3422    /// defaulting to `application/octet-stream` if unable to determine its
3423    /// type.
3424    ///
3425    /// B2-recognized content-types can be viewed
3426    /// [here](https://www.backblaze.com/b2/docs/content-types.html)
3427    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    /// The SHA1 checksum of the file's contents.
3433    ///
3434    /// B2 will use this to verify the accuracy of the file upload, and it will
3435    /// be returned in the header `X-Bz-Content-Sha1` when downloading the file.
3436    pub fn sha1_checksum(mut self, checksum: &'a str) -> Self {
3437        self.sha1_checksum = Some(checksum);
3438        self
3439    }
3440
3441    /// The time of the file's last modification.
3442    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    /// The value to use for the `Content-Disposition` header when downloading
3449    /// the file.
3450    ///
3451    /// Note that the download request can override this value.
3452    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    /// The value to use for the `Content-Language` header when downloading the
3461    /// file.
3462    ///
3463    /// Note that the download request can override this value.
3464    pub fn content_language(mut self, language: impl Into<String>) -> Self {
3465        // TODO: validate content_language
3466        self.content_language = Some(percent_encode!(language.into()));
3467        self
3468    }
3469
3470    /// The value to use for the `Expires` header when the file is downloaded.
3471    ///
3472    /// Note that the download request can override this value.
3473    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    /// The value to use for the `Cache-Control` header when the file is
3481    /// downloaded.
3482    ///
3483    /// This would override the value set at the bucket level, and can be
3484    /// overriden by a download request.
3485    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    /// The value to use for the `Content-Encoding` header when the file is
3491    /// downloaded.
3492    ///
3493    /// Note that this can be overriden by a download request.
3494    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    /// Set user-specified file metadata.
3501    ///
3502    /// For the following headers, use their corresponding methods instead of
3503    /// setting the values here:
3504    ///
3505    /// * X-Bz-Info-src_last_modified_millis:
3506    ///   [last_modified](Self::last_modified)
3507    /// * X-Bz-Info-large_file_sha1: [sha1_checksum](Self::sha1_checksum)
3508    /// * Content-Disposition: [content_disposition](Self::content_disposition)
3509    /// * Content-Language: [content_language](Self::content_language)
3510    /// * Expires: [expiration](Self::expiration)
3511    /// * Cache-Control: [cache_control](Self::cache_control)
3512    /// * Content-Encoding: [content_encoding](Self::content_encoding)
3513    ///
3514    /// If any of the above are set here and via their methods, the value from
3515    /// the method will override the value specified here.
3516    ///
3517    /// Any header names that do not begin with "`X-Bz-Info-`" will have it
3518    /// prepended to the supplied name.
3519    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    /// Set a legal hold on the file.
3546    pub fn with_legal_hold(mut self) -> Self {
3547        self.legal_hold = Some(LegalHoldValue::On);
3548        self
3549    }
3550
3551    /// Disable a legal hold on the file.
3552    pub fn without_legal_hold(mut self) -> Self {
3553        self.legal_hold = Some(LegalHoldValue::Off);
3554        self
3555    }
3556
3557    /// Set the file retention mode for the file.
3558    ///
3559    /// The bucket must be File Lock-enabled and the [Authorization] must have
3560    /// [Capability::WriteFileRetentions].
3561    pub fn file_retention_mode(mut self, mode: FileRetentionMode) -> Self {
3562        self.file_retention_mode = Some(mode);
3563        self
3564    }
3565
3566    /// Set the expiration date and time of a file lock.
3567    ///
3568    /// The bucket must be File Lock-enabled and the [Authorization] must have
3569    /// [Capability::WriteFileRetentions].
3570    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    /// Set the encryption settings to use for the file.
3577    pub fn encryption_settings(mut self, settings: ServerSideEncryption)
3578    -> Self {
3579        self.encryption = Some(settings);
3580        self
3581    }
3582
3583    /// Build an [UploadFile] request.
3584    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
3642/// Upload a file to a B2 bucket.
3643///
3644/// You must first call [get_upload_authorization] to obtain an authorization to
3645/// upload files to the bucket; then pass that authorization to `upload_file`.
3646pub 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    // Unwrap safety: an `UploadAuthorization` can only be created from
3655    // `get_upload_authorization`, which will always embed an `Authorization`
3656    // reference before returning.
3657    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        // We check this here rather than when we need it below to satisfy the
3663        // borrow checker.
3664        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                &timestamp.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/// A request to upload part of a large file.
3725#[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    /// Create a request to upload the next part.
3738    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
3753/// A builder for an [UploadFilePart] request.
3754pub 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    /// Set the number of this part.
3772    ///
3773    /// Part numbers increment from 1 to 10,000 inclusive and will be clamped to
3774    /// that range if necessary.
3775    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    /// The SHA1 checkum of this part of the file.
3783    ///
3784    /// If not provided, the file part will not be immediately verified. The
3785    /// SHA1 checksums are required to [finish the file
3786    /// upload](finish_large_file_upload), so they will be verified either when
3787    /// you upload the part or at the end of the process.
3788    pub fn part_sha1_checksum(mut self, sha1: &'a str) -> Self {
3789        self.content_sha1 = sha1;
3790        self
3791    }
3792
3793    /// Set the encryption settings for the source file.
3794    ///
3795    /// This must match the settings passed to [start_large_file].
3796    pub fn server_side_encryption(mut self, encryption: ServerSideEncryption)
3797    -> Self {
3798        self.encryption = Some(encryption);
3799        self
3800    }
3801
3802    /// Create an [UploadFilePart] request to pass to [upload_file_part].
3803    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
3812/// Upload a part of a large file to B2.
3813///
3814/// Once all parts are uploaded, call [finish_large_file_upload] to merge the
3815/// parts into a single file.
3816///
3817/// If you make two uploads with the same part number, the second upload to
3818/// complete will overwrite the first.
3819///
3820/// The [Authorization] used to create the given [UploadPartAuthorization] must
3821/// have [Capability::WriteFiles].
3822///
3823/// A large file must have at least two parts, and all parts except the last
3824/// must be at least 5 MB in size. See
3825/// <https://www.backblaze.com/b2/docs/uploading.html> for further information
3826/// on uploading files.
3827///
3828/// Some errors will requiring obtaining a new [UploadPartAuthorization]. See
3829/// the B2 documentation for
3830/// [b2_upload_part](https://www.backblaze.com/b2/docs/b2_upload_part.html) or
3831/// [uploading files](https://www.backblaze.com/b2/docs/uploading.html) for
3832/// information on these errors.
3833///
3834/// # Parameters
3835///
3836/// * `auth`: An upload authorization obtained via
3837///   [get_upload_part_authorization].
3838/// * `part_num`: The part number of this part; it must be between 1 and 10,000
3839///   inclusive and increment by one for each part.
3840/// * `sha1_checksum`: The SHA1 checksum of this part of the file. You may pass
3841///   `None` to defer verification until finishing the file.
3842/// * `data`: The data part of the file.
3843///
3844/// Uploading a file part without a checksum is not recommended as it prevents
3845/// B2 from determining if the file part is corrupt, allowing you to immediately
3846/// retry.
3847// TODO: Stream-based data upload to avoid requiring all data be in RAM at once.
3848pub 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    // Unwrap safety: an `UploadPartAuthorization` can only be created from
3857    // `get_upload_part_authorization`, which will always embed an
3858    // `Authorization` reference before returning.
3859    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        // I need two copies of an identical expiration, but it doesn't
3966        // implement Clone.
3967        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    // TODO: test copy_file with a byte range.
4153
4154    #[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    // TODO: File header tests.
4205
4206    #[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    /* TODO: Setup, write these tests.
4279    #[async_std::test]
4280    async fn download_file_not_authorized() -> anyhow::Result<()> {
4281        todo!()
4282    }
4283
4284    #[async_std::test]
4285    async fn download_public_file_without_read_cap() -> anyhow::Result<()> {
4286        todo!()
4287    }
4288    */
4289
4290    #[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    // TODO: Test download with custom headers.
4313
4314    #[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                        // We don't need to store 5 MB of nothing for our test.
4357                        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        // All but the last part must be at least 5MB.
4378        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                        // We don't need to store 5 MB of nothing for our test.
4559                        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        // We need a large file that hasn't been finished yet.
4570        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            // All but the last part must be at least 5MB.
4584            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}