Support for PostObject #222

Merged
lx merged 14 commits from trinity-1686a/garage:post-object into main 2022-02-21 22:02:31 +00:00
4 changed files with 58 additions and 12 deletions
Showing only changes of commit 93637d40ec - Show all commits

1
Cargo.lock generated
View file

@ -699,6 +699,7 @@ dependencies = [
"chrono", "chrono",
"crypto-mac 0.10.1", "crypto-mac 0.10.1",
"err-derive 0.3.0", "err-derive 0.3.0",
"form_urlencoded",
"futures", "futures",
"futures-util", "futures-util",
"garage_model 0.6.0", "garage_model 0.6.0",

View file

@ -36,6 +36,7 @@ futures-util = "0.3"
pin-project = "1.0" pin-project = "1.0"
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
form_urlencoded = "1.0.0"
http = "0.2" http = "0.2"
httpdate = "0.3" httpdate = "0.3"
http-range = "0.1" http-range = "0.1"

View file

@ -17,6 +17,7 @@ use garage_model::garage::Garage;
use crate::api_server::resolve_bucket; use crate::api_server::resolve_bucket;
use crate::error::*; use crate::error::*;
use crate::s3_put::{get_headers, save_stream}; use crate::s3_put::{get_headers, save_stream};
use crate::s3_xml;
use crate::signature::payload::{parse_date, verify_v4}; use crate::signature::payload::{parse_date, verify_v4};
pub async fn handle_post_object( pub async fn handle_post_object(
@ -39,7 +40,7 @@ pub async fn handle_post_object(
.for_field("file", 5 * 1024 * 1024 * 1024), .for_field("file", 5 * 1024 * 1024 * 1024),
); );
let (_head, body) = req.into_parts(); let (head, body) = req.into_parts();
let mut multipart = Multipart::with_constraints(body, boundary, constraints); let mut multipart = Multipart::with_constraints(body, boundary, constraints);
let mut params = HeaderMap::new(); let mut params = HeaderMap::new();
trinity-1686a marked this conversation as resolved Outdated
Outdated
Review

I think we can avoid putting almost the entire code of this function in the while loop (and remove 1 indentation level almost everywhere) by doing something like this:

let file = loop {
    let field = match multipart.next_field().await? {
        None => return Err(no file field found),
        Some(x) => x,
    };
    let name = ...;
    if name == "file" {
         break field;
    }
    // here handle header field adding it to the headermap
};
// here do all of the rest of the processing once we have all headers and are now reading the file body

This looks much nicer to me, especially as in the current version we have a for inside the while, which looks a bit like a nested loop but is in fact not at all.

I think we can avoid putting almost the entire code of this function in the `while` loop (and remove 1 indentation level almost everywhere) by doing something like this: ```rust let file = loop { let field = match multipart.next_field().await? { None => return Err(no file field found), Some(x) => x, }; let name = ...; if name == "file" { break field; } // here handle header field adding it to the headermap }; // here do all of the rest of the processing once we have all headers and are now reading the file body ``` This looks much nicer to me, especially as in the current version we have a `for` inside the `while`, which looks a bit like a nested loop but is in fact not at all.
@ -107,9 +108,11 @@ pub async fn handle_post_object(
.to_str()?; .to_str()?;
let key = if key.contains("${filename}") { let key = if key.contains("${filename}") {
let filename = field.file_name(); if let Some(filename) = field.file_name() {
// TODO is this correct? Maybe we should error instead of default? key.replace("${filename}", filename)
key.replace("${filename}", filename.unwrap_or_default()) } else {
key.to_owned()
trinity-1686a marked this conversation as resolved Outdated
Outdated
Review

This looks like it should rather be an error case: if the application builder put ${filename} in the key field but the browser for some reason didn't include the name of the uploaded file, we probably want to reject the request.

This looks like it should rather be an error case: if the application builder put `${filename}` in the key field but the browser for some reason didn't include the name of the uploaded file, we probably want to reject the request.

that was actually how a previous iteration worked. This behavior was added because it's exactly how AWS behave (not that I can say I agree with that behavior)

that was actually how a previous iteration worked. This behavior was added because it's exactly how AWS behave (not that I can say I agree with that behavior)
Outdated
Review

:/

:/
}
} else { } else {
key.to_owned() key.to_owned()
}; };
@ -229,10 +232,6 @@ pub async fn handle_post_object(
.await?; .await?;
let etag = format!("\"{}\"", md5); let etag = format!("\"{}\"", md5);
// TODO get uri
// get Host
// append www-form-urlencoded key
let location = "todo";
let resp = if let Some(mut target) = params let resp = if let Some(mut target) = params
.get("success_action_redirect") .get("success_action_redirect")
@ -252,18 +251,49 @@ pub async fn handle_post_object(
.header(header::ETAG, etag) .header(header::ETAG, etag)
.body(target.into())? .body(target.into())?
} else { } else {
let path = head
.uri
.into_parts()
.path_and_query
.map(|paq| paq.path().to_string())
.unwrap_or_else(|| "/".to_string());
let authority = head
.headers
.get(header::HOST)
.and_then(|h| h.to_str().ok())
.unwrap_or_default();
let proto = if !authority.is_empty() {
"https://"
} else {
""
};
let url_key: String = form_urlencoded::byte_serialize(key.as_bytes())
.flat_map(str::chars)
.collect();
let location = format!("{}{}{}{}", proto, authority, path, url_key);
let action = params let action = params
.get("success_action_status") .get("success_action_status")
.and_then(|h| h.to_str().ok()) .and_then(|h| h.to_str().ok())
.unwrap_or("204"); .unwrap_or("204");
let builder = Response::builder() let builder = Response::builder()
.header(header::LOCATION, location) .header(header::LOCATION, location.clone())
.header(header::ETAG, etag); .header(header::ETAG, etag.clone());
match action { match action {
"200" => builder.status(StatusCode::OK).body(Body::empty())?, "200" => builder.status(StatusCode::OK).body(Body::empty())?,
"201" => { "201" => {
// TODO body should be an XML document, not sure which yet let xml = s3_xml::PostObject {
builder.status(StatusCode::CREATED).body(Body::from(""))? xmlns: (),
location: s3_xml::Value(location),
bucket: s3_xml::Value(bucket),
key: s3_xml::Value(key),
etag: s3_xml::Value(etag),
};
let body = s3_xml::to_xml_with_header(&xml)?;
builder
.status(StatusCode::CREATED)
.body(Body::from(body.into_bytes()))?
} }
_ => builder.status(StatusCode::NO_CONTENT).body(Body::empty())?, _ => builder.status(StatusCode::NO_CONTENT).body(Body::empty())?,
} }

View file

@ -289,6 +289,20 @@ pub struct VersioningConfiguration {
pub status: Option<Value>, pub status: Option<Value>,
} }
#[derive(Debug, Serialize, PartialEq)]
pub struct PostObject {
#[serde(serialize_with = "xmlns_tag")]
pub xmlns: (),
#[serde(rename = "Location")]
pub location: Value,
#[serde(rename = "Bucket")]
pub bucket: Value,
#[serde(rename = "Key")]
pub key: Value,
#[serde(rename = "ETag")]
pub etag: Value,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;