use crate::error::{Error, GoogleResponse};
pub use crate::resources::bucket::Owner;
use crate::resources::object_access_control::ObjectAccessControl;
use crate::resources::common::ListResponse;
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Object {
pub kind: String,
pub id: String,
pub self_link: String,
pub name: String,
pub bucket: String,
#[serde(deserialize_with = "crate::from_str")]
pub generation: i64,
#[serde(deserialize_with = "crate::from_str")]
pub metageneration: i64,
pub content_type: Option<String>,
pub time_created: chrono::DateTime<chrono::Utc>,
pub updated: chrono::DateTime<chrono::Utc>,
pub time_deleted: Option<chrono::DateTime<chrono::Utc>>,
pub temporary_hold: Option<bool>,
pub event_based_hold: Option<bool>,
pub retention_expiration_time: Option<chrono::DateTime<chrono::Utc>>,
pub storage_class: String,
pub time_storage_class_updated: chrono::DateTime<chrono::Utc>,
#[serde(deserialize_with = "crate::from_str")]
pub size: u64,
pub md5_hash: Option<String>,
pub media_link: String,
pub content_encoding: Option<String>,
pub content_disposition: Option<String>,
pub content_language: Option<String>,
pub cache_control: Option<String>,
pub metadata: Option<std::collections::HashMap<String, String>>,
pub acl: Option<Vec<ObjectAccessControl>>,
pub owner: Option<Owner>,
pub crc32c: String,
#[serde(default, deserialize_with = "crate::from_str_opt")]
pub component_count: Option<i32>,
pub etag: String,
pub customer_encryption: Option<CustomerEncrypton>,
pub kms_key_name: Option<String>,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CustomerEncrypton {
pub encryption_algorithm: String,
pub key_sha256: String,
}
#[derive(Debug, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ComposeRequest {
pub kind: String,
pub source_objects: Vec<SourceObject>,
pub destination: Option<Object>,
}
#[derive(Debug, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SourceObject {
pub name: String,
pub generation: Option<i64>,
pub object_preconditions: Option<ObjectPrecondition>,
}
#[derive(Debug, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ObjectPrecondition {
pub if_generation_match: i64,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct ObjectList {
kind: String,
items: Vec<Object>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RewriteResponse {
kind: String,
total_bytes_rewritten: String,
object_size: String,
done: bool,
resource: Object,
}
impl Object {
pub fn create(
bucket: &str,
file: &[u8],
filename: &str,
mime_type: &str,
) -> Result<Self, Error> {
use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
const BASE_URL: &str = "https://www.googleapis.com/upload/storage/v1/b";
let client = reqwest::blocking::Client::new();
let url = &format!("{}/{}/o?uploadType=media&name={}", BASE_URL, bucket, filename);
let mut headers = crate::get_headers()?;
headers.insert(CONTENT_TYPE, mime_type.to_string().parse()?);
headers.insert(CONTENT_LENGTH, file.len().to_string().parse()?);
let response = client
.post(url)
.headers(headers)
.body(file.to_owned())
.send()?;
if response.status() == 200 {
Ok(serde_json::from_str(&response.text()?)?)
} else {
Err(Error::new(&response.text()?))
}
}
pub fn create_streamed<R: std::io::Read + Send + 'static>(
bucket: &str,
file: R,
length: u64,
filename: &str,
mime_type: &str,
) -> Result<Self, Error> {
use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
const BASE_URL: &str = "https://www.googleapis.com/upload/storage/v1/b";
let client = reqwest::blocking::Client::new();
let url = &format!(
"{}/{}/o?uploadType=media&name={}",
BASE_URL, bucket, filename
);
let mut headers = crate::get_headers()?;
headers.insert(CONTENT_TYPE, mime_type.to_string().parse()?);
headers.insert(CONTENT_LENGTH, length.to_string().parse()?);
let body = reqwest::blocking::Body::sized(file, length);
let response = client
.post(url)
.headers(headers)
.body(body)
.send()?;
if response.status() == 200 {
Ok(serde_json::from_str(&response.text()?)?)
} else {
Err(Error::new(&response.text()?))
}
}
pub fn list(bucket: &str) -> Result<Vec<Self>, Error> {
let url = format!("{}/b/{}/o", crate::BASE_URL, bucket);
let client = reqwest::blocking::Client::new();
let result: GoogleResponse<ListResponse<Self>> = client
.get(&url)
.headers(crate::get_headers()?)
.send()?
.json()?;
Ok(result?.items)
}
pub fn read(bucket: &str, file_name: &str) -> Result<Self, Error> {
let url = format!("{}/b/{}/o/{}", crate::BASE_URL, bucket, file_name);
let client = reqwest::blocking::Client::new();
let result: GoogleResponse<Self> = client
.get(&url)
.headers(crate::get_headers()?)
.send()?
.json()?;
Ok(result?)
}
pub fn update(&self) -> Result<Self, Error> {
let url = format!("{}/b/{}/o/{}", crate::BASE_URL, self.bucket, self.name);
let client = reqwest::blocking::Client::new();
let result: GoogleResponse<Self> = client
.put(&url)
.headers(crate::get_headers()?)
.json(&self)
.send()?
.json()?;
Ok(result?)
}
pub fn delete(self) -> Result<(), Error> {
let url = format!("{}/b/{}/o/{}", crate::BASE_URL, self.bucket, self.name);
let client = reqwest::blocking::Client::new();
let response = client.delete(&url).headers(crate::get_headers()?).send()?;
if response.status().is_success() {
Ok(())
} else {
Err(Error::Google(response.json()?))
}
}
pub fn compose(
bucket: &str,
req: &ComposeRequest,
destination_object: &str,
) -> Result<Self, Error> {
let url = format!(
"{}/b/{}/o/{}/compose",
crate::BASE_URL,
bucket,
destination_object
);
let client = reqwest::blocking::Client::new();
let result: GoogleResponse<Self> = client
.post(&url)
.headers(crate::get_headers()?)
.json(req)
.send()?
.json()?;
Ok(result?)
}
pub fn copy(&self, destination_bucket: &str, path: &str) -> Result<Self, Error> {
use reqwest::header::CONTENT_LENGTH;
let url = format!(
"{base}/b/{sBucket}/o/{sObject}/copyTo/b/{dBucket}/o/{dObject}",
base = crate::BASE_URL,
sBucket = self.bucket,
sObject = self.name,
dBucket = destination_bucket,
dObject = path
);
let client = reqwest::blocking::Client::new();
let mut headers = crate::get_headers()?;
headers.insert(CONTENT_LENGTH, "0".parse()?);
let response: GoogleResponse<Self> = client.post(&url).headers(headers).send()?.json()?;
Ok(response?)
}
pub fn rewrite(&self, destination_bucket: &str, path: &str) -> Result<Self, Error> {
use reqwest::header::CONTENT_LENGTH;
let url = format!(
"{base}/b/{sBucket}/o/{sObject}/rewriteTo/b/{dBucket}/o/{dObject}",
base = crate::BASE_URL,
sBucket = self.bucket,
sObject = self.name,
dBucket = destination_bucket,
dObject = path,
);
let client = reqwest::blocking::Client::new();
let mut headers = crate::get_headers()?;
headers.insert(CONTENT_LENGTH, "0".parse()?);
let response: GoogleResponse<RewriteResponse> =
client.post(&url).headers(headers).send()?.json()?;
Ok(response?.resource)
}
pub fn download_url(&self, duration: u32) -> Result<String, Error> {
self.sign(&self.name, duration, "GET")
}
#[inline(always)]
fn sign(&self, file_path: &str, duration: u32, http_verb: &str) -> Result<String, Error> {
use openssl::sha;
let issue_date = chrono::Utc::now();
let file_path = self.path_to_resource(file_path);
let query_string = Self::get_canonical_query_string(&issue_date, duration);
let canonical_request = self.get_canonical_request(&file_path, &query_string, http_verb);
let hash = sha::sha256(canonical_request.as_bytes());
let hex_hash = hex::encode(hash);
let string_to_sign = format!(
"{signing_algorithm}\n\
{current_datetime}\n\
{credential_scope}\n\
{hashed_canonical_request}",
signing_algorithm = "GOOG4-RSA-SHA256",
current_datetime = issue_date.format("%Y%m%dT%H%M%SZ"),
credential_scope = Self::get_credential_scope(&issue_date),
hashed_canonical_request = hex_hash,
);
let buffer = Self::sign_str(&string_to_sign);
let signature = hex::encode(&buffer?);
Ok(format!(
"https://storage.googleapis.com{path_to_resource}?\
{query_string}&\
X-Goog-Signature={request_signature}",
path_to_resource = file_path,
query_string = query_string,
request_signature = signature,
))
}
#[inline(always)]
fn get_canonical_request(&self, path: &str, query_string: &str, http_verb: &str) -> String {
format!(
"{http_verb}\n\
{path_to_resource}\n\
{canonical_query_string}\n\
{canonical_headers}\n\
\n\
{signed_headers}\n\
{payload}",
http_verb = http_verb,
path_to_resource = path,
canonical_query_string = query_string,
canonical_headers = "host:storage.googleapis.com",
signed_headers = "host",
payload = "UNSIGNED-PAYLOAD",
)
}
#[inline(always)]
fn get_canonical_query_string(date: &chrono::DateTime<chrono::Utc>, exp: u32) -> String {
let credential = format!(
"{authorizer}/{scope}",
authorizer = crate::SERVICE_ACCOUNT.client_email,
scope = Self::get_credential_scope(date),
);
format!(
"X-Goog-Algorithm={algo}&\
X-Goog-Credential={cred}&\
X-Goog-Date={date}&\
X-Goog-Expires={exp}&\
X-Goog-SignedHeaders={signed}",
algo = "GOOG4-RSA-SHA256",
cred = dbg!(percent_encode(&credential).replace("@", "%40")),
date = date.format("%Y%m%dT%H%M%SZ"),
exp = exp,
signed = "host",
)
}
#[inline(always)]
fn path_to_resource(&self, path: &str) -> String {
format!(
"/{bucket}/{file_path}",
bucket = self.bucket,
file_path = dbg!(percent_encode_noslash(path)),
)
}
#[inline(always)]
fn get_credential_scope(date: &chrono::DateTime<chrono::Utc>) -> String {
format!("{}/henk/storage/goog4_request", date.format("%Y%m%d"))
}
#[inline(always)]
fn sign_str(message: &str) -> Result<Vec<u8>, Error> {
use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer};
let key = PKey::private_key_from_pem(crate::SERVICE_ACCOUNT.private_key.as_bytes())?;
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
signer.update(message.as_bytes())?;
Ok(signer.sign_to_vec()?)
}
}
fn percent_encode_noslash(input: &str) -> String {
percent_encode(input).replace("%2F", "/")
}
fn percent_encode(input: &str) -> String {
use url::percent_encoding::{utf8_percent_encode, PATH_SEGMENT_ENCODE_SET};
utf8_percent_encode(input, PATH_SEGMENT_ENCODE_SET)
.to_string()
.replace("&", "%26")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::read_test_bucket();
Object::create(&bucket.name, &[0, 1], "test-create", "text/plain")?;
Ok(())
}
#[test]
fn create_streamed() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::read_test_bucket();
let cursor = std::io::Cursor::new([0, 1]);
Object::create_streamed(&bucket.name, cursor, 2, "test-create-streamed", "text/plain")?;
Ok(())
}
#[test]
fn list() -> Result<(), Box<dyn std::error::Error>> {
let test_bucket = crate::read_test_bucket();
Object::list(&test_bucket.name)?;
Ok(())
}
#[test]
fn read() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::read_test_bucket();
Object::create(&bucket.name, &[0, 1], "test-read", "text/plain")?;
Object::read(&bucket.name, "test-read")?;
Ok(())
}
#[test]
fn update() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::read_test_bucket();
let mut obj = Object::create(&bucket.name, &[0, 1], "test-update", "text/plain")?;
obj.content_type = Some("application/xml".to_string());
obj.update()?;
Ok(())
}
#[test]
fn delete() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::read_test_bucket();
let obj = Object::create(&bucket.name, &[0, 1], "test-delete", "text/plain")?;
obj.delete()?;
Ok(())
}
#[test]
fn compose() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::read_test_bucket();
let obj1 = Object::create(&bucket.name, &[0, 1], "test-compose-1", "text/plain")?;
let obj2 = Object::create(&bucket.name, &[2, 3], "test-compose-2", "text/plain")?;
let compose_request = ComposeRequest {
kind: "storage#composeRequest".to_string(),
source_objects: vec![
SourceObject {
name: obj1.name.clone(),
generation: None,
object_preconditions: None,
},
SourceObject {
name: obj2.name.clone(),
generation: None,
object_preconditions: None,
},
],
destination: None,
};
let obj3 = Object::compose(&bucket.name, &compose_request, "test-concatted-file")?;
let url = obj3.download_url(100)?;
let content = reqwest::blocking::get(&url)?.text()?;
assert_eq!(content.as_bytes(), &[0, 1, 2, 3]);
Ok(())
}
#[test]
fn copy() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::read_test_bucket();
let original = Object::create(&bucket.name, &[2, 3], "test-copy", "text/plain")?;
original.copy(&bucket.name, "test-copy - copy")?;
Ok(())
}
#[test]
fn rewrite() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::read_test_bucket();
let obj = Object::create(&bucket.name, &[0, 1], "test-rewrite", "text/plain")?;
let obj = obj.rewrite(&bucket.name, "test-rewritten")?;
let url = dbg!(obj.download_url(100)?);
let client = reqwest::blocking::Client::new();
let download = client.head(&url).send()?;
assert_eq!(download.status().as_u16(), 200);
Ok(())
}
#[test]
fn test_url_encoding() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::read_test_bucket();
let complicated_names = [
"asdf",
"asdf+1",
"asdf&&+1?=3,,-_()*&^%$#@!`~{}[]\\|:;\"'<>,.?/äöüëß",
"https://www.google.com",
"परिक्षण फाईल",
"測試文件",
];
for name in &complicated_names {
let obj = Object::create(&bucket.name, &[0, 1], name, "text/plain")?;
let url = obj.download_url(100)?;
let client = reqwest::blocking::Client::new();
let download = client.head(&url).send()?;
assert_eq!(download.status().as_u16(), 200);
}
Ok(())
}
}