google_cloud_storage/http/objects/
upload.rs

1use std::borrow::Cow;
2
3use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
4use reqwest::multipart::{Form, Part};
5use reqwest_middleware::{ClientWithMiddleware as Client, RequestBuilder};
6
7use crate::http::object_access_controls::{PredefinedObjectAcl, Projection};
8use crate::http::objects::{Encryption, Object};
9use crate::http::{Error, Escape};
10
11#[derive(Clone, Debug)]
12pub struct Media {
13    pub name: Cow<'static, str>,
14    pub content_type: Cow<'static, str>,
15    pub content_length: Option<u64>,
16}
17
18impl Media {
19    pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
20        Self {
21            name: name.into(),
22            content_type: "application/octet-stream".into(),
23            content_length: None,
24        }
25    }
26}
27
28#[derive(Clone, Debug)]
29pub enum UploadType {
30    Simple(Media),
31    Multipart(Box<Object>),
32}
33
34#[derive(Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize, Debug, Default)]
35#[serde(rename_all = "camelCase")]
36pub struct UploadObjectRequest {
37    /// Name of the bucket in which to store the new object.
38    /// Overrides the provided object metadata's bucket value, if any.
39    #[serde(skip_serializing)]
40    pub bucket: String,
41    pub generation: Option<i64>,
42    /// Makes the operation conditional on whether the object's current generation
43    /// matches the given value. Setting to 0 makes the operation succeed only if
44    /// there are no live versions of the object.
45    pub if_generation_match: Option<i64>,
46    /// Makes the operation conditional on whether the object's current generation
47    /// does not match the given value. If no live object exists, the precondition
48    /// fails. Setting to 0 makes the operation succeed only if there is a live
49    /// version of the object.
50    pub if_generation_not_match: Option<i64>,
51    /// Makes the operation conditional on whether the object's current
52    /// metageneration matches the given value.
53    pub if_metageneration_match: Option<i64>,
54    /// Makes the operation conditional on whether the object's current
55    /// metageneration does not match the given value.
56    pub if_metageneration_not_match: Option<i64>,
57    /// Resource name of the Cloud KMS key that will be used to encrypt the object.
58    /// If not specified, the request uses the bucket's default Cloud KMS key, if any,
59    /// or a Google-managed encryption key.
60    pub kms_key_name: Option<String>,
61    ///Apply a predefined set of access controls to this object.
62    /// Acceptable values are:
63    /// authenticatedRead: Object owner gets OWNER access, and allAuthenticatedUsers get READER access.
64    /// bucketOwnerFullControl: Object owner gets OWNER access, and project team owners get OWNER access.
65    /// bucketOwnerRead: Object owner gets OWNER access, and project team owners get READER access.
66    /// private: Object owner gets OWNER access.
67    /// projectPrivate: Object owner gets OWNER access, and project team members get access according to their roles.
68    /// publicRead: Object owner gets OWNER access, and allUsers get READER access.
69    /// If iamConfiguration.uniformBucketLevelAccess.enabled is set to true,
70    /// requests that include this parameter fail with a 400 Bad Request response.
71    pub predefined_acl: Option<PredefinedObjectAcl>,
72    /// Set of properties to return. Defaults to noAcl,
73    /// unless the object resource specifies the acl property, when it defaults to full.
74    /// Acceptable values are:
75    /// full: Include all properties.
76    /// noAcl: Omit the owner, acl property.
77    pub projection: Option<Projection>,
78    #[serde(skip_serializing)]
79    pub encryption: Option<Encryption>,
80}
81
82pub(crate) fn build<T: Into<reqwest::Body>>(
83    base_url: &str,
84    client: &Client,
85    req: &UploadObjectRequest,
86    media: &Media,
87    body: T,
88) -> RequestBuilder {
89    let url = format!("{}/b/{}/o?uploadType=media", base_url, req.bucket.escape(),);
90    let mut builder = client
91        .post(url)
92        .query(&req)
93        .query(&[("name", &media.name)])
94        .body(body)
95        .header(CONTENT_TYPE, media.content_type.to_string());
96
97    if let Some(len) = media.content_length {
98        builder = builder.header(CONTENT_LENGTH, len.to_string())
99    }
100    if let Some(e) = &req.encryption {
101        e.with_headers(builder)
102    } else {
103        builder
104    }
105}
106
107pub(crate) fn build_multipart<T: Into<reqwest::Body>>(
108    base_url: &str,
109    client: &Client,
110    req: &UploadObjectRequest,
111    metadata: &Object,
112    body: T,
113) -> Result<RequestBuilder, Error> {
114    let url = format!("{}/b/{}/o?uploadType=multipart", base_url, req.bucket.escape(),);
115    let form = Form::new();
116    let metadata_part = Part::text(serde_json::to_string(metadata).expect("object serialize failed"))
117        .mime_str("application/json; charset=UTF-8")?;
118    let data_part = Part::stream(body);
119    let form = form.part("metadata", metadata_part).part("data", data_part);
120
121    // Content-Length is automatically set by multipart
122    let builder = client.post(url).query(&req).multipart(form);
123
124    Ok(if let Some(e) = &req.encryption {
125        e.with_headers(builder)
126    } else {
127        builder
128    })
129}
130
131pub(crate) fn build_resumable_session_simple(
132    base_url: &str,
133    client: &Client,
134    req: &UploadObjectRequest,
135    media: &Media,
136) -> RequestBuilder {
137    let url = format!("{}/b/{}/o?uploadType=resumable", base_url, req.bucket.escape(),);
138    let mut builder = client
139        .post(url)
140        .query(&req)
141        .query(&[("name", &media.name)])
142        .header(CONTENT_LENGTH, 0)
143        .header("X-Upload-Content-Type", media.content_type.to_string());
144    if let Some(len) = media.content_length {
145        builder = builder.header("X-Upload-Content-Length", len)
146    }
147    if let Some(e) = &req.encryption {
148        e.with_headers(builder)
149    } else {
150        builder
151    }
152}
153
154pub(crate) fn build_resumable_session_metadata(
155    base_url: &str,
156    client: &Client,
157    req: &UploadObjectRequest,
158    metadata: &Object,
159) -> RequestBuilder {
160    let url = format!("{}/b/{}/o?uploadType=resumable", base_url, req.bucket.escape(),);
161    let builder = client.post(url).query(&req).json(&metadata);
162    if let Some(e) = &req.encryption {
163        e.with_headers(builder)
164    } else {
165        builder
166    }
167}