tangled
alpha
login
or
join now
keii.dev
/
wisp
3
fork
atom
🧚 A practical web framework for Gleam
3
fork
atom
overview
issues
pulls
pipelines
Begin parsing multipart
Louis Pilfold
2 years ago
d8e722e2
b88fef4d
+205
-18
1 changed file
expand all
collapse all
unified
split
src
wisp.gleam
+205
-18
src/wisp.gleam
···
3
3
// - [ ] Form data
4
4
// - [ ] Multipart
5
5
// - [ ] Json
6
6
-
// - [ ] String
7
7
-
// - [ ] Bit string
6
6
+
// - [x] String
7
7
+
// - [x] Bit string
8
8
// - [ ] Body writing
9
9
// - [x] Html
10
10
// - [x] Json
···
32
32
import gleam/list
33
33
import gleam/result
34
34
import gleam/string
35
35
+
import gleam/option.{Option}
35
36
import gleam/uri
36
37
import gleam/io
37
38
import gleam/int
···
91
92
let body = case response.body {
92
93
Empty -> mist.Bytes(bit_builder.new())
93
94
Text(text) -> mist.Bytes(bit_builder.from_string_builder(text))
94
94
-
File(path, content_type) -> {
95
95
-
let path = <<path:utf8>>
96
96
-
case mist_file.open(path) {
97
97
-
Error(_) -> {
98
98
-
// TODO: log error
99
99
-
mist.Bytes(bit_builder.new())
100
100
-
}
101
101
-
Ok(descriptor) -> {
102
102
-
mist.File(descriptor, content_type, 0, mist_file.size(path))
103
103
-
}
104
104
-
}
105
105
-
}
95
95
+
File(path, content_type) -> mist_send_file(path, content_type)
106
96
}
107
97
response
108
98
|> response.set_body(body)
109
99
}
110
100
101
101
+
fn mist_send_file(path: String, content_type: String) -> mist.ResponseData {
102
102
+
let path = <<path:utf8>>
103
103
+
case mist_file.open(path) {
104
104
+
Error(_) -> {
105
105
+
// TODO: log error
106
106
+
mist.Bytes(bit_builder.new())
107
107
+
}
108
108
+
Ok(descriptor) -> {
109
109
+
mist.File(descriptor, content_type, 0, mist_file.size(path))
110
110
+
}
111
111
+
}
112
112
+
}
113
113
+
111
114
//
112
115
// Responses
113
116
//
···
123
126
pub type Response =
124
127
HttpResponse(ResponseBody)
125
128
129
129
+
// TODO: document
130
130
+
pub fn response(status: Int) -> Response {
131
131
+
HttpResponse(status, [], Empty)
132
132
+
}
133
133
+
134
134
+
pub fn set_body(response: Response, body: ResponseBody) -> Response {
135
135
+
response
136
136
+
|> response.set_body(body)
137
137
+
}
138
138
+
139
139
+
// TODO: test
126
140
// TODO: document
127
141
pub fn html_response(html: StringBuilder, status: Int) -> Response {
128
142
HttpResponse(status, [#("content-type", "text/html")], Text(html))
···
200
214
)
201
215
}
202
216
217
217
+
type BufferedReader {
218
218
+
BufferedReader(reader: Reader, buffer: BitString)
219
219
+
}
220
220
+
221
221
+
type Quotas {
222
222
+
Quotas(body: Int, files: Int)
223
223
+
}
224
224
+
225
225
+
fn decrement_body_quota(quotas: Quotas, size: Int) -> Result(Quotas, Response) {
226
226
+
let quotas = Quotas(..quotas, body: quotas.body - size)
227
227
+
case quotas.body < 0 {
228
228
+
True -> Error(entity_too_large())
229
229
+
False -> Ok(quotas)
230
230
+
}
231
231
+
}
232
232
+
233
233
+
fn decrement_files_quota(quotas: Quotas, size: Int) -> Result(Quotas, Response) {
234
234
+
let quotas = Quotas(..quotas, files: quotas.files - size)
235
235
+
case quotas.files < 0 {
236
236
+
True -> Error(entity_too_large())
237
237
+
False -> Ok(quotas)
238
238
+
}
239
239
+
}
240
240
+
241
241
+
fn buffered_read(reader: BufferedReader, chunk_size: Int) -> Result(Read, Nil) {
242
242
+
case reader.buffer {
243
243
+
<<>> -> reader.reader(chunk_size)
244
244
+
_ -> Ok(Chunk(reader.buffer, reader.reader))
245
245
+
}
246
246
+
}
247
247
+
203
248
type Reader =
204
249
fn(Int) -> Result(Read, Nil)
205
250
···
257
302
}
258
303
}
259
304
305
305
+
// TODO: re-export once Gleam has a syntax for that
260
306
/// Return the non-empty segments of a request path.
261
307
///
262
308
/// # Examples
···
347
393
}
348
394
}
349
395
350
350
-
// TODO: replace with a function that also supports multipart forms
396
396
+
// TODO: make private and replace with a generic require_form function
351
397
// TODO: test
352
398
// TODO: document
353
399
pub fn require_form_urlencoded_body(
354
400
request: Request,
355
355
-
next: fn(List(#(String, String))) -> Response,
401
401
+
next: fn(FormData) -> Response,
356
402
) -> Response {
357
403
use body <- require_string_body(request)
358
358
-
require(uri.parse_query(body), next)
404
404
+
use pairs <- require(uri.parse_query(body))
405
405
+
let pairs = sort_keys(pairs)
406
406
+
next(FormData(values: pairs, files: []))
407
407
+
}
408
408
+
409
409
+
// TODO: make private and replace with a generic require_form function
410
410
+
// TODO: test
411
411
+
// TODO: document
412
412
+
pub fn require_multipart_body(
413
413
+
request: Request,
414
414
+
boundary: String,
415
415
+
next: fn(FormData) -> Response,
416
416
+
) -> Response {
417
417
+
let quotas =
418
418
+
Quotas(files: request.body.max_files_size, body: request.body.max_body_size)
419
419
+
let chunk_size = request.body.read_chunk_size
420
420
+
let reader = BufferedReader(request.body.reader, <<>>)
421
421
+
422
422
+
let result =
423
423
+
read_multipart(reader, boundary, chunk_size, quotas, FormData([], []))
424
424
+
case result {
425
425
+
Ok(form_data) -> next(form_data)
426
426
+
Error(response) -> response
427
427
+
}
428
428
+
}
429
429
+
430
430
+
fn read_multipart(
431
431
+
reader: BufferedReader,
432
432
+
boundary: String,
433
433
+
chunk_size: Int,
434
434
+
quotas: Quotas,
435
435
+
data: FormData,
436
436
+
) -> Result(FormData, Response) {
437
437
+
let header_parser = fn(chunk) {
438
438
+
http.parse_multipart_headers(chunk, boundary)
439
439
+
|> result.replace_error(bad_request())
440
440
+
}
441
441
+
442
442
+
let result = multipart_headers(reader, header_parser, chunk_size, quotas)
443
443
+
use #(headers, reader, quotas) <- result.try(result)
444
444
+
use #(name, filename) <- result.try(multipart_content_disposition(headers))
445
445
+
446
446
+
use #(data, reader, quotas) <- result.try(case filename {
447
447
+
option.Some(_) -> multipart_body(reader, boundary, chunk_size, quotas, data)
448
448
+
option.None -> multipart_file(reader, boundary, chunk_size, quotas, data)
449
449
+
})
450
450
+
451
451
+
case reader {
452
452
+
option.None -> Ok(data)
453
453
+
option.Some(reader) ->
454
454
+
read_multipart(reader, boundary, chunk_size, quotas, data)
455
455
+
}
456
456
+
}
457
457
+
458
458
+
fn multipart_body(
459
459
+
reader: BufferedReader,
460
460
+
boundary: String,
461
461
+
chunk_size: Int,
462
462
+
quotas: Quotas,
463
463
+
data: FormData,
464
464
+
) -> Result(#(FormData, Option(BufferedReader), Quotas), Response) {
465
465
+
todo
466
466
+
}
467
467
+
468
468
+
fn multipart_file(
469
469
+
reader: BufferedReader,
470
470
+
boundary: String,
471
471
+
chunk_size: Int,
472
472
+
quotas: Quotas,
473
473
+
data: FormData,
474
474
+
) -> Result(#(FormData, Option(BufferedReader), Quotas), Response) {
475
475
+
todo
476
476
+
}
477
477
+
478
478
+
fn multipart_content_disposition(
479
479
+
headers: List(http.Header),
480
480
+
) -> Result(#(String, Option(String)), Response) {
481
481
+
{
482
482
+
use header <- result.try(list.key_find(headers, "content-disposition"))
483
483
+
use header <- result.try(http.parse_content_disposition(header))
484
484
+
use name <- result.map(list.key_find(header.parameters, "name"))
485
485
+
let filename =
486
486
+
option.from_result(list.key_find(header.parameters, "filename"))
487
487
+
#(name, filename)
488
488
+
}
489
489
+
|> result.replace_error(bad_request())
490
490
+
}
491
491
+
492
492
+
fn read_chunk(
493
493
+
reader: BufferedReader,
494
494
+
chunk_size: Int,
495
495
+
) -> Result(#(BitString, Reader), Response) {
496
496
+
buffered_read(reader, chunk_size)
497
497
+
|> result.replace_error(bad_request())
498
498
+
|> result.try(fn(chunk) {
499
499
+
case chunk {
500
500
+
Chunk(chunk, next) -> Ok(#(chunk, next))
501
501
+
ReadingFinished -> Error(bad_request())
502
502
+
}
503
503
+
})
504
504
+
}
505
505
+
506
506
+
fn multipart_headers(
507
507
+
reader: BufferedReader,
508
508
+
parse: fn(BitString) -> Result(http.MultipartHeaders, Response),
509
509
+
chunk_size: Int,
510
510
+
quotas: Quotas,
511
511
+
) -> Result(#(List(http.Header), BufferedReader, Quotas), Response) {
512
512
+
use #(chunk, reader) <- result.try(read_chunk(reader, chunk_size))
513
513
+
use headers <- result.try(parse(chunk))
514
514
+
515
515
+
case headers {
516
516
+
http.MultipartHeaders(headers, remaining) -> {
517
517
+
let used = bit_string.byte_size(chunk) - bit_string.byte_size(remaining)
518
518
+
use quotas <- result.map(decrement_body_quota(quotas, used))
519
519
+
let reader = BufferedReader(reader, remaining)
520
520
+
#(headers, reader, quotas)
521
521
+
}
522
522
+
http.MoreRequiredForHeaders(parse) -> {
523
523
+
let parse = fn(chunk) {
524
524
+
parse(chunk)
525
525
+
|> result.replace_error(bad_request())
526
526
+
}
527
527
+
let reader = BufferedReader(reader, <<>>)
528
528
+
multipart_headers(reader, parse, chunk_size, quotas)
529
529
+
}
530
530
+
}
531
531
+
}
532
532
+
533
533
+
fn sort_keys(pairs: List(#(String, t))) -> List(#(String, t)) {
534
534
+
list.sort(pairs, fn(a, b) { string.compare(a.0, b.0) })
359
535
}
360
536
361
537
// TODO: test
···
368
544
Ok(value) -> next(value)
369
545
Error(_) -> bad_request()
370
546
}
547
547
+
}
548
548
+
549
549
+
pub type FormData {
550
550
+
FormData(
551
551
+
values: List(#(String, String)),
552
552
+
files: List(#(String, UploadedFile)),
553
553
+
)
554
554
+
}
555
555
+
556
556
+
pub type UploadedFile {
557
557
+
UploadedFile(filename: String, path: String, size: Int)
371
558
}
372
559
373
560
//
···
521
708
"jar" -> "application/java-archive"
522
709
"jpeg" -> "image/jpeg"
523
710
"jpg" -> "image/jpeg"
524
524
-
"js" -> "application/javascript"
711
711
+
"js" -> "text/javascript"
525
712
"json" -> "application/json"
526
713
"json-api" -> "application/vnd.api+json"
527
714
"json-patch" -> "application/json-patch+json"