diff --git a/Cargo.lock b/Cargo.lock index 880a1462..d4f5ec58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -469,6 +469,15 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encoding_rs" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -704,6 +713,7 @@ dependencies = [ "idna", "log", "md-5", + "multer", "nom", "percent-encoding", "pin-project", @@ -1314,6 +1324,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1342,6 +1358,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "multer" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836" +dependencies = [ + "bytes 1.1.0", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.2", + "version_check", +] + [[package]] name = "netapp" version = "0.3.0" @@ -1670,7 +1704,7 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", + "spin 0.5.2", "untrusted", "web-sys", "winapi", @@ -1933,6 +1967,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" + [[package]] name = "static_init" version = "1.0.2" diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index e93e5ec5..ad7bdc65 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -40,6 +40,7 @@ http = "0.2" httpdate = "0.3" http-range = "0.1" hyper = { version = "0.14", features = ["server", "http1", "runtime", "tcp", "stream"] } +multer = "2.0" percent-encoding = "2.1.0" roxmltree = "0.14" serde = { version = "1.0", features = ["derive"] } diff --git a/src/api/api_server.rs b/src/api/api_server.rs index a0c7655c..77587de8 100644 --- a/src/api/api_server.rs +++ b/src/api/api_server.rs @@ -25,6 +25,7 @@ use crate::s3_cors::*; use crate::s3_delete::*; use crate::s3_get::*; use crate::s3_list::*; +use crate::s3_post_object::handle_post_object; use crate::s3_put::*; use crate::s3_router::{Authorization, Endpoint}; use crate::s3_website::*; @@ -111,9 +112,7 @@ async fn handler_inner(garage: Arc, req: Request) -> Result, + req: Request, + bucket: String, +) -> Result, Error> { + let boundary = req + .headers() + .get(header::CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .and_then(|ct| multer::parse_boundary(ct).ok()) + .ok_or_else(|| Error::BadRequest("Counld not get multipart boundary".to_owned()))?; + + // these limits are rather arbitrary + let constraints = Constraints::new().size_limit( + SizeLimit::new() + .per_field(32 * 1024) + .for_field("file", 5 * 1024 * 1024 * 1024), + ); + + let mut multipart = Multipart::with_constraints(req.into_body(), boundary, constraints); + + let mut headers = HashMap::new(); + let mut key_id = None; + let mut key = None; + let mut policy = None; + let mut redirect = Err(204); + while let Some(mut field) = multipart.next_field().await.unwrap() { + let name = if let Some(name) = field.name() { + name.to_owned() + } else { + continue; + }; + + if name != "file" { + let content = field.text().await.unwrap(); + match name.as_str() { + // main fields + "AWSAccessKeyId" => { + key_id = Some(content); + } + "key" => { + key = Some(content); + } + "policy" => { + policy = Some(content); + } + // special handling + "success_action_redirect" | "redirect" => { + // TODO should verify it's a valid looking URI + redirect = Ok(content); + } + "success_action_status" => { + let code = name.parse::().unwrap_or(204); + redirect = Err(code); + } + "tagging" => { + // TODO Garage does not support tagging so this can be left empty. It's essentially + // a header except it must be parsed from xml to x-www-form-urlencoded + continue; + } + // headers to PutObject + "acl" | "Cache-Control" | "Content-Type" | "Content-Encoding" | "Expires" => { + headers.insert(name, content); + } + _ if name.starts_with("x-amz-") => { + headers.insert(name, content); + } + _ => { + // TODO should we ignore or error? + } + } + continue; + } + + let _file_name = field.file_name(); + let _content_type = field.content_type(); + while let Some(_chunk) = field.chunk().await.unwrap() {} + + let resp = match redirect { + Err(200) => Response::builder() + .status(StatusCode::OK) + .body(Body::empty())?, + Err(201) => { + // body should be an XML document, not sure which yet + Response::builder() + .status(StatusCode::CREATED) + .body(todo!())? + } + // invalid codes are handled as 204 + Err(_) => Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty())?, + Ok(uri) => { + // TODO maybe body should contain a link to the ressource? + Response::builder() + .status(StatusCode::SEE_OTHER) + .header(header::LOCATION, uri) + .body(Body::empty())? + } + }; + + return Ok(resp); + } + + return Err(Error::BadRequest( + "Request did not contain a file".to_owned(), + )); +}