diff --git a/vlib/net/http/http.v b/vlib/net/http/http.v index 3ad3e514ad..7f7f867e81 100644 --- a/vlib/net/http/http.v +++ b/vlib/net/http/http.v @@ -80,6 +80,22 @@ pub fn post_form(url string, data map[string]string) ?Response { ) } +pub struct PostMultipartFormConfig { + form map[string]string + files map[string][]FileData +} + +// post_multipart_form sends a POST HTTP request to the URL with multipart form data +pub fn post_multipart_form(url string, conf PostMultipartFormConfig) ?Response { + body, boundary := multipart_form_body(conf.form, conf.files) + return fetch( + method: .post + url: url + header: new_header(key: .content_type, value: 'multipart/form-data; boundary="$boundary"') + data: body + ) +} + // put sends a PUT HTTP request to the URL with a string data pub fn put(url string, data string) ?Response { return fetch( diff --git a/vlib/net/http/request.v b/vlib/net/http/request.v index 73bdf38504..8930c6aaba 100644 --- a/vlib/net/http/request.v +++ b/vlib/net/http/request.v @@ -6,6 +6,7 @@ module http import io import net import net.urllib +import rand import strings import time @@ -261,6 +262,43 @@ struct MultiplePathAttributesError { code int } +// multipart_form_body converts form and file data into a multipart/form +// HTTP request body. It is the inverse of parse_multipart_form. Returns +// (body, boundary). +// NB: Form keys should not contain quotes +fn multipart_form_body(form map[string]string, files map[string][]FileData) (string, string) { + alpha_numeric := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + boundary := rand.string_from_set(alpha_numeric, 64) + + mut sb := strings.new_builder(1024) + for name, value in form { + sb.write_string('\r\n--') + sb.write_string(boundary) + sb.write_string('\r\nContent-Disposition: form-data; name="') + sb.write_string(name) + sb.write_string('"\r\n\r\n') + sb.write_string(value) + } + for name, fs in files { + for f in fs { + sb.write_string('\r\n--') + sb.write_string(boundary) + sb.write_string('\r\nContent-Disposition: form-data; name="') + sb.write_string(name) + sb.write_string('"; filename="') + sb.write_string(f.filename) + sb.write_string('"\r\nContent-Type: ') + sb.write_string(f.content_type) + sb.write_string('\r\n\r\n') + sb.write_string(f.data) + } + } + sb.write_string('\r\n--') + sb.write_string(boundary) + sb.write_string('--') + return sb.str(), boundary +} + pub fn parse_multipart_form(body string, boundary string) (map[string]string, map[string][]FileData) { sections := body.split(boundary) fields := sections[1..sections.len - 1] @@ -275,8 +313,7 @@ pub fn parse_multipart_form(body string, boundary string) (map[string]string, ma name := disposition['name'] or { continue } // Parse files // TODO: filename* - if 'filename' in disposition { - filename := disposition['filename'] + if filename := disposition['filename'] { // Parse Content-Type header if lines.len == 1 || !lines[1].to_lower().starts_with('content-type:') { continue diff --git a/vlib/net/http/request_test.v b/vlib/net/http/request_test.v index 3950ad80a3..7e2c282fd6 100644 --- a/vlib/net/http/request_test.v +++ b/vlib/net/http/request_test.v @@ -128,6 +128,24 @@ ${contents[1]} } } +fn test_multipart_form_body() { + files := { + 'foo': [FileData{ + filename: 'bar.v' + content_type: 'application/octet-stream' + data: 'baz' + }] + } + form := { + 'fooz': 'buzz' + } + + body, boundary := multipart_form_body(form, files) + parsed_form, parsed_files := parse_multipart_form(body, boundary) + assert parsed_files == files + assert parsed_form == form +} + fn test_parse_large_body() ? { body := 'A'.repeat(101) // greater than max_bytes req := 'GET / HTTP/1.1\r\nContent-Length: $body.len\r\n\r\n$body'