vweb: add multipart/form-data parser and file upload (#8160)

pull/8243/head
Louis Schmieder 2021-01-21 11:08:51 +01:00 committed by GitHub
parent b44ec4921f
commit f7c251f8f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 140 additions and 2 deletions

View File

@ -0,0 +1,6 @@
<h2>File Upload</h2>
<form method="POST" enctype="multipart/form-data" action="/upload">
<input type="file" name="upfile" multiple>
<input type="submit" value="Press">
</form>

View File

@ -0,0 +1,14 @@
<meta charset="utf-8">
File amount: @fdata.len
@for i, data in fdata
<h2>Filename: @data.filename</h2>
<h2>Type: @data.content_type</h2>
<p>@{files[i]}</p>
@end
<a href="/">Back</a>

View File

@ -0,0 +1,33 @@
module main
import vweb
const (
port = 8082
)
struct App {
vweb.Context
}
fn main() {
vweb.run<App>(port)
}
pub fn (mut app App) index() vweb.Result {
return $vweb.html()
}
[post]
['/upload']
pub fn (mut app App) upload() vweb.Result {
fdata := app.files['upfile']
mut files := []vweb.RawHtml{}
for d in fdata {
files << d.data.replace_each(['\n', '<br>', '\n\r', '<br>' '\t', ' ', ' ', '&nbsp;'])
}
return $vweb.html()
}

View File

@ -52,6 +52,7 @@ pub mut:
static_mime_types map[string]string static_mime_types map[string]string
form map[string]string form map[string]string
query map[string]string query map[string]string
files map[string][]FileData
headers string // response headers headers string // response headers
done bool done bool
page_gen_start i64 page_gen_start i64
@ -60,6 +61,13 @@ pub mut:
max_chunk_len int = 20 max_chunk_len int = 20
} }
struct FileData {
pub:
filename string
content_type string
data string
}
// declaring init_once in your App struct is optional // declaring init_once in your App struct is optional
pub fn (ctx Context) init_once() {} pub fn (ctx Context) init_once() {}
@ -316,6 +324,9 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T) {
mut body := '' mut body := ''
mut in_headers := true mut in_headers := true
mut len := 0 mut len := 0
// File receive stuff
mut ct := 'text/plain'
mut boundary := ''
// for line in lines[1..] { // for line in lines[1..] {
for lindex in 0 .. 100 { for lindex in 0 .. 100 {
// println(j) // println(j)
@ -324,6 +335,14 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T) {
break break
} }
sline := strip(line) sline := strip(line)
// Parse content type
if sline.len >= 14 && sline[..14] == 'Content-Type: ' {
args := sline[14..].split('; ')
ct = args[0]
if args.len > 1 {
boundary = args[1][9..]
}
}
if sline == '' { if sline == '' {
// if in_headers { // if in_headers {
// End of headers, no body => exit // End of headers, no body => exit
@ -340,8 +359,13 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T) {
body += read_body.bytestr() body += read_body.bytestr()
break break
} }
if ct == 'multipart/form-data' && sline == boundary {
body += boundary
read_body := io.read_all(reader: reader) or { []byte{} }
body += read_body.bytestr()
break
}
if in_headers { if in_headers {
// println(sline)
headers << sline headers << sline
if sline.starts_with('Content-Length') { if sline.starts_with('Content-Length') {
len = sline.all_after(': ').int() len = sline.all_after(': ').int()
@ -374,7 +398,11 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T) {
} }
// } // }
if req.method in methods_with_form { if req.method in methods_with_form {
app.parse_form(req.data) if ct == 'multipart/form-data' {
app.parse_multipart_form(body, boundary)
} else {
app.parse_form(req.data)
}
} }
if vals.len < 2 { if vals.len < 2 {
$if debug { $if debug {
@ -603,6 +631,63 @@ pub fn (mut ctx Context) parse_form(s string) {
// ... // ...
} }
[manualfree]
pub fn (mut ctx Context) parse_multipart_form(s string, b string) {
if ctx.req.method !in methods_with_form {
return
}
a := s.split('$b')[1..]
fields := a[..a.len - 1]
for field in fields {
lines := field.split_into_lines()[1..]
mut l := 0
// Parse name
disposition_data := lines[l].split('; ')[1..]
l++
name := disposition_data[0][6..disposition_data[0].len - 1]
// Parse files
if disposition_data.len > 1 {
filename := disposition_data[1][10..disposition_data[1].len - 1]
ct := lines[l].split(': ')[1]
l++
if name !in ctx.files {
ctx.files[name] = []FileData{}
}
mut sb := strings.new_builder(field.len)
for i in l + 1 .. lines.len - 1 {
sb.writeln(lines[i])
}
ctx.files[name] << FileData{
filename: filename
content_type: ct
data: sb.str()
}
unsafe {
filename.free()
ct.free()
sb.free()
}
continue
}
mut sb := strings.new_builder(field.len)
for i in l + 1 .. lines.len - 1 {
sb.writeln(lines[i])
}
ctx.form[name] = sb.str()
unsafe {
disposition_data.free()
name.free()
sb.free()
}
}
unsafe {
fields.free()
s.free()
b.free()
a.free()
}
}
fn (mut ctx Context) scan_static_directory(directory_path string, mount_path string) { fn (mut ctx Context) scan_static_directory(directory_path string, mount_path string) {
files := os.ls(directory_path) or { panic(err) } files := os.ls(directory_path) or { panic(err) }
if files.len > 0 { if files.len > 0 {