Fixed xml parsing of bucket_lifecycle, added support for with_updated_at (#153)

This commit is contained in:
Henk-Jan Lebbink 2025-05-15 19:14:40 +02:00 committed by GitHub
parent 9495c5dcce
commit f6ca7dba03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 622 additions and 407 deletions

View File

@ -15,11 +15,11 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use minio::s3::builders::PostPolicy; use minio::s3::builders::PostPolicy;
use minio::s3::lifecycle_config::{LifecycleConfig, LifecycleRule};
use minio::s3::types::{ use minio::s3::types::{
AndOperator, CsvInputSerialization, CsvOutputSerialization, Destination, FileHeaderInfo, AndOperator, CsvInputSerialization, CsvOutputSerialization, Destination, FileHeaderInfo,
Filter, LifecycleConfig, LifecycleRule, NotificationConfig, ObjectLockConfig, PrefixFilterRule, Filter, NotificationConfig, ObjectLockConfig, PrefixFilterRule, QueueConfig, QuoteFields,
QueueConfig, QuoteFields, ReplicationConfig, ReplicationRule, RetentionMode, SelectRequest, ReplicationConfig, ReplicationRule, RetentionMode, SelectRequest, SuffixFilterRule,
SuffixFilterRule,
}; };
use minio::s3::utils::utc_now; use minio::s3::utils::utc_now;
use std::collections::HashMap; use std::collections::HashMap;
@ -27,29 +27,19 @@ use std::collections::HashMap;
pub fn create_bucket_lifecycle_config_examples() -> LifecycleConfig { pub fn create_bucket_lifecycle_config_examples() -> LifecycleConfig {
LifecycleConfig { LifecycleConfig {
rules: vec![LifecycleRule { rules: vec![LifecycleRule {
abort_incomplete_multipart_upload_days_after_initiation: None,
expiration_date: None,
expiration_days: Some(365),
expiration_expired_object_delete_marker: None,
filter: Filter {
and_operator: None,
prefix: Some(String::from("logs/")),
tag: None,
},
id: String::from("rule1"), id: String::from("rule1"),
noncurrent_version_expiration_noncurrent_days: None, expiration_days: Some(365),
noncurrent_version_transition_noncurrent_days: None, filter: Filter {
noncurrent_version_transition_storage_class: None, prefix: Some(String::from("logs/")),
..Default::default()
},
status: true, status: true,
transition_date: None, ..Default::default()
transition_days: None,
transition_storage_class: None,
}], }],
} }
} }
pub fn create_bucket_notification_config_example() -> NotificationConfig { pub fn create_bucket_notification_config_example() -> NotificationConfig {
NotificationConfig { NotificationConfig {
cloud_func_config_list: None,
queue_config_list: Some(vec![QueueConfig { queue_config_list: Some(vec![QueueConfig {
events: vec![ events: vec![
String::from("s3:ObjectCreated:Put"), String::from("s3:ObjectCreated:Put"),
@ -64,7 +54,7 @@ pub fn create_bucket_notification_config_example() -> NotificationConfig {
}), }),
queue: String::from("arn:minio:sqs::miniojavatest:webhook"), queue: String::from("arn:minio:sqs::miniojavatest:webhook"),
}]), }]),
topic_config_list: None, ..Default::default()
} }
} }
pub fn create_bucket_policy_config_example(bucket_name: &str) -> String { pub fn create_bucket_policy_config_example(bucket_name: &str) -> String {
@ -148,31 +138,22 @@ pub fn create_bucket_replication_config_example(dst_bucket: &str) -> Replication
ReplicationConfig { ReplicationConfig {
role: Some("example1".to_string()), role: Some("example1".to_string()),
rules: vec![ReplicationRule { rules: vec![ReplicationRule {
id: Some(String::from("rule1")),
destination: Destination { destination: Destination {
bucket_arn: String::from(&format!("arn:aws:s3:::{}", dst_bucket)), bucket_arn: String::from(&format!("arn:aws:s3:::{}", dst_bucket)),
access_control_translation: None, ..Default::default()
account: None,
encryption_config: None,
metrics: None,
replication_time: None,
storage_class: None,
}, },
delete_marker_replication_status: None,
existing_object_replication_status: None,
filter: Some(Filter { filter: Some(Filter {
and_operator: Some(AndOperator { and_operator: Some(AndOperator {
prefix: Some(String::from("TaxDocs")), prefix: Some(String::from("TaxDocs")),
tags: Some(tags), tags: Some(tags),
}), }),
prefix: None, ..Default::default()
tag: None,
}), }),
id: Some(String::from("rule1")),
prefix: None,
priority: Some(1), priority: Some(1),
source_selection_criteria: None,
delete_replication_status: Some(false), delete_replication_status: Some(false),
status: true, status: true,
..Default::default()
}], }],
} }
} }

View File

@ -17,10 +17,11 @@ mod common;
use crate::common::{create_bucket_if_not_exists, create_client_on_play}; use crate::common::{create_bucket_if_not_exists, create_client_on_play};
use minio::s3::Client; use minio::s3::Client;
use minio::s3::lifecycle_config::{LifecycleConfig, LifecycleRule};
use minio::s3::response::{ use minio::s3::response::{
DeleteBucketLifecycleResponse, GetBucketLifecycleResponse, PutBucketLifecycleResponse, DeleteBucketLifecycleResponse, GetBucketLifecycleResponse, PutBucketLifecycleResponse,
}; };
use minio::s3::types::{Filter, LifecycleConfig, LifecycleRule, S3Api}; use minio::s3::types::{Filter, S3Api};
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@ -38,23 +39,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
} }
let rules: Vec<LifecycleRule> = vec![LifecycleRule { let rules: Vec<LifecycleRule> = vec![LifecycleRule {
abort_incomplete_multipart_upload_days_after_initiation: None,
expiration_date: None,
expiration_days: Some(365),
expiration_expired_object_delete_marker: None,
filter: Filter {
and_operator: None,
prefix: Some(String::from("logs/")),
tag: None,
},
id: String::from("rule1"), id: String::from("rule1"),
noncurrent_version_expiration_noncurrent_days: None, expiration_days: Some(365),
noncurrent_version_transition_noncurrent_days: None, filter: Filter {
noncurrent_version_transition_storage_class: None, prefix: Some(String::from("logs/")),
..Default::default()
},
status: true, status: true,
transition_date: None, ..Default::default()
transition_days: None,
transition_storage_class: None,
}]; }];
let resp: PutBucketLifecycleResponse = client let resp: PutBucketLifecycleResponse = client

View File

@ -13,8 +13,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use crate::s3::builders::BucketCommon; use crate::s3::Client;
use crate::s3::error::Error; use crate::s3::error::Error;
use crate::s3::multimap::{Multimap, MultimapExt};
use crate::s3::response::GetBucketLifecycleResponse; use crate::s3::response::GetBucketLifecycleResponse;
use crate::s3::types::{S3Api, S3Request, ToS3Request}; use crate::s3::types::{S3Api, S3Request, ToS3Request};
use crate::s3::utils::{check_bucket_name, insert}; use crate::s3::utils::{check_bucket_name, insert};
@ -23,10 +24,42 @@ use http::Method;
/// Argument builder for the [`GetBucketLifecycle`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLifecycle.html) S3 API operation. /// Argument builder for the [`GetBucketLifecycle`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLifecycle.html) S3 API operation.
/// ///
/// This struct constructs the parameters required for the [`Client::get_bucket_lifecycle`](crate::s3::client::Client::get_bucket_lifecycle) method. /// This struct constructs the parameters required for the [`Client::get_bucket_lifecycle`](crate::s3::client::Client::get_bucket_lifecycle) method.
pub type GetBucketLifecycle = BucketCommon<GetBucketLifecyclePhantomData>;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct GetBucketLifecyclePhantomData; pub struct GetBucketLifecycle {
client: Client,
extra_headers: Option<Multimap>,
extra_query_params: Option<Multimap>,
region: Option<String>,
bucket: String,
with_updated_at: bool,
}
impl GetBucketLifecycle {
pub fn new(client: Client, bucket: String) -> Self {
Self {
client,
bucket,
..Default::default()
}
}
pub fn extra_headers(mut self, extra_headers: Option<Multimap>) -> Self {
self.extra_headers = extra_headers;
self
}
pub fn extra_query_params(mut self, extra_query_params: Option<Multimap>) -> Self {
self.extra_query_params = extra_query_params;
self
}
pub fn with_updated_at(mut self, with_updated_at: bool) -> Self {
self.with_updated_at = with_updated_at;
self
}
}
impl S3Api for GetBucketLifecycle { impl S3Api for GetBucketLifecycle {
type S3Response = GetBucketLifecycleResponse; type S3Response = GetBucketLifecycleResponse;
@ -36,10 +69,15 @@ impl ToS3Request for GetBucketLifecycle {
fn to_s3request(self) -> Result<S3Request, Error> { fn to_s3request(self) -> Result<S3Request, Error> {
check_bucket_name(&self.bucket, true)?; check_bucket_name(&self.bucket, true)?;
let mut query_params: Multimap = insert(self.extra_query_params, "lifecycle");
if self.with_updated_at {
query_params.add("withUpdatedAt", "true");
}
Ok(S3Request::new(self.client, Method::GET) Ok(S3Request::new(self.client, Method::GET)
.region(self.region) .region(self.region)
.bucket(Some(self.bucket)) .bucket(Some(self.bucket))
.query_params(insert(self.extra_query_params, "lifecycle")) .query_params(query_params)
.headers(self.extra_headers.unwrap_or_default())) .headers(self.extra_headers.unwrap_or_default()))
} }
} }

View File

@ -15,15 +15,18 @@
use crate::s3::Client; use crate::s3::Client;
use crate::s3::error::Error; use crate::s3::error::Error;
use crate::s3::lifecycle_config::LifecycleConfig;
use crate::s3::multimap::{Multimap, MultimapExt}; use crate::s3::multimap::{Multimap, MultimapExt};
use crate::s3::response::PutBucketLifecycleResponse; use crate::s3::response::PutBucketLifecycleResponse;
use crate::s3::segmented_bytes::SegmentedBytes; use crate::s3::segmented_bytes::SegmentedBytes;
use crate::s3::types::{LifecycleConfig, S3Api, S3Request, ToS3Request}; use crate::s3::types::{S3Api, S3Request, ToS3Request};
use crate::s3::utils::{check_bucket_name, insert, md5sum_hash}; use crate::s3::utils::{check_bucket_name, insert, md5sum_hash};
use bytes::Bytes; use bytes::Bytes;
use http::Method; use http::Method;
/// Argument builder for [put_bucket_lifecycle()](crate::s3::client::Client::put_bucket_lifecycle) API /// Argument builder for the [`PutBucketLifecycle`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycle.html) S3 API operation.
///
/// This struct constructs the parameters required for the [`Client::put_bucket_lifecycle`](crate::s3::client::Client::put_bucket_lifecycle) method.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct PutBucketLifecycle { pub struct PutBucketLifecycle {
client: Client, client: Client,

View File

@ -31,26 +31,19 @@ impl Client {
/// use minio::s3::Client; /// use minio::s3::Client;
/// use minio::s3::builders::VersioningStatus; /// use minio::s3::builders::VersioningStatus;
/// use minio::s3::response::PutBucketLifecycleResponse; /// use minio::s3::response::PutBucketLifecycleResponse;
/// use minio::s3::types::{Filter, LifecycleConfig, LifecycleRule, S3Api}; /// use minio::s3::types::{Filter, S3Api};
/// use minio::s3::lifecycle_config::{LifecycleRule, LifecycleConfig};
/// ///
/// #[tokio::main] /// #[tokio::main]
/// async fn main() { /// async fn main() {
/// let client: Client = Default::default(); // configure your client here /// let client: Client = Default::default(); // configure your client here
/// ///
/// let rules: Vec<LifecycleRule> = vec![LifecycleRule { /// let rules: Vec<LifecycleRule> = vec![LifecycleRule {
/// abort_incomplete_multipart_upload_days_after_initiation: None,
/// expiration_date: None,
/// expiration_days: Some(365),
/// expiration_expired_object_delete_marker: None,
/// filter: Filter {and_operator: None, prefix: Some(String::from("logs/")), tag: None},
/// id: String::from("rule1"), /// id: String::from("rule1"),
/// noncurrent_version_expiration_noncurrent_days: None, /// filter: Filter {and_operator: None, prefix: Some(String::from("logs/")), tag: None},
/// noncurrent_version_transition_noncurrent_days: None, /// expiration_days: Some(365),
/// noncurrent_version_transition_storage_class: None,
/// status: true, /// status: true,
/// transition_date: None, /// ..Default::default()
/// transition_days: None,
/// transition_storage_class: None,
/// }]; /// }];
/// ///
/// let resp: PutBucketLifecycleResponse = client /// let resp: PutBucketLifecycleResponse = client

View File

@ -37,6 +37,7 @@ pub enum ErrorCode {
ServerSideEncryptionConfigurationNotFoundError, ServerSideEncryptionConfigurationNotFoundError,
NoSuchTagSet, NoSuchTagSet,
NoSuchObjectLockConfiguration, NoSuchObjectLockConfiguration,
NoSuchLifecycleConfiguration,
NoSuchKey, NoSuchKey,
ResourceNotFound, ResourceNotFound,
MethodNotAllowed, MethodNotAllowed,
@ -66,6 +67,7 @@ impl ErrorCode {
} }
"nosuchtagset" => ErrorCode::NoSuchTagSet, "nosuchtagset" => ErrorCode::NoSuchTagSet,
"nosuchobjectlockconfiguration" => ErrorCode::NoSuchObjectLockConfiguration, "nosuchobjectlockconfiguration" => ErrorCode::NoSuchObjectLockConfiguration,
"nosuchlifecycleconfiguration" => ErrorCode::NoSuchLifecycleConfiguration,
"nosuchkey" => ErrorCode::NoSuchKey, "nosuchkey" => ErrorCode::NoSuchKey,
"resourcenotfound" => ErrorCode::ResourceNotFound, "resourcenotfound" => ErrorCode::ResourceNotFound,
"methodnotallowed" => ErrorCode::MethodNotAllowed, "methodnotallowed" => ErrorCode::MethodNotAllowed,

484
src/s3/lifecycle_config.rs Normal file
View File

@ -0,0 +1,484 @@
use crate::s3::error::Error;
use crate::s3::types::Filter;
use crate::s3::utils::to_iso8601utc;
use xmltree::Element;
#[derive(PartialEq, Clone, Debug, Default)]
/// Lifecycle configuration
pub struct LifecycleConfig {
pub rules: Vec<LifecycleRule>,
}
impl LifecycleConfig {
pub fn from_xml(root: &Element) -> Result<LifecycleConfig, Error> {
let mut config = LifecycleConfig { rules: Vec::new() };
// Process all Rule elements in the XML
for rule_elem in root.children.iter().filter_map(|c| c.as_element()) {
if rule_elem.name == "Rule" {
config.rules.push(LifecycleRule::from_xml(rule_elem)?);
}
}
Ok(config)
}
pub fn validate(&self) -> Result<(), Error> {
// Skip validation if empty
if self.rules.is_empty() {
return Ok(());
}
for rule in &self.rules {
rule.validate()?;
}
Ok(())
}
pub fn empty(&self) -> bool {
self.rules.is_empty()
}
pub fn to_xml(&self) -> String {
let mut data = String::from("<LifecycleConfiguration>");
for rule in &self.rules {
data.push_str("<Rule>");
// ID should come earlier in XML based on Go ordering
if !rule.id.is_empty() {
data.push_str("<ID>");
data.push_str(&rule.id);
data.push_str("</ID>");
}
// Status comes next
data.push_str("<Status>");
if rule.status {
data.push_str("Enabled");
} else {
data.push_str("Disabled");
}
data.push_str("</Status>");
// Filter
data.push_str(&rule.filter.to_xml());
// AbortIncompleteMultipartUpload
if let Some(days) = rule.abort_incomplete_multipart_upload_days_after_initiation {
data.push_str("<AbortIncompleteMultipartUpload><DaysAfterInitiation>");
data.push_str(&days.to_string());
data.push_str("</DaysAfterInitiation></AbortIncompleteMultipartUpload>");
}
// Expiration
let has_expiration = rule.expiration_date.is_some()
|| rule.expiration_days.is_some()
|| rule.expiration_expired_object_delete_marker.is_some()
|| rule.expiration_expired_object_all_versions.is_some();
if has_expiration {
data.push_str("<Expiration>");
if let Some(date) = rule.expiration_date {
data.push_str("<Date>");
data.push_str(&to_iso8601utc(date));
data.push_str("</Date>");
}
if let Some(days) = rule.expiration_days {
data.push_str("<Days>");
data.push_str(&days.to_string());
data.push_str("</Days>");
}
if let Some(delete_marker) = rule.expiration_expired_object_delete_marker {
if delete_marker {
data.push_str(
"<ExpiredObjectDeleteMarker>true</ExpiredObjectDeleteMarker>",
);
}
}
if let Some(delete_all) = rule.expiration_expired_object_all_versions {
if delete_all {
data.push_str("<ExpiredObjectAllVersions>true</ExpiredObjectAllVersions>");
}
}
data.push_str("</Expiration>");
}
// DelMarkerExpiration
if let Some(days) = rule.del_marker_expiration_days {
data.push_str("<DelMarkerExpiration><Days>");
data.push_str(&days.to_string());
data.push_str("</Days></DelMarkerExpiration>");
}
// AllVersionsExpiration
if let Some(days) = rule.all_versions_expiration_days {
data.push_str("<AllVersionsExpiration><Days>");
data.push_str(&days.to_string());
data.push_str("</Days>");
if let Some(delete_marker) = rule.all_versions_expiration_delete_marker {
if delete_marker {
data.push_str("<DeleteMarker>true</DeleteMarker>");
}
}
data.push_str("</AllVersionsExpiration>");
}
// NoncurrentVersionExpiration
if let Some(days) = rule.noncurrent_version_expiration_noncurrent_days {
data.push_str("<NoncurrentVersionExpiration><NoncurrentDays>");
data.push_str(&days.to_string());
data.push_str("</NoncurrentDays>");
if let Some(versions) = rule.noncurrent_version_expiration_newer_versions {
data.push_str("<NewerNoncurrentVersions>");
data.push_str(&versions.to_string());
data.push_str("</NewerNoncurrentVersions>");
}
data.push_str("</NoncurrentVersionExpiration>");
}
// NoncurrentVersionTransition
let has_noncurrent_transition =
rule.noncurrent_version_transition_noncurrent_days.is_some()
|| rule.noncurrent_version_transition_storage_class.is_some()
|| rule.noncurrent_version_transition_newer_versions.is_some();
if has_noncurrent_transition {
data.push_str("<NoncurrentVersionTransition>");
if let Some(days) = rule.noncurrent_version_transition_noncurrent_days {
data.push_str("<NoncurrentDays>");
data.push_str(&days.to_string());
data.push_str("</NoncurrentDays>");
}
if let Some(storage_class) = &rule.noncurrent_version_transition_storage_class {
data.push_str("<StorageClass>");
data.push_str(storage_class);
data.push_str("</StorageClass>");
}
if let Some(versions) = rule.noncurrent_version_transition_newer_versions {
data.push_str("<NewerNoncurrentVersions>");
data.push_str(&versions.to_string());
data.push_str("</NewerNoncurrentVersions>");
}
data.push_str("</NoncurrentVersionTransition>");
}
// Transition
let has_transition = rule.transition_date.is_some()
|| rule.transition_days.is_some()
|| rule.transition_storage_class.is_some();
if has_transition {
data.push_str("<Transition>");
if let Some(date) = rule.transition_date {
data.push_str("<Date>");
data.push_str(&to_iso8601utc(date));
data.push_str("</Date>");
}
if let Some(days) = rule.transition_days {
data.push_str("<Days>");
data.push_str(&days.to_string());
data.push_str("</Days>");
}
if let Some(storage_class) = &rule.transition_storage_class {
data.push_str("<StorageClass>");
data.push_str(storage_class);
data.push_str("</StorageClass>");
}
data.push_str("</Transition>");
}
data.push_str("</Rule>");
}
data.push_str("</LifecycleConfiguration>");
data
}
}
#[derive(PartialEq, Clone, Debug, Default)]
pub struct LifecycleRule {
// Common
pub id: String,
pub status: bool,
pub filter: Filter,
// Expiration
pub expiration_days: Option<u32>,
pub expiration_date: Option<chrono::DateTime<chrono::Utc>>,
pub expiration_expired_object_delete_marker: Option<bool>,
pub expiration_expired_object_all_versions: Option<bool>,
// DelMarkerExpiration
pub del_marker_expiration_days: Option<u32>,
// AllVersionsExpiration
pub all_versions_expiration_days: Option<u32>,
pub all_versions_expiration_delete_marker: Option<bool>,
// Transition
pub transition_days: Option<u32>,
pub transition_date: Option<chrono::DateTime<chrono::Utc>>,
pub transition_storage_class: Option<String>,
// NoncurrentVersionExpiration
pub noncurrent_version_expiration_noncurrent_days: Option<u32>,
pub noncurrent_version_expiration_newer_versions: Option<u32>,
// NoncurrentVersionTransition
pub noncurrent_version_transition_noncurrent_days: Option<u32>,
pub noncurrent_version_transition_storage_class: Option<String>,
pub noncurrent_version_transition_newer_versions: Option<u32>,
// AbortIncompleteMultipartUpload
pub abort_incomplete_multipart_upload_days_after_initiation: Option<u32>,
}
impl LifecycleRule {
pub fn from_xml(rule_elem: &Element) -> Result<Self, Error> {
let mut rule = LifecycleRule::default();
// Parse ID
if let Some(id_elem) = rule_elem.get_child("ID") {
if let Some(id_text) = id_elem.get_text() {
rule.id = id_text.to_string();
}
}
// Parse Status
if let Some(status_elem) = rule_elem.get_child("Status") {
if let Some(status_text) = status_elem.get_text() {
rule.status = status_text == "Enabled";
}
} else {
return Err(Error::XmlError("Missing <Status> element".to_string()));
}
// Parse Filter
if let Some(filter_elem) = rule_elem.get_child("Filter") {
rule.filter = Filter::from_xml(filter_elem)?;
}
// Parse AbortIncompleteMultipartUpload
if let Some(abort_elem) = rule_elem.get_child("AbortIncompleteMultipartUpload") {
if let Some(days_elem) = abort_elem.get_child("DaysAfterInitiation") {
if let Some(days_text) = days_elem.get_text() {
rule.abort_incomplete_multipart_upload_days_after_initiation =
Some(days_text.parse().map_err(|_| {
Error::XmlError("Invalid DaysAfterInitiation value".to_string())
})?);
}
}
}
// Parse Expiration
if let Some(expiration_elem) = rule_elem.get_child("Expiration") {
// Date
if let Some(date_elem) = expiration_elem.get_child("Date") {
if let Some(date_text) = date_elem.get_text() {
// Assume a function that parses ISO8601 to DateTime<Utc>
rule.expiration_date = Some(parse_iso8601(&date_text)?);
}
}
// Days
if let Some(days_elem) = expiration_elem.get_child("Days") {
if let Some(days_text) = days_elem.get_text() {
rule.expiration_days = Some(days_text.parse().map_err(|_| {
Error::XmlError("Invalid Expiration Days value".to_string())
})?);
}
}
// ExpiredObjectDeleteMarker
if let Some(delete_marker_elem) = expiration_elem.get_child("ExpiredObjectDeleteMarker")
{
if let Some(delete_marker_text) = delete_marker_elem.get_text() {
rule.expiration_expired_object_delete_marker =
Some(delete_marker_text == "true");
}
}
// ExpiredObjectAllVersions
if let Some(all_versions_elem) = expiration_elem.get_child("ExpiredObjectAllVersions") {
if let Some(all_versions_text) = all_versions_elem.get_text() {
rule.expiration_expired_object_all_versions = Some(all_versions_text == "true");
}
}
}
// Parse DelMarkerExpiration
if let Some(del_marker_elem) = rule_elem.get_child("DelMarkerExpiration") {
if let Some(days_elem) = del_marker_elem.get_child("Days") {
if let Some(days_text) = days_elem.get_text() {
rule.del_marker_expiration_days = Some(days_text.parse().map_err(|_| {
Error::XmlError("Invalid DelMarkerExpiration Days value".to_string())
})?);
}
}
}
// Parse AllVersionsExpiration
if let Some(all_versions_elem) = rule_elem.get_child("AllVersionsExpiration") {
if let Some(days_elem) = all_versions_elem.get_child("Days") {
if let Some(days_text) = days_elem.get_text() {
rule.all_versions_expiration_days = Some(days_text.parse().map_err(|_| {
Error::XmlError("Invalid AllVersionsExpiration Days value".to_string())
})?);
}
}
if let Some(delete_marker_elem) = all_versions_elem.get_child("DeleteMarker") {
if let Some(delete_marker_text) = delete_marker_elem.get_text() {
rule.all_versions_expiration_delete_marker = Some(delete_marker_text == "true");
}
}
}
// Parse NoncurrentVersionExpiration
if let Some(noncurrent_exp_elem) = rule_elem.get_child("NoncurrentVersionExpiration") {
if let Some(days_elem) = noncurrent_exp_elem.get_child("NoncurrentDays") {
if let Some(days_text) = days_elem.get_text() {
rule.noncurrent_version_expiration_noncurrent_days =
Some(days_text.parse().map_err(|_| {
Error::XmlError(
"Invalid NoncurrentVersionExpiration NoncurrentDays value"
.to_string(),
)
})?);
}
}
if let Some(versions_elem) = noncurrent_exp_elem.get_child("NewerNoncurrentVersions") {
if let Some(versions_text) = versions_elem.get_text() {
rule.noncurrent_version_expiration_newer_versions =
Some(versions_text.parse().map_err(|_| {
Error::XmlError("Invalid NewerNoncurrentVersions value".to_string())
})?);
}
}
}
// Parse NoncurrentVersionTransition
if let Some(noncurrent_trans_elem) = rule_elem.get_child("NoncurrentVersionTransition") {
if let Some(days_elem) = noncurrent_trans_elem.get_child("NoncurrentDays") {
if let Some(days_text) = days_elem.get_text() {
rule.noncurrent_version_transition_noncurrent_days =
Some(days_text.parse().map_err(|_| {
Error::XmlError(
"Invalid NoncurrentVersionTransition NoncurrentDays value"
.to_string(),
)
})?);
}
}
if let Some(storage_elem) = noncurrent_trans_elem.get_child("StorageClass") {
if let Some(storage_text) = storage_elem.get_text() {
rule.noncurrent_version_transition_storage_class =
Some(storage_text.to_string());
}
}
if let Some(versions_elem) = noncurrent_trans_elem.get_child("NewerNoncurrentVersions")
{
if let Some(versions_text) = versions_elem.get_text() {
rule.noncurrent_version_transition_newer_versions =
Some(versions_text.parse().map_err(|_| {
Error::XmlError("Invalid NewerNoncurrentVersions value".to_string())
})?);
}
}
}
// Parse Transition
if let Some(transition_elem) = rule_elem.get_child("Transition") {
// Date
if let Some(date_elem) = transition_elem.get_child("Date") {
if let Some(date_text) = date_elem.get_text() {
rule.transition_date = Some(parse_iso8601(&date_text)?);
}
}
// Days
if let Some(days_elem) = transition_elem.get_child("Days") {
if let Some(days_text) = days_elem.get_text() {
rule.transition_days = Some(days_text.parse().map_err(|_| {
Error::XmlError("Invalid Transition Days value".to_string())
})?);
}
}
// StorageClass
if let Some(storage_elem) = transition_elem.get_child("StorageClass") {
if let Some(storage_text) = storage_elem.get_text() {
rule.transition_storage_class = Some(storage_text.to_string());
}
}
}
Ok(rule)
}
pub fn validate(&self) -> Result<(), Error> {
// Basic validation requirements
// Ensure ID is present
if self.id.is_empty() {
return Err(Error::XmlError("Rule ID cannot be empty".to_string()));
}
// Validate storage classes in transitions
if let Some(storage_class) = &self.transition_storage_class {
if storage_class.is_empty() {
return Err(Error::XmlError(
"Transition StorageClass cannot be empty".to_string(),
));
}
}
if let Some(storage_class) = &self.noncurrent_version_transition_storage_class {
if storage_class.is_empty() {
return Err(Error::XmlError(
"NoncurrentVersionTransition StorageClass cannot be empty".to_string(),
));
}
}
// Check that expiration has either days or date, not both
if self.expiration_days.is_some() && self.expiration_date.is_some() {
return Err(Error::XmlError(
"Expiration cannot specify both Days and Date".to_string(),
));
}
// Check that transition has either days or date, not both
if self.transition_days.is_some() && self.transition_date.is_some() {
return Err(Error::XmlError(
"Transition cannot specify both Days and Date".to_string(),
));
}
Ok(())
}
}
// Helper function to parse ISO8601 dates
fn parse_iso8601(date_str: &str) -> Result<chrono::DateTime<chrono::Utc>, Error> {
chrono::DateTime::parse_from_rfc3339(date_str)
.map(|dt| dt.with_timezone(&chrono::Utc))
.map_err(|_| Error::XmlError(format!("Invalid date format: {}", date_str)))
}

View File

@ -20,6 +20,7 @@ pub mod client;
pub mod creds; pub mod creds;
pub mod error; pub mod error;
pub mod http; pub mod http;
pub mod lifecycle_config;
pub mod multimap; pub mod multimap;
mod object_content; mod object_content;
pub mod response; pub mod response;

View File

@ -14,10 +14,12 @@
// limitations under the License. // limitations under the License.
use crate::s3::error::Error; use crate::s3::error::Error;
use crate::s3::types::{FromS3Response, LifecycleConfig, S3Request}; use crate::s3::lifecycle_config::LifecycleConfig;
use crate::s3::utils::take_bucket; use crate::s3::types::{FromS3Response, S3Request};
use crate::s3::utils::{UtcTime, take_bucket};
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Buf; use bytes::Buf;
use chrono::{DateTime, NaiveDateTime, Utc};
use http::HeaderMap; use http::HeaderMap;
use std::mem; use std::mem;
use xmltree::Element; use xmltree::Element;
@ -47,6 +49,10 @@ pub struct GetBucketLifecycleResponse {
/// ///
/// If the bucket has no lifecycle configuration, this field may contain an empty configuration. /// If the bucket has no lifecycle configuration, this field may contain an empty configuration.
pub config: LifecycleConfig, pub config: LifecycleConfig,
/// Optional value of `X-Minio-LifecycleConfig-UpdatedAt` header, indicating the last update
/// time of the lifecycle configuration.
pub updated_at: Option<UtcTime>,
} }
#[async_trait] #[async_trait]
@ -62,11 +68,21 @@ impl FromS3Response for GetBucketLifecycleResponse {
let mut root = Element::parse(body.reader())?; let mut root = Element::parse(body.reader())?;
LifecycleConfig::from_xml(&mut root)? LifecycleConfig::from_xml(&mut root)?
}; };
let updated_at: Option<DateTime<Utc>> = headers
.get("x-minio-lifecycleconfig-updatedat")
.and_then(|v| v.to_str().ok())
.and_then(|v| {
NaiveDateTime::parse_from_str(v, "%Y%m%dT%H%M%SZ")
.ok()
.map(|naive| DateTime::from_naive_utc_and_offset(naive, Utc))
});
Ok(Self { Ok(Self {
headers, headers,
region: req.inner_region, region: req.inner_region,
bucket: take_bucket(req.bucket)?, bucket: take_bucket(req.bucket)?,
config, config,
updated_at,
}) })
} }
} }

View File

@ -17,9 +17,7 @@
use super::client::{Client, DEFAULT_REGION}; use super::client::{Client, DEFAULT_REGION};
use crate::s3::error::Error; use crate::s3::error::Error;
use crate::s3::utils::{ use crate::s3::utils::{UtcTime, get_option_text, get_text};
UtcTime, from_iso8601utc, get_default_text, get_option_text, get_text, to_iso8601utc,
};
use crate::s3::multimap::Multimap; use crate::s3::multimap::Multimap;
use crate::s3::segmented_bytes::SegmentedBytes; use crate::s3::segmented_bytes::SegmentedBytes;
@ -934,7 +932,7 @@ pub struct AndOperator {
pub tags: Option<HashMap<String, String>>, pub tags: Option<HashMap<String, String>>,
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Default)]
/// Filter information /// Filter information
pub struct Filter { pub struct Filter {
pub and_operator: Option<AndOperator>, pub and_operator: Option<AndOperator>,
@ -1050,309 +1048,6 @@ impl Filter {
} }
} }
#[derive(PartialEq, Clone, Debug)]
/// Lifecycle rule information
pub struct LifecycleRule {
pub abort_incomplete_multipart_upload_days_after_initiation: Option<usize>,
pub expiration_date: Option<UtcTime>,
pub expiration_days: Option<usize>,
pub expiration_expired_object_delete_marker: Option<bool>,
pub filter: Filter,
pub id: String,
pub noncurrent_version_expiration_noncurrent_days: Option<usize>,
pub noncurrent_version_transition_noncurrent_days: Option<usize>,
pub noncurrent_version_transition_storage_class: Option<String>,
pub status: bool,
pub transition_date: Option<UtcTime>,
pub transition_days: Option<usize>,
pub transition_storage_class: Option<String>,
}
impl LifecycleRule {
pub fn from_xml(element: &Element) -> Result<LifecycleRule, Error> {
let expiration = element.get_child("Expiration");
let noncurrent_version_transition = element.get_child("NoncurrentVersionTransition");
let transition = element.get_child("Transition");
Ok(LifecycleRule {
abort_incomplete_multipart_upload_days_after_initiation: match element
.get_child("AbortIncompleteMultipartUpload")
{
Some(v) => {
let text = get_text(v, "DaysAfterInitiation")?;
Some(text.parse::<usize>()?)
}
None => None,
},
expiration_date: match expiration {
Some(v) => {
let text = get_text(v, "Date")?;
Some(from_iso8601utc(&text)?)
}
None => None,
},
expiration_days: match expiration {
Some(v) => {
let text = get_text(v, "Days")?;
Some(text.parse::<usize>()?)
}
None => None,
},
expiration_expired_object_delete_marker: match expiration {
Some(v) => Some(get_text(v, "ExpiredObjectDeleteMarker")?.to_lowercase() == "true"),
None => None,
},
filter: Filter::from_xml(
element
.get_child("Filter")
.ok_or(Error::XmlError("<Filter> tag not found".to_string()))?,
)?,
id: get_default_text(element, "ID"),
noncurrent_version_expiration_noncurrent_days: match element
.get_child("NoncurrentVersionExpiration")
{
Some(v) => {
let text = get_text(v, "NoncurrentDays")?;
Some(text.parse::<usize>()?)
}
None => None,
},
noncurrent_version_transition_noncurrent_days: match noncurrent_version_transition {
Some(v) => {
let text = get_text(v, "NoncurrentDays")?;
Some(text.parse::<usize>()?)
}
None => None,
},
noncurrent_version_transition_storage_class: match noncurrent_version_transition {
Some(v) => Some(get_text(v, "StorageClass")?),
None => None,
},
status: get_text(element, "Status")?.to_lowercase() == "Enabled",
transition_date: match transition {
Some(v) => {
let text = get_text(v, "Date")?;
Some(from_iso8601utc(&text)?)
}
None => None,
},
transition_days: match transition {
Some(v) => {
let text = get_text(v, "Days")?;
Some(text.parse::<usize>()?)
}
None => None,
},
transition_storage_class: match transition {
Some(v) => Some(get_text(v, "StorageClass")?),
None => None,
},
})
}
pub fn validate(&self) -> Result<(), Error> {
if self
.abort_incomplete_multipart_upload_days_after_initiation
.is_none()
&& self.expiration_date.is_none()
&& self.expiration_days.is_none()
&& self.expiration_expired_object_delete_marker.is_none()
&& self.noncurrent_version_expiration_noncurrent_days.is_none()
&& self.noncurrent_version_transition_storage_class.is_none()
&& self.transition_date.is_none()
&& self.transition_days.is_none()
&& self.transition_storage_class.is_none()
{
return Err(Error::MissingLifecycleAction);
}
self.filter.validate()?;
if self.expiration_expired_object_delete_marker.is_some() {
if self.expiration_date.is_some() || self.expiration_days.is_some() {
return Err(Error::InvalidExpiredObjectDeleteMarker);
}
} else if self.expiration_date.is_some() && self.expiration_days.is_some() {
return Err(Error::InvalidDateAndDays(String::from("expiration")));
}
if self.transition_date.is_some() && self.transition_days.is_some() {
return Err(Error::InvalidDateAndDays(String::from("transition")));
}
if self.id.len() > 255 {
return Err(Error::InvalidLifecycleRuleId);
}
Ok(())
}
}
#[derive(PartialEq, Clone, Debug, Default)]
/// Lifecycle configuration
pub struct LifecycleConfig {
pub rules: Vec<LifecycleRule>,
}
impl LifecycleConfig {
pub fn from_xml(root: &mut Element) -> Result<LifecycleConfig, Error> {
let mut config = LifecycleConfig { rules: Vec::new() };
// TODO consider consuming root
if let Some(v) = root.get_child("Rule") {
for rule in &v.children {
config
.rules
.push(LifecycleRule::from_xml(rule.as_element().ok_or(
Error::XmlError("<Rule> tag not found".to_string()),
)?)?);
}
}
Ok(config)
}
pub fn validate(&self) -> Result<(), Error> {
for rule in &self.rules {
rule.validate()?;
}
Ok(())
}
pub fn to_xml(&self) -> String {
let mut data = String::from("<LifecycleConfiguration>");
for rule in &self.rules {
data.push_str("<Rule>");
if rule
.abort_incomplete_multipart_upload_days_after_initiation
.is_some()
{
data.push_str("<AbortIncompleteMultipartUpload><DaysAfterInitiation>");
data.push_str(
&rule
.abort_incomplete_multipart_upload_days_after_initiation
.unwrap()
.to_string(),
);
data.push_str("</DaysAfterInitiation></AbortIncompleteMultipartUpload>");
}
if rule.expiration_date.is_some()
|| rule.expiration_days.is_some()
|| rule.expiration_expired_object_delete_marker.is_some()
{
data.push_str("<Expiration>");
if rule.expiration_date.is_some() {
data.push_str("<Date>");
data.push_str(&to_iso8601utc(rule.expiration_date.unwrap()));
data.push_str("</Date>");
}
if rule.expiration_days.is_some() {
data.push_str("<Days>");
data.push_str(&rule.expiration_days.unwrap().to_string());
data.push_str("</Days>");
}
if rule.expiration_expired_object_delete_marker.is_some() {
data.push_str("<ExpiredObjectDeleteMarker>");
data.push_str(
&rule
.expiration_expired_object_delete_marker
.unwrap()
.to_string(),
);
data.push_str("</ExpiredObjectDeleteMarker>");
}
data.push_str("</Expiration>");
}
data.push_str(&rule.filter.to_xml());
if !rule.id.is_empty() {
data.push_str("<ID>");
data.push_str(&rule.id);
data.push_str("</ID>");
}
if rule.noncurrent_version_expiration_noncurrent_days.is_some() {
data.push_str("<NoncurrentVersionExpiration><NoncurrentDays>");
data.push_str(
&rule
.noncurrent_version_expiration_noncurrent_days
.unwrap()
.to_string(),
);
data.push_str("</NoncurrentDays></NoncurrentVersionExpiration>");
}
if rule.noncurrent_version_transition_noncurrent_days.is_some()
|| rule.noncurrent_version_transition_storage_class.is_some()
{
data.push_str("<NoncurrentVersionTransition>");
if rule.noncurrent_version_transition_noncurrent_days.is_some() {
data.push_str("<NoncurrentDays>");
data.push_str(
&rule
.noncurrent_version_expiration_noncurrent_days
.unwrap()
.to_string(),
);
data.push_str("</NoncurrentDays>");
}
if rule.noncurrent_version_transition_storage_class.is_some() {
data.push_str("<StorageClass>");
data.push_str(
rule.noncurrent_version_transition_storage_class
.as_ref()
.unwrap(),
);
data.push_str("</StorageClass>");
}
data.push_str("</NoncurrentVersionTransition>");
}
data.push_str("<Status>");
if rule.status {
data.push_str("Enabled");
} else {
data.push_str("Disabled");
}
data.push_str("</Status>");
if rule.transition_date.is_some()
|| rule.transition_days.is_some()
|| rule.transition_storage_class.is_some()
{
data.push_str("<Transition>");
if rule.transition_date.is_some() {
data.push_str("<Date>");
data.push_str(&to_iso8601utc(rule.transition_date.unwrap()));
data.push_str("</Date>");
}
if rule.transition_days.is_some() {
data.push_str("<Days>");
data.push_str(&rule.transition_days.unwrap().to_string());
data.push_str("</Days>");
}
if rule.transition_storage_class.is_some() {
data.push_str("<StorageClass>");
data.push_str(rule.transition_storage_class.as_ref().unwrap());
data.push_str("</StorageClass>");
}
data.push_str("</Transition>");
}
data.push_str("</Rule>");
}
data.push_str("</LifecycleConfiguration>");
data
}
}
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn parse_common_notification_config( fn parse_common_notification_config(
element: &mut Element, element: &mut Element,
@ -1768,7 +1463,7 @@ impl ReplicationTime {
} }
} }
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug, Default)]
/// Destination information /// Destination information
pub struct Destination { pub struct Destination {
pub bucket_arn: String, pub bucket_arn: String,
@ -1909,7 +1604,7 @@ pub struct SourceSelectionCriteria {
pub sse_kms_encrypted_objects_status: Option<bool>, pub sse_kms_encrypted_objects_status: Option<bool>,
} }
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug, Default)]
/// Replication rule information /// Replication rule information
pub struct ReplicationRule { pub struct ReplicationRule {
pub destination: Destination, pub destination: Destination,

View File

@ -14,10 +14,12 @@
// limitations under the License. // limitations under the License.
use minio::s3::client::DEFAULT_REGION; use minio::s3::client::DEFAULT_REGION;
use minio::s3::error::{Error, ErrorCode};
use minio::s3::lifecycle_config::LifecycleConfig;
use minio::s3::response::{ use minio::s3::response::{
DeleteBucketLifecycleResponse, GetBucketLifecycleResponse, PutBucketLifecycleResponse, DeleteBucketLifecycleResponse, GetBucketLifecycleResponse, PutBucketLifecycleResponse,
}; };
use minio::s3::types::{LifecycleConfig, S3Api}; use minio::s3::types::S3Api;
use minio_common::example::create_bucket_lifecycle_config_examples; use minio_common::example::create_bucket_lifecycle_config_examples;
use minio_common::test_context::TestContext; use minio_common::test_context::TestContext;
@ -37,39 +39,47 @@ async fn bucket_lifecycle() {
.unwrap(); .unwrap();
assert_eq!(resp.bucket, bucket_name); assert_eq!(resp.bucket, bucket_name);
assert_eq!(resp.region, DEFAULT_REGION); assert_eq!(resp.region, DEFAULT_REGION);
//println!("response of setting lifecycle: resp={:?}", resp);
if false { let resp: GetBucketLifecycleResponse = ctx
// TODO panics with: called `Result::unwrap()` on an `Err` value: XmlError("<Filter> tag not found") .client
let resp: GetBucketLifecycleResponse = ctx .get_bucket_lifecycle(&bucket_name)
.client .with_updated_at(false)
.get_bucket_lifecycle(&bucket_name) .send()
.send() .await
.await .unwrap();
.unwrap(); assert_eq!(resp.config, config);
assert_eq!(resp.config, config); assert_eq!(resp.bucket, bucket_name);
assert_eq!(resp.bucket, bucket_name); assert_eq!(resp.region, DEFAULT_REGION);
assert_eq!(resp.region, DEFAULT_REGION); assert!(resp.updated_at.is_none());
println!("response of getting lifecycle: resp={:?}", resp);
}
let _resp: DeleteBucketLifecycleResponse = ctx let resp: GetBucketLifecycleResponse = ctx
.client
.get_bucket_lifecycle(&bucket_name)
.with_updated_at(true)
.send()
.await
.unwrap();
assert_eq!(resp.config, config);
assert_eq!(resp.bucket, bucket_name);
assert_eq!(resp.region, DEFAULT_REGION);
assert!(resp.updated_at.is_some());
let resp: DeleteBucketLifecycleResponse = ctx
.client .client
.delete_bucket_lifecycle(&bucket_name) .delete_bucket_lifecycle(&bucket_name)
.send() .send()
.await .await
.unwrap(); .unwrap();
//println!("response of deleting lifecycle: resp={:?}", resp); assert_eq!(resp.bucket, bucket_name);
assert_eq!(resp.region, DEFAULT_REGION);
if false { let resp: Result<GetBucketLifecycleResponse, Error> =
// TODO panics with: called `Result::unwrap()` on an `Err` value: XmlError("<Filter> tag not found") ctx.client.get_bucket_lifecycle(&bucket_name).send().await;
let resp: GetBucketLifecycleResponse = ctx match resp {
.client Err(Error::S3Error(e)) => assert_eq!(e.code, ErrorCode::NoSuchLifecycleConfiguration),
.get_bucket_lifecycle(&bucket_name) v => panic!(
.send() "Expected error S3Error(NoSuchLifecycleConfiguration): but got {:?}",
.await v
.unwrap(); ),
println!("response of getting policy: resp={:?}", resp);
//assert_eq!(resp.config, LifecycleConfig::default());
} }
} }