use quick_xml::de::from_reader; use std::sync::Arc; use hyper::{body::HttpBody, Body, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; use crate::s3::error::*; use crate::s3::xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; use crate::signature::verify_signed_content; use garage_model::bucket_table::{ parse_lifecycle_date, Bucket, LifecycleExpiration as GarageLifecycleExpiration, LifecycleFilter as GarageLifecycleFilter, LifecycleRule as GarageLifecycleRule, }; use garage_model::garage::Garage; use garage_util::data::*; pub async fn handle_get_lifecycle(bucket: &Bucket) -> Result, Error> { let param = bucket .params() .ok_or_internal_error("Bucket should not be deleted at this point")?; if let Some(lifecycle) = param.lifecycle_config.get() { let wc = LifecycleConfiguration::from_garage_lifecycle_config(lifecycle); let xml = to_xml_with_header(&wc)?; Ok(Response::builder() .status(StatusCode::OK) .header(http::header::CONTENT_TYPE, "application/xml") .body(Body::from(xml))?) } else { Ok(Response::builder() .status(StatusCode::NO_CONTENT) .body(Body::empty())?) } } pub async fn handle_delete_lifecycle( garage: Arc, mut bucket: Bucket, ) -> Result, Error> { let param = bucket .params_mut() .ok_or_internal_error("Bucket should not be deleted at this point")?; param.lifecycle_config.update(None); garage.bucket_table.insert(&bucket).await?; Ok(Response::builder() .status(StatusCode::NO_CONTENT) .body(Body::empty())?) } pub async fn handle_put_lifecycle( garage: Arc, mut bucket: Bucket, req: Request, content_sha256: Option, ) -> Result, Error> { let body = req.into_body().collect().await?.to_bytes(); if let Some(content_sha256) = content_sha256 { verify_signed_content(content_sha256, &body[..])?; } let param = bucket .params_mut() .ok_or_internal_error("Bucket should not be deleted at this point")?; let conf: LifecycleConfiguration = from_reader(&body as &[u8])?; let config = conf .validate_into_garage_lifecycle_config() .ok_or_bad_request("Invalid lifecycle configuration")?; param.lifecycle_config.update(Some(config)); garage.bucket_table.insert(&bucket).await?; Ok(Response::builder() .status(StatusCode::OK) .body(Body::empty())?) } // ---- SERIALIZATION AND DESERIALIZATION TO/FROM S3 XML ---- #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct LifecycleConfiguration { #[serde(serialize_with = "xmlns_tag", skip_deserializing)] pub xmlns: (), #[serde(rename = "Rule")] pub lifecycle_rules: Vec, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct LifecycleRule { #[serde(rename = "ID")] pub id: Option, #[serde(rename = "Status")] pub status: Value, #[serde(rename = "Filter", default)] pub filter: Option, #[serde(rename = "Expiration", default)] pub expiration: Option, #[serde(rename = "AbortIncompleteMultipartUpload", default)] pub abort_incomplete_mpu: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)] pub struct Filter { #[serde(rename = "And")] pub and: Option>, #[serde(rename = "Prefix")] pub prefix: Option, #[serde(rename = "ObjectSizeGreaterThan")] pub size_gt: Option, #[serde(rename = "ObjectSizeLessThan")] pub size_lt: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Expiration { #[serde(rename = "Days")] pub days: Option, #[serde(rename = "Date")] pub at_date: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct AbortIncompleteMpu { #[serde(rename = "DaysAfterInitiation")] pub days: IntValue, } impl LifecycleConfiguration { pub fn validate_into_garage_lifecycle_config( self, ) -> Result, &'static str> { let mut ret = vec![]; for rule in self.lifecycle_rules { ret.push(rule.validate_into_garage_lifecycle_rule()?); } Ok(ret) } pub fn from_garage_lifecycle_config(config: &[GarageLifecycleRule]) -> Self { Self { xmlns: (), lifecycle_rules: config .iter() .map(LifecycleRule::from_garage_lifecycle_rule) .collect(), } } } impl LifecycleRule { pub fn validate_into_garage_lifecycle_rule(self) -> Result { let enabled = match self.status.0.as_str() { "Enabled" => true, "Disabled" => false, _ => return Err("invalid value for "), }; let filter = self .filter .map(Filter::validate_into_garage_lifecycle_filter) .transpose()? .unwrap_or_default(); let abort_incomplete_mpu_days = self.abort_incomplete_mpu.map(|x| x.days.0 as usize); let expiration = self .expiration .map(Expiration::validate_into_garage_lifecycle_expiration) .transpose()?; Ok(GarageLifecycleRule { id: self.id.map(|x| x.0), enabled, filter, abort_incomplete_mpu_days, expiration, }) } pub fn from_garage_lifecycle_rule(rule: &GarageLifecycleRule) -> Self { Self { id: rule.id.as_deref().map(Value::from), status: if rule.enabled { Value::from("Enabled") } else { Value::from("Disabled") }, filter: Filter::from_garage_lifecycle_filter(&rule.filter), abort_incomplete_mpu: rule .abort_incomplete_mpu_days .map(|days| AbortIncompleteMpu { days: IntValue(days as i64), }), expiration: rule .expiration .as_ref() .map(Expiration::from_garage_lifecycle_expiration), } } } impl Filter { pub fn count(&self) -> i32 { fn count(x: &Option) -> i32 { x.as_ref().map(|_| 1).unwrap_or(0) } count(&self.prefix) + count(&self.size_gt) + count(&self.size_lt) } pub fn validate_into_garage_lifecycle_filter( self, ) -> Result { if self.count() > 0 && self.and.is_some() { Err("Filter tag cannot contain both and another condition") } else if let Some(and) = self.and { if and.and.is_some() { return Err("Nested tags"); } Ok(and.internal_into_garage_lifecycle_filter()) } else if self.count() > 1 { Err("Multiple Filter conditions must be wrapped in an tag") } else { Ok(self.internal_into_garage_lifecycle_filter()) } } fn internal_into_garage_lifecycle_filter(self) -> GarageLifecycleFilter { GarageLifecycleFilter { prefix: self.prefix.map(|x| x.0), size_gt: self.size_gt.map(|x| x.0 as u64), size_lt: self.size_lt.map(|x| x.0 as u64), } } pub fn from_garage_lifecycle_filter(rule: &GarageLifecycleFilter) -> Option { let filter = Filter { and: None, prefix: rule.prefix.as_deref().map(Value::from), size_gt: rule.size_gt.map(|x| IntValue(x as i64)), size_lt: rule.size_lt.map(|x| IntValue(x as i64)), }; match filter.count() { 0 => None, 1 => Some(filter), _ => Some(Filter { and: Some(Box::new(filter)), ..Default::default() }), } } } impl Expiration { pub fn validate_into_garage_lifecycle_expiration( self, ) -> Result { match (self.days, self.at_date) { (Some(_), Some(_)) => Err("cannot have both and in "), (None, None) => Err(" must contain either or "), (Some(days), None) => Ok(GarageLifecycleExpiration::AfterDays(days.0 as usize)), (None, Some(date)) => { parse_lifecycle_date(&date.0)?; Ok(GarageLifecycleExpiration::AtDate(date.0)) } } } pub fn from_garage_lifecycle_expiration(exp: &GarageLifecycleExpiration) -> Self { match exp { GarageLifecycleExpiration::AfterDays(days) => Expiration { days: Some(IntValue(*days as i64)), at_date: None, }, GarageLifecycleExpiration::AtDate(date) => Expiration { days: None, at_date: Some(Value(date.to_string())), }, } } } #[cfg(test)] mod tests { use super::*; use quick_xml::de::from_str; #[test] fn test_deserialize_lifecycle_config() -> Result<(), Error> { let message = r#" id1 Enabled documents/ 7 id2 Enabled logs/ 1000000 365 "#; let conf: LifecycleConfiguration = from_str(message).unwrap(); let ref_value = LifecycleConfiguration { xmlns: (), lifecycle_rules: vec![ LifecycleRule { id: Some("id1".into()), status: "Enabled".into(), filter: Some(Filter { prefix: Some("documents/".into()), ..Default::default() }), expiration: None, abort_incomplete_mpu: Some(AbortIncompleteMpu { days: IntValue(7) }), }, LifecycleRule { id: Some("id2".into()), status: "Enabled".into(), filter: Some(Filter { and: Some(Box::new(Filter { prefix: Some("logs/".into()), size_gt: Some(IntValue(1000000)), ..Default::default() })), ..Default::default() }), expiration: Some(Expiration { days: Some(IntValue(365)), at_date: None, }), abort_incomplete_mpu: None, }, ], }; assert_eq! { ref_value, conf }; let message2 = to_xml_with_header(&ref_value)?; let cleanup = |c: &str| c.replace(char::is_whitespace, ""); assert_eq!(cleanup(message), cleanup(&message2)); // Check validation let validated = ref_value .validate_into_garage_lifecycle_config() .ok_or_bad_request("invalid xml config")?; let ref_config = vec![ GarageLifecycleRule { id: Some("id1".into()), enabled: true, filter: GarageLifecycleFilter { prefix: Some("documents/".into()), ..Default::default() }, expiration: None, abort_incomplete_mpu_days: Some(7), }, GarageLifecycleRule { id: Some("id2".into()), enabled: true, filter: GarageLifecycleFilter { prefix: Some("logs/".into()), size_gt: Some(1000000), ..Default::default() }, expiration: Some(GarageLifecycleExpiration::AfterDays(365)), abort_incomplete_mpu_days: None, }, ]; assert_eq!(validated, ref_config); let message3 = to_xml_with_header(&LifecycleConfiguration::from_garage_lifecycle_config( &validated, ))?; assert_eq!(cleanup(message), cleanup(&message3)); Ok(()) } }