cloud_storage_rs/resources/
object.rs

1use crate::error::{Error, GoogleResponse};
2pub use crate::resources::bucket::Owner;
3use crate::resources::object_access_control::ObjectAccessControl;
4use crate::resources::common::ListResponse;
5use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
6
7/// A resource representing a file in Google Cloud Storage.
8#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct Object {
11    /// The kind of item this is. For objects, this is always `storage#object`.
12    pub kind: String,
13    /// The ID of the object, including the bucket name, object name, and generation number.
14    pub id: String,
15    /// The link to this object.
16    pub self_link: String,
17    /// The name of the object. Required if not specified by URL parameter.
18    pub name: String,
19    /// The name of the bucket containing this object.
20    pub bucket: String,
21    /// The content generation of this object. Used for object versioning.
22    #[serde(deserialize_with = "crate::from_str")]
23    pub generation: i64,
24    /// The version of the metadata for this object at this generation. Used for preconditions and
25    /// for detecting changes in metadata. A metageneration number is only meaningful in the context
26    /// of a particular generation of a particular object.
27    #[serde(deserialize_with = "crate::from_str")]
28    pub metageneration: i64,
29    /// Content-Type of the object data. If an object is stored without a Content-Type, it is served
30    /// as application/octet-stream.
31    pub content_type: Option<String>,
32    /// The creation time of the object in RFC 3339 format.
33    pub time_created: chrono::DateTime<chrono::Utc>,
34    /// The modification time of the object metadata in RFC 3339 format.
35    pub updated: chrono::DateTime<chrono::Utc>,
36    /// The deletion time of the object in RFC 3339 format. Returned if and only if this version of
37    /// the object is no longer a live version, but remains in the bucket as a noncurrent version.
38    pub time_deleted: Option<chrono::DateTime<chrono::Utc>>,
39    /// Whether or not the object is subject to a temporary hold.
40    pub temporary_hold: Option<bool>,
41    /// Whether or not the object is subject to an event-based hold.
42    pub event_based_hold: Option<bool>,
43    /// The earliest time that the object can be deleted, based on a bucket's retention policy, in
44    /// RFC 3339 format.
45    pub retention_expiration_time: Option<chrono::DateTime<chrono::Utc>>,
46    /// Storage class of the object.
47    pub storage_class: String,
48    /// The time at which the object's storage class was last changed. When the object is initially
49    /// created, it will be set to timeCreated.
50    pub time_storage_class_updated: chrono::DateTime<chrono::Utc>,
51    /// Content-Length of the data in bytes.
52    #[serde(deserialize_with = "crate::from_str")]
53    pub size: u64,
54    /// MD5 hash of the data; encoded using base64. For more information about using the MD5 hash,
55    /// see Hashes and ETags: Best Practices.
56    pub md5_hash: Option<String>,
57    /// Media download link.
58    pub media_link: String,
59    /// Content-Encoding of the object data.
60    pub content_encoding: Option<String>,
61    /// Content-Disposition of the object data.
62    pub content_disposition: Option<String>,
63    /// Content-Language of the object data.
64    pub content_language: Option<String>,
65    /// Cache-Control directive for the object data. If omitted, and the object is accessible to all
66    /// anonymous users, the default will be public, max-age=3600.
67    pub cache_control: Option<String>,
68    /// User-provided metadata, in key/value pairs.
69    pub metadata: Option<std::collections::HashMap<String, String>>,
70    /// Access controls on the object, containing one or more objectAccessControls Resources. If
71    /// iamConfiguration.uniformBucketLevelAccess.enabled is set to true, this field is omitted in
72    /// responses, and requests that specify this field fail.
73    pub acl: Option<Vec<ObjectAccessControl>>,
74    /// The owner of the object. This will always be the uploader of the object. If
75    /// `iamConfiguration.uniformBucketLevelAccess.enabled` is set to true, this field does not
76    /// apply, and is omitted in responses.   
77    pub owner: Option<Owner>,
78    /// CRC32c checksum, as described in RFC 4960, Appendix B; encoded using base64 in big-endian
79    /// byte order. For more information about using the CRC32c checksum, see Hashes and ETags: Best
80    /// Practices.
81    pub crc32c: String,
82    /// Number of underlying components that make up a composite object. Components are accumulated
83    /// by compose operations, counting 1 for each non-composite source object and componentCount
84    /// for each composite source object. Note: componentCount is included in the metadata for
85    /// composite objects only.
86    #[serde(default, deserialize_with = "crate::from_str_opt")]
87    pub component_count: Option<i32>,
88    /// HTTP 1.1 Entity tag for the object.
89    pub etag: String,
90    /// Metadata of customer-supplied encryption key, if the object is encrypted by such a key.
91    pub customer_encryption: Option<CustomerEncrypton>,
92    /// Cloud KMS Key used to encrypt this object, if the object is encrypted by such a key.
93    pub kms_key_name: Option<String>,
94}
95
96/// Contains data about how a user might encrypt their files in Google Cloud Storage.
97#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct CustomerEncrypton {
100    /// The encryption algorithm.
101    pub encryption_algorithm: String,
102    /// SHA256 hash value of the encryption key.
103    pub key_sha256: String,
104}
105
106/// The request that is supplied to perform `Object::compose`.
107#[derive(Debug, PartialEq, serde::Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct ComposeRequest {
110    /// The kind of item this is. Will always be `storage#composeRequest`.
111    pub kind: String,
112    /// The list of source objects that will be concatenated into a single object.
113    pub source_objects: Vec<SourceObject>,
114    /// Properties of the resulting object.
115    pub destination: Option<Object>,
116}
117
118/// A SourceObject represents one of the objects that is to be composed.
119#[derive(Debug, PartialEq, serde::Serialize)]
120#[serde(rename_all = "camelCase")]
121pub struct SourceObject {
122    /// The source object's name. All source objects must have the same storage class and reside in
123    /// the same bucket.
124    pub name: String,
125    /// The generation of this object to use as the source.
126    pub generation: Option<i64>,
127    /// Conditions that must be met for this operation to execute.
128    pub object_preconditions: Option<ObjectPrecondition>,
129}
130
131/// Allows conditional copying of this file.
132#[derive(Debug, PartialEq, serde::Serialize)]
133#[serde(rename_all = "camelCase")]
134pub struct ObjectPrecondition {
135    /// Only perform the composition if the generation of the source object that would be used
136    /// matches this value. If this value and a generation are both specified, they must be the same
137    /// value or the call will fail.
138    pub if_generation_match: i64,
139}
140
141#[derive(Debug, serde::Deserialize)]
142#[serde(rename_all = "camelCase")]
143struct ObjectList {
144    kind: String,
145    items: Vec<Object>,
146}
147
148#[derive(Debug, serde::Deserialize)]
149#[serde(rename_all = "camelCase")]
150struct RewriteResponse {
151    kind: String,
152    total_bytes_rewritten: String,
153    object_size: String,
154    done: bool,
155    resource: Object,
156}
157
158impl Object {
159    /// Create a new object.
160    /// Upload a file as that is loaded in memory to google cloud storage, where it will be
161    /// interpreted according to the mime type you specified.
162    /// ## Example
163    /// ```rust,no_run
164    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
165    /// # fn read_cute_cat(_in: &str) -> Vec<u8> { vec![0, 1] }
166    /// use cloud_storage::Object;
167    ///
168    /// let file: Vec<u8> = read_cute_cat("cat.png");
169    /// Object::create("cat-photos", &file, "recently read cat.png", "image/png")
170    ///     .expect("cat not uploaded");
171    /// # Ok(())
172    /// # }
173    /// ```
174    pub fn create(
175        bucket: &str,
176        file: &[u8],
177        filename: &str,
178        mime_type: &str,
179    ) -> Result<Self, Error> {
180        use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
181
182        // has its own url for some reason
183        const BASE_URL: &str = "https://www.googleapis.com/upload/storage/v1/b";
184        let client = reqwest::blocking::Client::new();
185        let url = &format!("{}/{}/o?uploadType=media&name={}",
186            BASE_URL,
187            percent_encode(&bucket),
188            percent_encode(&filename),
189        );
190        let mut headers = crate::get_headers()?;
191        headers.insert(CONTENT_TYPE, mime_type.to_string().parse()?);
192        headers.insert(CONTENT_LENGTH, file.len().to_string().parse()?);
193        let response = client
194            .post(url)
195            .headers(headers)
196            .body(file.to_owned())
197            .send()?;
198        if response.status() == 200 {
199            Ok(serde_json::from_str(&response.text()?)?)
200        } else {
201            Err(Error::new(&response.text()?))
202        }
203    }
204
205
206    /// Create a new object. This works in the same way as `Object::create`, except it does not need
207    /// to load the entire file in ram.
208    /// ## Example
209    /// ```rust,no_run
210    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
211    /// # fn read_cute_cat(_in: &str) -> Vec<u8> { vec![0, 1] }
212    /// use cloud_storage::Object;
213    ///
214    /// let mut file = std::io::Cursor::new(read_cute_cat("cat.png"));
215    /// Object::create_streamed("cat-photos", file, 10, "recently read cat.png", "image/png")
216    ///     .expect("cat not uploaded");
217    /// Ok(())
218    /// # }
219    /// ```
220    pub fn create_streamed<R: std::io::Read + Send + 'static>(
221        bucket: &str,
222        file: R,
223        length: u64,
224        filename: &str,
225        mime_type: &str,
226    ) -> Result<Self, Error> {
227        use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
228
229        // has its own url for some reason
230        const BASE_URL: &str = "https://www.googleapis.com/upload/storage/v1/b";
231        let client = reqwest::blocking::Client::new();
232        let url = &format!(
233            "{}/{}/o?uploadType=media&name={}",
234            BASE_URL,
235            percent_encode(&bucket),
236            percent_encode(&filename),
237        );
238        let mut headers = crate::get_headers()?;
239        headers.insert(CONTENT_TYPE, mime_type.to_string().parse()?);
240        headers.insert(CONTENT_LENGTH, length.to_string().parse()?);
241        let body = reqwest::blocking::Body::sized(file, length);
242        let response = client
243            .post(url)
244            .headers(headers)
245            .body(body)
246            .send()?;
247        if response.status() == 200 {
248            Ok(serde_json::from_str(&response.text()?)?)
249        } else {
250            Err(Error::new(&response.text()?))
251        }
252    }
253
254    /// Obtain a list of objects within this Bucket.
255    /// ### Example
256    /// ```no_run
257    /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 
258    /// use cloud_storage::Object;
259    ///
260    /// let all_objects = Object::list("my_bucket")?;
261    /// # Ok(())
262    /// # }
263    /// ```
264    pub fn list(bucket: &str) -> Result<Vec<Self>, Error> {
265        Self::list_from(bucket, None, None)
266    }
267
268    /// Obtain a list of objects by prefix within this Bucket .
269    /// ### Example
270    /// ```no_run
271    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
272    /// use cloud_storage::Object;
273    ///
274    /// let all_objects = Object::list_prefix("my_bucket", "prefix/")?;
275    /// # Ok(())
276    /// # }
277    /// ```
278    pub fn list_prefix(bucket: &str, prefix: &str) -> Result<Vec<Self>, Error> {
279        Self::list_from(bucket, Some(prefix), None)
280    }
281
282    fn list_from(bucket: &str,  prefix: Option<&str>, page_token: Option<&str>) -> Result<Vec<Self>, Error> {
283        let url = format!("{}/b/{}/o", crate::BASE_URL, percent_encode(bucket));
284        let client = reqwest::blocking::Client::new();
285        let mut query = if let Some(page_token) = page_token {
286            vec![("pageToken", page_token)]
287        } else {
288            vec![]
289        };
290        if let Some(prefix) = prefix {
291            query.push(("prefix", prefix));
292        };
293
294        let result: GoogleResponse<ListResponse<Self>> = client
295            .get(&url)
296            .query(&query)
297            .headers(crate::get_headers()?)
298            .send()?
299            .json()?;
300        match result {
301            GoogleResponse::Success(mut s) => {
302                if let Some(page_token) = s.next_page_token {
303                    s.items.extend(Self::list_from(bucket, prefix, Some(&page_token))?.into_iter());
304                }
305                Ok(s.items)
306            },
307            GoogleResponse::Error(e) => Err(e.into()),
308        }
309    }
310
311    /// Obtains a single object with the specified name in the specified bucket.
312    /// ### Example
313    /// ```no_run
314    /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 
315    /// use cloud_storage::Object;
316    ///
317    /// let object = Object::read("my_bucket", "path/to/my/file.png")?;
318    /// # Ok(())
319    /// # }
320    /// ```
321    pub fn read(bucket: &str, file_name: &str) -> Result<Self, Error> {
322        let url = format!(
323            "{}/b/{}/o/{}",
324            crate::BASE_URL,
325            percent_encode(bucket),
326            percent_encode(file_name),
327        );
328        let client = reqwest::blocking::Client::new();
329        let result: GoogleResponse<Self> = client
330            .get(&url)
331            .headers(crate::get_headers()?)
332            .send()?
333            .json()?;
334        match result {
335            GoogleResponse::Success(s) => Ok(s),
336            GoogleResponse::Error(e) => Err(e.into()),
337        }
338    }
339
340    /// Download the content of the object with the specified name in the specified bucket.
341    /// ### Example
342    /// ```no_run
343    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
344    /// use cloud_storage::Object;
345    ///
346    /// let bytes = Object::download("my_bucket", "path/to/my/file.png")?;
347    /// # Ok(())
348    /// # }
349    /// ```
350    pub fn download(bucket: &str, file_name: &str) -> Result<Vec<u8>, Error> {
351        let url = format!(
352            "{}/b/{}/o/{}?alt=media",
353            crate::BASE_URL,
354            percent_encode(bucket),
355            percent_encode(file_name),
356        );
357        let client = reqwest::blocking::Client::new();
358        Ok(client
359            .get(&url)
360            .headers(crate::get_headers()?)
361            .send()?
362            .bytes()?.to_vec())
363    }
364
365    /// Obtains a single object with the specified name in the specified bucket.
366    /// ### Example
367    /// ```no_run
368    /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 
369    /// use cloud_storage::Object;
370    ///
371    /// let mut object = Object::read("my_bucket", "path/to/my/file.png")?;
372    /// object.content_type = Some("application/xml".to_string());
373    /// object.update();
374    /// # Ok(())
375    /// # }
376    /// ```
377    pub fn update(&self) -> Result<Self, Error> {
378        let url = format!("{}/b/{}/o/{}",
379            crate::BASE_URL,
380            percent_encode(&self.bucket),
381            percent_encode(&self.name),
382        );
383        let client = reqwest::blocking::Client::new();
384        let result: GoogleResponse<Self> = client
385            .put(&url)
386            .headers(crate::get_headers()?)
387            .json(&self)
388            .send()?
389            .json()?;
390        match result {
391            GoogleResponse::Success(s) => Ok(s),
392            GoogleResponse::Error(e) => Err(e.into()),
393        }
394    }
395
396    /// Deletes a single object with the specified name in the specified bucket.
397    /// ### Example
398    /// ```no_run
399    /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 
400    /// use cloud_storage::Object;
401    ///
402    /// Object::delete("my_bucket", "path/to/my/file.png")?;
403    /// # Ok(())
404    /// # }
405    /// ```
406    pub fn delete(bucket: &str, file_name: &str) -> Result<(), Error> {
407        let url = format!("{}/b/{}/o/{}",
408            crate::BASE_URL,
409            percent_encode(bucket),
410            percent_encode(file_name),
411        );
412        let client = reqwest::blocking::Client::new();
413        let response = client.delete(&url).headers(crate::get_headers()?).send()?;
414        if response.status().is_success() {
415            Ok(())
416        } else {
417            Err(Error::Google(response.json()?))
418        }
419    }
420
421    /// Obtains a single object with the specified name in the specified bucket.
422    /// ### Example
423    /// ```no_run
424    /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 
425    /// use cloud_storage::object::{Object, ComposeRequest, SourceObject};
426    ///
427    /// let obj1 = Object::read("my_bucket", "file1")?;
428    /// let obj2 = Object::read("my_bucket", "file2")?;
429    /// let compose_request = ComposeRequest {
430    ///     kind: "storage#composeRequest".to_string(),
431    ///     source_objects: vec![
432    ///         SourceObject {
433    ///             name: obj1.name.clone(),
434    ///             generation: None,
435    ///             object_preconditions: None,
436    ///         },
437    ///         SourceObject {
438    ///             name: obj2.name.clone(),
439    ///             generation: None,
440    ///             object_preconditions: None,
441    ///         },
442    ///     ],
443    ///     destination: None,
444    /// };
445    /// let obj3 = Object::compose("my_bucket", &compose_request, "test-concatted-file")?;
446    /// // obj3 is now a file with the content of obj1 and obj2 concatted together.
447    /// # Ok(())
448    /// # }
449    /// ```
450    pub fn compose(
451        bucket: &str,
452        req: &ComposeRequest,
453        destination_object: &str,
454    ) -> Result<Self, Error> {
455        let url = format!(
456            "{}/b/{}/o/{}/compose",
457            crate::BASE_URL,
458            percent_encode(&bucket),
459            percent_encode(&destination_object)
460        );
461        let client = reqwest::blocking::Client::new();
462        let result: GoogleResponse<Self> = client
463            .post(&url)
464            .headers(crate::get_headers()?)
465            .json(req)
466            .send()?
467            .json()?;
468        match result {
469            GoogleResponse::Success(s) => Ok(s),
470            GoogleResponse::Error(e) => Err(e.into()),
471        }
472    }
473
474    /// Copy this object to the target bucket and path
475    /// ### Example
476    /// ```no_run
477    /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 
478    /// use cloud_storage::object::{Object, ComposeRequest};
479    ///
480    /// let obj1 = Object::read("my_bucket", "file1")?;
481    /// let obj2 = obj1.copy("my_other_bucket", "file2")?;
482    /// // obj2 is now a copy of obj1.
483    /// # Ok(())
484    /// # }
485    /// ```
486    pub fn copy(&self, destination_bucket: &str, path: &str) -> Result<Self, Error> {
487        use reqwest::header::CONTENT_LENGTH;
488
489        let url = format!(
490            "{base}/b/{sBucket}/o/{sObject}/copyTo/b/{dBucket}/o/{dObject}",
491            base=crate::BASE_URL,
492            sBucket=percent_encode(&self.bucket),
493            sObject=percent_encode(&self.name),
494            dBucket=percent_encode(&destination_bucket),
495            dObject=percent_encode(&path),
496        );
497        let client = reqwest::blocking::Client::new();
498        let mut headers = crate::get_headers()?;
499        headers.insert(CONTENT_LENGTH, "0".parse()?);
500        let result: GoogleResponse<Self> = client.post(&url).headers(headers).send()?.json()?;
501        match result {
502            GoogleResponse::Success(s) => Ok(s),
503            GoogleResponse::Error(e) => Err(e.into()),
504        }
505    }
506
507    /// Moves a file from the current location to the target bucket and path.
508    ///
509    /// ## Limitations
510    /// This function does not yet support rewriting objects to another
511    /// * Geographical Location,
512    /// * Encryption,
513    /// * Storage class.
514    /// These limitations mean that for now, the rewrite and the copy methods do the same thing.
515    /// ### Example
516    /// ```no_run
517    /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 
518    /// use cloud_storage::object::Object;
519    ///
520    /// let obj1 = Object::read("my_bucket", "file1")?;
521    /// let obj2 = obj1.rewrite("my_other_bucket", "file2")?;
522    /// // obj2 is now a copy of obj1.
523    /// # Ok(())
524    /// # }
525    /// ```
526    pub fn rewrite(&self, destination_bucket: &str, path: &str) -> Result<Self, Error> {
527        use reqwest::header::CONTENT_LENGTH;
528
529        let url = format!(
530            "{base}/b/{sBucket}/o/{sObject}/rewriteTo/b/{dBucket}/o/{dObject}",
531            base=crate::BASE_URL,
532            sBucket=percent_encode(&self.bucket),
533            sObject=percent_encode(&self.name),
534            dBucket=percent_encode(destination_bucket),
535            dObject=percent_encode(path),
536        );
537        let client = reqwest::blocking::Client::new();
538        let mut headers = crate::get_headers()?;
539        headers.insert(CONTENT_LENGTH, "0".parse()?);
540        let result: GoogleResponse<RewriteResponse> =
541            client.post(&url).headers(headers).send()?.json()?;
542        match result {
543            GoogleResponse::Success(s) => Ok(s.resource),
544            GoogleResponse::Error(e) => Err(e.into()),
545        }
546    }
547
548    /// Creates a [Signed Url](https://cloud.google.com/storage/docs/access-control/signed-urls)
549    /// which is valid for `duration` seconds, and lets the posessor download the file contents
550    /// without any authentication.
551    /// ### Example
552    /// ```no_run
553    /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 
554    /// use cloud_storage::object::{Object, ComposeRequest};
555    ///
556    /// let obj1 = Object::read("my_bucket", "file1")?;
557    /// let url = obj1.download_url(50)?;
558    /// // url is now a url to which an unauthenticated user can make a request to download a file
559    /// // for 50 seconds.
560    /// # Ok(())
561    /// # }
562    /// ```
563    pub fn download_url(&self, duration: u32) -> Result<String, Error> {
564        self.sign(&self.name, duration, "GET")
565    }
566
567    // /// Creates a [Signed Url](https://cloud.google.com/storage/docs/access-control/signed-urls)
568    // /// which is valid for `duration` seconds, and lets the posessor upload new file contents.
569    // /// without any authentication.
570    // pub fn upload_url(&self, duration: u32) -> Result<String, Error> {
571    //     self.sign(&self.name, duration, "POST")
572    // }
573
574    #[inline(always)]
575    fn sign(&self, file_path: &str, duration: u32, http_verb: &str) -> Result<String, Error> {
576        use openssl::sha;
577
578        if duration > 604800 {
579            let msg = format!("duration may not be greater than 604800, but was {}", duration);
580            return Err(Error::Other(msg));
581        }
582
583        // 1 construct the canonical reques
584        let issue_date = chrono::Utc::now();
585        let file_path = self.path_to_resource(file_path);
586        let query_string = Self::get_canonical_query_string(&issue_date, duration);
587        let canonical_request = self.get_canonical_request(&file_path, &query_string, http_verb);
588
589        // 2 get hex encoded SHA256 hash the canonical request
590        let hash = sha::sha256(canonical_request.as_bytes());
591        let hex_hash = hex::encode(hash);
592
593        // 3 construct the string to sign
594        let string_to_sign = format!(
595            "{signing_algorithm}\n\
596            {current_datetime}\n\
597            {credential_scope}\n\
598            {hashed_canonical_request}",
599            signing_algorithm="GOOG4-RSA-SHA256",
600            current_datetime=issue_date.format("%Y%m%dT%H%M%SZ"),
601            credential_scope=Self::get_credential_scope(&issue_date),
602            hashed_canonical_request=hex_hash,
603        );
604
605        // 4 sign the string to sign with RSA - SHA256
606        let buffer = Self::sign_str(&string_to_sign);
607        let signature = hex::encode(&buffer?);
608
609        // 5 construct the signed url
610        Ok(format!(
611            "https://storage.googleapis.com{path_to_resource}?\
612            {query_string}&\
613            X-Goog-Signature={request_signature}",
614            path_to_resource=file_path,
615            query_string=query_string,
616            request_signature=signature,
617        ))
618    }
619
620    #[inline(always)]
621    fn get_canonical_request(&self, path: &str, query_string: &str, http_verb: &str) -> String {
622        format!(
623            "{http_verb}\n\
624            {path_to_resource}\n\
625            {canonical_query_string}\n\
626            {canonical_headers}\n\
627            \n\
628            {signed_headers}\n\
629            {payload}",
630            http_verb=http_verb,
631            path_to_resource=path,
632            canonical_query_string=query_string,
633            canonical_headers="host:storage.googleapis.com",
634            signed_headers="host",
635            payload="UNSIGNED-PAYLOAD",
636        )
637    }
638
639    #[inline(always)]
640    fn get_canonical_query_string(date: &chrono::DateTime<chrono::Utc>, exp: u32) -> String {
641        let credential = format!(
642            "{authorizer}/{scope}",
643            authorizer=crate::SERVICE_ACCOUNT.client_email,
644            scope=Self::get_credential_scope(date),
645        );
646        format!(
647            "X-Goog-Algorithm={algo}&\
648            X-Goog-Credential={cred}&\
649            X-Goog-Date={date}&\
650            X-Goog-Expires={exp}&\
651            X-Goog-SignedHeaders={signed}",
652            algo="GOOG4-RSA-SHA256",
653            cred=percent_encode(&credential),
654            date=date.format("%Y%m%dT%H%M%SZ"),
655            exp=exp,
656            signed="host",
657        )
658    }
659
660    #[inline(always)]
661    fn path_to_resource(&self, path: &str) -> String {
662        format!(
663            "/{bucket}/{file_path}",
664            bucket=self.bucket,
665            file_path=percent_encode_noslash(path),
666        )
667    }
668
669    #[inline(always)]
670    fn get_credential_scope(date: &chrono::DateTime<chrono::Utc>) -> String {
671        format!("{}/henk/storage/goog4_request", date.format("%Y%m%d"))
672    }
673
674    #[inline(always)]
675    fn sign_str(message: &str) -> Result<Vec<u8>, Error> {
676        use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer};
677
678        let key = PKey::private_key_from_pem(crate::SERVICE_ACCOUNT.private_key.as_bytes())?;
679        let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
680        signer.update(message.as_bytes())?;
681        Ok(signer.sign_to_vec()?)
682    }
683}
684
685const ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC.remove(b'*').remove(b'-').remove(b'.').remove(b'_');
686
687const NOSLASH_ENCODE_SET: &AsciiSet = &ENCODE_SET.remove(b'/').remove(b'~');
688
689// We need to be able to percent encode stuff, but without touching the slashes in filenames. To
690// this end we create an implementation that does this, without touching the slashes.
691fn percent_encode_noslash(input: &str) -> String {
692    utf8_percent_encode(input, NOSLASH_ENCODE_SET).to_string()
693}
694
695fn percent_encode(input: &str) -> String {
696    utf8_percent_encode(input, ENCODE_SET).to_string()
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702
703    #[test]
704    fn create() -> Result<(), Box<dyn std::error::Error>> {
705        let bucket = crate::read_test_bucket();
706        Object::create(&bucket.name, &[0, 1], "test-create", "text/plain")?;
707        Ok(())
708    }
709
710    #[test]
711    fn create_streamed() -> Result<(), Box<dyn std::error::Error>> {
712        let bucket = crate::read_test_bucket();
713        let cursor = std::io::Cursor::new([0, 1]);
714        Object::create_streamed(&bucket.name, cursor, 2, "test-create-streamed", "text/plain")?;
715        Ok(())
716    }
717
718    #[test]
719    fn list() -> Result<(), Box<dyn std::error::Error>> {
720        let test_bucket = crate::read_test_bucket();
721        Object::list(&test_bucket.name)?;
722        Ok(())
723    }
724
725    #[test]
726    fn list_prefix() -> Result<(), Box<dyn std::error::Error>> {
727        let test_bucket = crate::read_test_bucket();
728
729        let prefix_names = [
730            "test-list-prefix/1",
731            "test-list-prefix/2",
732            "test-list-prefix/sub/1",
733            "test-list-prefix/sub/2",
734        ];
735
736        for name in &prefix_names {
737            Object::create(&test_bucket.name, &[0, 1], name, "text/plain")?;
738        }
739
740        let list = Object::list_prefix(&test_bucket.name, "test-list-prefix/")?;
741        assert_eq!(list.len(), 4);
742        let list = Object::list_prefix(&test_bucket.name, "test-list-prefix/sub")?;
743        assert_eq!(list.len(), 2);
744        Ok(())
745    }
746
747
748    #[test]
749    fn read() -> Result<(), Box<dyn std::error::Error>> {
750        let bucket = crate::read_test_bucket();
751        Object::create(&bucket.name, &[0, 1], "test-read", "text/plain")?;
752        Object::read(&bucket.name, "test-read")?;
753        Ok(())
754    }
755
756    #[test]
757    fn download() -> Result<(), Box<dyn std::error::Error>> {
758        let bucket = crate::read_test_bucket();
759        let content = b"hello world";
760        Object::create(&bucket.name, content, "test-download", "application/octet-stream")?;
761
762        let data = Object::download(&bucket.name, "test-download")?;
763        assert_eq!(data, content);
764
765        Ok(())
766    }
767
768    #[test]
769    fn update() -> Result<(), Box<dyn std::error::Error>> {
770        let bucket = crate::read_test_bucket();
771        let mut obj = Object::create(&bucket.name, &[0, 1], "test-update", "text/plain")?;
772        obj.content_type = Some("application/xml".to_string());
773        obj.update()?;
774        Ok(())
775    }
776
777    #[test]
778    fn delete() -> Result<(), Box<dyn std::error::Error>> {
779        let bucket = crate::read_test_bucket();
780        Object::create(&bucket.name, &[0, 1], "test-delete", "text/plain")?;
781
782        Object::delete(&bucket.name, "test-delete")?;
783
784        let list = Object::list_prefix(&bucket.name, "test-delete")?;
785        assert!(list.is_empty());
786
787        Ok(())
788    }
789
790    #[test]
791    fn delete_nonexistent() -> Result<(), Box<dyn std::error::Error>> {
792        let bucket = crate::read_test_bucket();
793
794        let nonexistent_object = "test-delete-nonexistent";
795
796        let delete_result = Object::delete(&bucket.name, nonexistent_object);
797
798        if let Err(Error::Google(google_error_response)) = delete_result {
799            assert!(google_error_response.to_string().contains(
800                &format!("No such object: {}/{}", bucket.name, nonexistent_object)));
801        } else {
802            panic!("Expected a Google error, instead got {:?}", delete_result);
803        }
804
805        Ok(())
806    }
807
808    #[test]
809    fn compose() -> Result<(), Box<dyn std::error::Error>> {
810        let bucket = crate::read_test_bucket();
811        let obj1 = Object::create(&bucket.name, &[0, 1], "test-compose-1", "text/plain")?;
812        let obj2 = Object::create(&bucket.name, &[2, 3], "test-compose-2", "text/plain")?;
813        let compose_request = ComposeRequest {
814            kind: "storage#composeRequest".to_string(),
815            source_objects: vec![
816                SourceObject {
817                    name: obj1.name.clone(),
818                    generation: None,
819                    object_preconditions: None,
820                },
821                SourceObject {
822                    name: obj2.name.clone(),
823                    generation: None,
824                    object_preconditions: None,
825                },
826            ],
827            destination: None,
828        };
829        let obj3 = Object::compose(&bucket.name, &compose_request, "test-concatted-file")?;
830        let url = obj3.download_url(100)?;
831        let content = reqwest::blocking::get(&url)?.text()?;
832        assert_eq!(content.as_bytes(), &[0, 1, 2, 3]);
833        Ok(())
834    }
835
836    #[test]
837    fn copy() -> Result<(), Box<dyn std::error::Error>> {
838        let bucket = crate::read_test_bucket();
839        let original = Object::create(&bucket.name, &[2, 3], "test-copy", "text/plain")?;
840        original.copy(&bucket.name, "test-copy - copy")?;
841        Ok(())
842    }
843
844    #[test]
845    fn rewrite() -> Result<(), Box<dyn std::error::Error>> {
846        let bucket = crate::read_test_bucket();
847        let obj = Object::create(&bucket.name, &[0, 1], "test-rewrite", "text/plain")?;
848        let obj = obj.rewrite(&bucket.name, "test-rewritten")?;
849        let url = obj.download_url(100)?;
850        let client = reqwest::blocking::Client::new();
851        let download = client.head(&url).send()?;
852        assert_eq!(download.status().as_u16(), 200);
853        Ok(())
854    }
855
856    #[test]
857    fn test_url_encoding() -> Result<(), Box<dyn std::error::Error>> {
858        let bucket = crate::read_test_bucket();
859        let complicated_names = [
860            "asdf",
861            "asdf+1",
862            "asdf&&+1?=3,,-_()*&^%$#@!`~{}[]\\|:;\"'<>,.?/äöüëß",
863            "https://www.google.com",
864            "परिक्षण फाईल",
865            "测试很重要",
866        ];
867        for name in &complicated_names {
868            let _obj = Object::create(&bucket.name, &[0, 1], name, "text/plain")?;
869            let obj = Object::read(&bucket.name, &name).unwrap();
870            let url = obj.download_url(100)?;
871            let client = reqwest::blocking::Client::new();
872            let download = client.head(&url).send()?;
873            assert_eq!(download.status().as_u16(), 200);
874        }
875        Ok(())
876    }
877}