net.http: add post_multipart_form function (#11511)

pull/11518/head
Miccah 2021-09-15 23:34:07 -05:00 committed by GitHub
parent f295469fac
commit ead5e66afd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 73 additions and 2 deletions

View File

@ -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(

View File

@ -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

View File

@ -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'