garage_api_s3 2.3.0

S3 API server crate for the Garage object store
Documentation
use hyper::{Request, Response, StatusCode};

use garage_util::data::*;

use garage_model::s3::object_table::*;

use garage_api_common::helpers::*;

use crate::api_server::{ReqBody, ResBody};
use crate::error::*;
use crate::put::next_timestamp;
use crate::xml as s3_xml;

async fn handle_delete_internal(ctx: &ReqCtx, key: &str) -> Result<(Uuid, Uuid), Error> {
	let ReqCtx {
		garage, bucket_id, ..
	} = ctx;
	let object = garage
		.object_table
		.get(bucket_id, &key.to_string())
		.await?
		.ok_or(Error::NoSuchKey)?; // No need to delete

	let del_timestamp = next_timestamp(Some(&object));
	let del_uuid = gen_uuid();

	let deleted_version = object
		.versions()
		.iter()
		.rev()
		.find(|v| !matches!(&v.state, ObjectVersionState::Aborted))
		.or_else(|| object.versions().iter().next_back());
	let deleted_version = match deleted_version {
		Some(dv) => dv.uuid,
		None => {
			warn!("Object has no versions: {:?}", object);
			Uuid::from([0u8; 32])
		}
	};

	let object = Object::new(
		*bucket_id,
		key.into(),
		vec![ObjectVersion {
			uuid: del_uuid,
			timestamp: del_timestamp,
			state: ObjectVersionState::Complete(ObjectVersionData::DeleteMarker),
		}],
	);

	garage.object_table.insert(&object).await?;

	Ok((deleted_version, del_uuid))
}

pub async fn handle_delete(ctx: ReqCtx, key: &str) -> Result<Response<ResBody>, Error> {
	match handle_delete_internal(&ctx, key).await {
		Ok(_) | Err(Error::NoSuchKey) => Ok(Response::builder()
			.status(StatusCode::NO_CONTENT)
			.body(empty_body())
			.unwrap()),
		Err(e) => Err(e),
	}
}

pub async fn handle_delete_objects(
	ctx: ReqCtx,
	req: Request<ReqBody>,
) -> Result<Response<ResBody>, Error> {
	let body = req.into_body().collect().await?;

	let cmd_xml = roxmltree::Document::parse(std::str::from_utf8(&body)?)?;
	let cmd = parse_delete_objects_xml(&cmd_xml).ok_or_bad_request("Invalid delete XML query")?;

	let mut ret_deleted = Vec::new();
	let mut ret_errors = Vec::new();

	for obj in cmd.objects.iter() {
		match handle_delete_internal(&ctx, &obj.key).await {
			Ok((deleted_version, delete_marker_version)) => {
				if cmd.quiet {
					continue;
				}
				ret_deleted.push(s3_xml::Deleted {
					key: s3_xml::Value(obj.key.clone()),
					version_id: s3_xml::Value(hex::encode(deleted_version)),
					delete_marker_version_id: s3_xml::Value(hex::encode(delete_marker_version)),
				});
			}
			Err(e) => {
				ret_errors.push(s3_xml::DeleteError {
					code: s3_xml::Value(e.aws_code().to_string()),
					key: Some(s3_xml::Value(obj.key.clone())),
					message: s3_xml::Value(format!("{}", e)),
					version_id: None,
				});
			}
		}
	}

	let xml = s3_xml::to_xml_with_header(&s3_xml::DeleteResult {
		xmlns: (),
		deleted: ret_deleted,
		errors: ret_errors,
	})?;

	Ok(Response::builder()
		.header("Content-Type", "application/xml")
		.body(string_body(xml))?)
}

struct DeleteRequest {
	quiet: bool,
	objects: Vec<DeleteObject>,
}

struct DeleteObject {
	key: String,
}

fn parse_delete_objects_xml(xml: &roxmltree::Document) -> Option<DeleteRequest> {
	let mut ret = DeleteRequest {
		quiet: false,
		objects: vec![],
	};

	let root = xml.root();
	let delete = root.children().find(|n| n.is_element())?;

	if !delete.has_tag_name("Delete") {
		return None;
	}

	for item in delete.children() {
		// Skip text nodes introduced by formatted XML.
		if !item.is_element() {
			// text nodes are allowed only if they contain whitespace characters only
			if !item.text()?.trim().is_empty() {
				return None;
			}
			continue;
		}

		if item.has_tag_name("Object") {
			let key = item.children().find(|e| e.has_tag_name("Key"))?;
			let key_str = key.text()?;
			ret.objects.push(DeleteObject {
				key: key_str.to_string(),
			});
		} else if item.has_tag_name("Quiet") {
			ret.quiet = item.text()? == "true";
		} else {
			return None;
		}
	}

	Some(ret)
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn parse_delete_objects_xml_with_formatting() {
		let body = r#"
			<Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
			    <Object>
			        <Key>1_746573745f66696c65</Key>
			    </Object>
			    <Quiet>true</Quiet>
			</Delete>
		"#;
		let xml = roxmltree::Document::parse(body).expect("valid delete XML");
		let req = parse_delete_objects_xml(&xml).expect("request should be parsed");

		assert_eq!(req.objects.len(), 1);
		assert_eq!(req.objects[0].key, "1_746573745f66696c65");
		assert!(req.quiet);
	}

	#[test]
	fn parse_delete_objects_xml_rejects_non_whitespace_text_node() {
		let body = r#"<Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/">oops<Object><Key>1_746573745f66696c65</Key></Object></Delete>"#;
		let xml = roxmltree::Document::parse(body).expect("valid XML");
		let req = parse_delete_objects_xml(&xml);
		assert!(req.is_none());
	}

	#[test]
	fn parse_delete_objects_xml_rejects_pretty_print_with_stray_text() {
		let body = r#"
			<Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
			    oops
			    <Object>
			        <Key>1_746573745f66696c65</Key>
			    </Object>
			</Delete>
		"#;
		let xml = roxmltree::Document::parse(body).expect("valid XML");
		let req = parse_delete_objects_xml(&xml);
		assert!(req.is_none());
	}

	#[test]
	fn parse_delete_objects_xml_accepts_compact_valid_xml() {
		let body = r#"<Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Object><Key>1_746573745f66696c65</Key></Object><Quiet>false</Quiet></Delete>"#;
		let xml = roxmltree::Document::parse(body).expect("valid XML");
		let req = parse_delete_objects_xml(&xml).expect("request should be parsed");
		assert_eq!(req.objects.len(), 1);
		assert_eq!(req.objects[0].key, "1_746573745f66696c65");
		assert!(!req.quiet);
	}
}