diff --git a/src/api/s3/lifecycle.rs b/src/api/s3/lifecycle.rs index cb0cc83a..48265870 100644 --- a/src/api/s3/lifecycle.rs +++ b/src/api/s3/lifecycle.rs @@ -22,13 +22,7 @@ pub async fn handle_get_lifecycle(bucket: &Bucket) -> Result, Err .ok_or_internal_error("Bucket should not be deleted at this point")?; if let Some(lifecycle) = param.lifecycle_config.get() { - let wc = LifecycleConfiguration { - xmlns: (), - lifecycle_rules: lifecycle - .iter() - .map(LifecycleRule::from_garage_lifecycle_rule) - .collect::>(), - }; + let wc = LifecycleConfiguration::from_garage_lifecycle_config(lifecycle); let xml = to_xml_with_header(&wc)?; Ok(Response::builder() .status(StatusCode::OK) @@ -81,9 +75,10 @@ pub async fn handle_put_lifecycle( let conf: LifecycleConfiguration = from_reader(&body as &[u8])?; - param - .lifecycle_config - .update(Some(conf.validate_into_garage_lifecycle_config()?)); + 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() @@ -109,7 +104,7 @@ pub struct LifecycleRule { #[serde(rename = "Status")] pub status: Value, #[serde(rename = "Filter", default)] - pub filter: Filter, + pub filter: Option, #[serde(rename = "Expiration", default)] pub expiration: Option, #[serde(rename = "AbortIncompleteMultipartUpload", default)] @@ -139,11 +134,13 @@ pub struct Expiration { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct AbortIncompleteMpu { #[serde(rename = "DaysAfterInitiation")] - pub days: Option, + pub days: IntValue, } impl LifecycleConfiguration { - pub fn validate_into_garage_lifecycle_config(self) -> Result, Error> { + 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()?); @@ -163,12 +160,136 @@ impl LifecycleConfiguration { } impl LifecycleRule { - pub fn validate_into_garage_lifecycle_rule(self) -> Result { - todo!() + 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 { - todo!() + 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 usize), + size_lt: self.size_lt.map(|x| x.0 as usize), + } + } + + 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)) => { + if date.0.parse::().is_err() { + return Err("Invalid expiration "); + } + 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(days) => Expiration { + days: None, + at_date: Some(Value::from(days.as_str())), + }, + } } } @@ -213,26 +334,24 @@ mod tests { LifecycleRule { id: Some("id1".into()), status: "Enabled".into(), - filter: Filter { + filter: Some(Filter { prefix: Some("documents/".into()), ..Default::default() - }, - expiration: None, - abort_incomplete_mpu: Some(AbortIncompleteMpu { - days: Some(IntValue(7)), }), + expiration: None, + abort_incomplete_mpu: Some(AbortIncompleteMpu { days: IntValue(7) }), }, LifecycleRule { id: Some("id2".into()), status: "Enabled".into(), - filter: Filter { + 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, @@ -251,6 +370,41 @@ mod tests { 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(()) } } diff --git a/src/model/bucket_table.rs b/src/model/bucket_table.rs index dc4e4509..fed20e05 100644 --- a/src/model/bucket_table.rs +++ b/src/model/bucket_table.rs @@ -90,7 +90,7 @@ mod v08 { /// A lifecycle filter is a set of conditions that must all be true. /// For each condition, if it is None, it is not verified (always true), /// and if it is Some(x), then it is verified for value x - #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Default)] pub struct LifecycleFilter { /// If Some(x), object key has to start with prefix x pub prefix: Option,