use crate::error::{Error, GoogleResponse};
use crate::resources::bucket_access_control::{BucketAccessControl, NewBucketAccessControl};
pub use crate::resources::common::Entity;
use crate::resources::common::ListResponse;
use crate::resources::default_object_access_control::{
DefaultObjectAccessControl, NewDefaultObjectAccessControl,
};
pub use crate::resources::location::*;
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bucket {
pub kind: String,
pub id: String, pub self_link: String,
#[serde(deserialize_with = "crate::from_str")]
pub project_number: u64,
pub name: String,
pub time_created: chrono::DateTime<chrono::Utc>,
pub updated: chrono::DateTime<chrono::Utc>,
pub default_event_based_hold: Option<bool>,
pub retention_policy: Option<RetentionPolicy>,
#[serde(deserialize_with = "crate::from_str")]
pub metageneration: i64,
pub acl: Option<Vec<BucketAccessControl>>,
pub default_object_acl: Option<Vec<DefaultObjectAccessControl>>,
pub iam_configuration: IamConfiguration,
pub encryption: Option<Encryption>,
pub owner: Option<Owner>,
pub location: Location,
pub location_type: String,
pub website: Option<Website>,
pub logging: Option<Logging>,
pub versioning: Option<Versioning>,
pub cors: Option<Vec<Cors>>,
pub lifecycle: Option<Lifecycle>,
pub labels: Option<std::collections::HashMap<String, String>>,
pub storage_class: StorageClass,
pub billing: Option<Billing>,
pub etag: String,
}
#[derive(Debug, PartialEq, Default, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NewBucket {
pub name: String,
pub default_event_based_hold: Option<bool>,
pub acl: Option<Vec<NewBucketAccessControl>>,
pub default_object_acl: Option<Vec<NewDefaultObjectAccessControl>>,
pub iam_configuration: Option<IamConfiguration>,
pub encryption: Option<Encryption>,
pub location: Location,
pub website: Option<Website>,
pub logging: Option<Logging>,
pub versioning: Option<Versioning>,
pub cors: Option<Vec<Cors>>,
pub lifecycle: Option<Lifecycle>,
pub labels: Option<std::collections::HashMap<String, String>>,
pub storage_class: Option<StorageClass>,
pub billing: Option<Billing>,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetentionPolicy {
#[serde(deserialize_with = "crate::from_str")]
pub retention_period: u64,
pub effective_time: chrono::DateTime<chrono::Utc>,
pub is_locked: Option<bool>,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IamConfiguration {
pub uniform_bucket_level_access: UniformBucketLevelAccess,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UniformBucketLevelAccess {
pub enabled: bool,
pub locked_time: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Encryption {
pub default_kms_key_name: String,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Owner {
pub entity: Entity,
pub entity_id: Option<String>,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Website {
pub main_page_suffix: String,
pub not_found_page: String,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Logging {
pub log_bucket: String,
pub log_object_prefix: String,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Versioning {
pub enabled: bool,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Cors {
pub origin: Vec<String>,
pub method: Vec<String>,
pub response_header: Vec<String>,
#[serde(deserialize_with = "crate::from_str")]
pub max_age_seconds: i32,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Lifecycle {
pub rule: Vec<Rule>,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Rule {
pub action: Action,
pub condition: Condition,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Action {
pub r#type: ActionType,
pub storage_class: Option<StorageClass>,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ActionType {
Delete,
SetStorageClass,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Condition {
#[serde(deserialize_with = "crate::from_str")]
pub age: i32,
pub created_before: chrono::DateTime<chrono::Utc>,
pub is_live: bool,
pub matches_storage_class: Vec<String>,
#[serde(deserialize_with = "crate::from_str")]
pub num_newer_versions: i32,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Billing {
pub requester_pays: bool,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum StorageClass {
Standard,
Nearline,
Coldline,
MultiRegional,
Regional,
DurableReducedAvailability,
}
#[derive(Debug, PartialEq, Default, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IamPolicy {
pub version: i32,
pub kind: Option<String>,
pub resource_id: Option<String>,
pub bindings: Vec<Binding>,
pub etag: String,
}
#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Binding {
pub role: IamRole,
pub members: Vec<String>,
pub condition: Option<IamCondition>,
}
#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IamCondition {
pub title: String,
pub description: Option<String>,
pub expression: String,
}
#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum IamRole {
Standard(StandardIamRole),
Primitive(PrimitiveIamRole),
Legacy(LegacyIamRole),
}
#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum StandardIamRole {
#[serde(rename = "roles/storage.objectCreator")]
ObjectCreator,
#[serde(rename = "roles/storage.objectViewer")]
ObjectViewer,
#[serde(rename = "roles/storage.objectAdmin")]
ObjectAdmin,
#[serde(rename = "roles/storage.hmacKeyAdmin")]
HmacKeyAdmin,
#[serde(rename = "roles/storage.admin")]
Admin,
}
#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum PrimitiveIamRole {
#[serde(rename = "role/viewer")]
Viewer,
#[serde(rename = "role/editor")]
Editor,
#[serde(rename = "role/owner")]
Owner,
}
#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum LegacyIamRole {
#[serde(rename = "roles/storage.legacyObjectReader")]
LegacyObjectReader,
#[serde(rename = "roles/storage.legacyObjectOwner")]
LegacyObjectOwner,
#[serde(rename = "roles/storage.legacyBucketReader")]
LegacyBucketReader,
#[serde(rename = "roles/storage.legacyBucketWriter")]
LegacyBucketWriter,
#[serde(rename = "roles/storage.legacyBucketOwner")]
LegacyBucketOwner,
}
#[derive(Debug, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TestIamPermission {
kind: String,
permissions: Vec<String>,
}
impl Bucket {
pub fn create(new_bucket: &NewBucket) -> Result<Self, Error> {
let url = format!("{}/b/", crate::BASE_URL);
let project = crate::SERVICE_ACCOUNT.project_id.clone();
let query = [("project", project)];
let client = reqwest::blocking::Client::new();
let result: GoogleResponse<Self> = client
.post(&url)
.headers(crate::get_headers()?)
.query(&query)
.json(new_bucket)
.send()?
.json()?;
Ok(result?)
}
pub fn list() -> Result<Vec<Self>, Error> {
let url = format!("{}/b/", crate::BASE_URL);
let project = crate::SERVICE_ACCOUNT.project_id.clone();
let query = [("project", project)];
let client = reqwest::blocking::Client::new();
let result: GoogleResponse<ListResponse<Self>> = client
.get(&url)
.headers(crate::get_headers()?)
.query(&query)
.send()?
.json()?;
Ok(result?.items)
}
pub fn read(name: &str) -> Result<Self, Error> {
let url = format!("{}/b/{}", crate::BASE_URL, 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/{}", crate::BASE_URL, 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/{}", crate::BASE_URL, 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 get_iam_policy(&self) -> Result<IamPolicy, Error> {
let url = format!("{}/b/{}/iam", crate::BASE_URL, self.name);
let client = reqwest::blocking::Client::new();
let response: GoogleResponse<IamPolicy> = client
.get(&url)
.headers(crate::get_headers()?)
.send()?
.json()?;
Ok(response?)
}
pub fn set_iam_policy(&self, iam: &IamPolicy) -> Result<IamPolicy, Error> {
let url = format!("{}/b/{}/iam", crate::BASE_URL, self.name);
let client = reqwest::blocking::Client::new();
let response: GoogleResponse<IamPolicy> = client
.put(&url)
.headers(crate::get_headers()?)
.json(iam)
.send()?
.json()?;
Ok(response?)
}
pub fn test_iam_permission(&self, permission: &str) -> Result<TestIamPermission, Error> {
if permission == "storage.buckets.list" || permission == "storage.buckets.create" {
return Err(Error::new(
"tested permission must not be `storage.buckets.list` or `storage.buckets.create`",
));
}
let url = format!("{}/b/{}/iam/testPermissions", crate::BASE_URL, self.name);
let client = reqwest::blocking::Client::new();
let response: GoogleResponse<TestIamPermission> = client
.get(&url)
.headers(crate::get_headers()?)
.query(&[("permissions", permission)])
.send()?
.json()?;
Ok(response?)
}
fn _lock_retention_policy() {
todo!()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resources::common::Role;
#[test]
fn create() -> Result<(), Box<dyn std::error::Error>> {
dotenv::dotenv().ok();
let base_name = std::env::var("TEST_BUCKET")?;
let new_bucket = NewBucket {
name: format!("{}-test-create", base_name),
default_event_based_hold: Some(true),
acl: Some(vec![NewBucketAccessControl {
entity: Entity::AllUsers,
role: Role::Reader,
}]),
default_object_acl: Some(vec![NewDefaultObjectAccessControl {
entity: Entity::AllUsers,
role: Role::Reader,
}]),
iam_configuration: Some(IamConfiguration {
uniform_bucket_level_access: UniformBucketLevelAccess {
enabled: false,
locked_time: None,
},
}),
..Default::default()
};
let bucket = Bucket::create(&new_bucket)?;
bucket.delete()?;
Ok(())
}
#[test]
fn list() -> Result<(), Box<dyn std::error::Error>> {
Bucket::list()?;
Ok(())
}
#[test]
fn read() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::create_test_bucket("test-read");
let also_bucket = Bucket::read(&bucket.name)?;
assert_eq!(bucket, also_bucket);
bucket.delete()?;
assert!(also_bucket.delete().is_err());
Ok(())
}
#[test]
fn update() -> Result<(), Box<dyn std::error::Error>> {
let mut bucket = crate::create_test_bucket("test-update");
bucket.retention_policy = Some(RetentionPolicy {
retention_period: 50,
effective_time: chrono::Utc::now() + chrono::Duration::seconds(50),
is_locked: Some(false),
});
bucket.update()?;
let updated = Bucket::read(&bucket.name)?;
assert_eq!(updated.retention_policy.unwrap().retention_period, 50);
bucket.delete()?;
Ok(())
}
#[test]
fn delete() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::create_test_bucket("test-delete");
bucket.delete()?;
Ok(())
}
#[test]
fn get_iam_policy() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::create_test_bucket("test-get-iam-policy");
bucket.get_iam_policy()?;
bucket.delete()?;
Ok(())
}
#[test]
fn set_iam_policy() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::create_test_bucket("test-set-iam-policy");
let iam_policy = IamPolicy {
bindings: vec![Binding {
role: IamRole::Standard(StandardIamRole::ObjectViewer),
members: vec!["allUsers".to_string()],
condition: None,
}],
..Default::default()
};
bucket.set_iam_policy(&iam_policy)?;
assert_eq!(
bucket.get_iam_policy()?.bindings,
iam_policy.bindings
);
bucket.delete()?;
Ok(())
}
#[test]
fn test_iam_permission() -> Result<(), Box<dyn std::error::Error>> {
let bucket = crate::create_test_bucket("test-test-ia-permission");
bucket.test_iam_permission("storage.buckets.get")?;
bucket.delete()?;
Ok(())
}
}