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
Collect bodies
Louis Pilfold
2 years ago
1d81d55d
d8e722e2
+104
-51
3 changed files
expand all
collapse all
unified
split
gleam.toml
manifest.toml
src
wisp.gleam
+1
-1
gleam.toml
···
11
11
12
12
[dependencies]
13
13
gleam_stdlib = "~> 0.29"
14
14
-
gleam_http = "~> 3.2"
14
14
+
gleam_http = "~> 3.5"
15
15
mist = "~> 0.13"
16
16
simplifile = "~> 0.1"
17
17
+4
-4
manifest.toml
···
3
3
4
4
packages = [
5
5
{ name = "gleam_erlang", version = "0.20.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F216A80C8FDFF774447B494D5E08AE4E9A911E971727B9A78FEBF5F300914584" },
6
6
-
{ name = "gleam_http", version = "3.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "B6EB76D304E0E66267485983E6B7BC28F3BFA6795BB2BF90FC411F6903AF6A1A" },
6
6
+
{ name = "gleam_http", version = "3.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "FAE9AE3EB1CA90C2194615D20FFFD1E28B630E84DACA670B28D959B37BCBB02C" },
7
7
{ name = "gleam_otp", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "E31B158857E3D2AF946FE6E90E0CB21699AF226F4630E93FBEAC5DB4515F8920" },
8
8
{ name = "gleam_stdlib", version = "0.30.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "03710B3DA047A3683117591707FCA19D32B980229DD8CE8B0603EB5B5144F6C3" },
9
9
{ name = "gleeunit", version = "0.10.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "ECEA2DE4BE6528D36AFE74F42A21CDF99966EC36D7F25DEB34D47DD0F7977BAF" },
10
10
-
{ name = "glisten", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang", "gleam_otp"], otp_app = "glisten", source = "hex", outer_checksum = "6DDE276F8A2E3C79E5A580DEA05C7D87FCDE3A37FF69F607770D92686E193531" },
11
11
-
{ name = "mist", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "glisten", "gleam_stdlib", "gleam_http"], otp_app = "mist", source = "hex", outer_checksum = "9A374CA245D682E2C08A5224B4420DDA252EF553AE5FD0ED7BAD33F86ACF7C98" },
10
10
+
{ name = "glisten", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_otp", "gleam_erlang", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "6DDE276F8A2E3C79E5A580DEA05C7D87FCDE3A37FF69F607770D92686E193531" },
11
11
+
{ name = "mist", version = "0.13.1", build_tools = ["gleam"], requirements = ["glisten", "gleam_stdlib", "gleam_erlang", "gleam_http"], otp_app = "mist", source = "hex", outer_checksum = "178EDF5F396570DD53BE2A94C8F9759072093DACB81B62CD47A620B961DB2F2D" },
12
12
{ name = "simplifile", version = "0.1.8", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "9CED66E65AF32C98AA336A65365A498DCF018D2A3D96A05D861C4005DCDE4D2D" },
13
13
]
14
14
15
15
[requirements]
16
16
-
gleam_http = { version = "~> 3.2" }
16
16
+
gleam_http = { version = "~> 3.5" }
17
17
gleam_stdlib = { version = "~> 0.29" }
18
18
gleeunit = { version = "~> 0.10" }
19
19
mist = { version = "~> 0.13" }
+99
-46
src/wisp.gleam
···
38
38
import gleam/int
39
39
import simplifile
40
40
import mist
41
41
-
import mist/file as mist_file
42
41
43
42
//
44
43
// Running the server
···
92
91
let body = case response.body {
93
92
Empty -> mist.Bytes(bit_builder.new())
94
93
Text(text) -> mist.Bytes(bit_builder.from_string_builder(text))
95
95
-
File(path, content_type) -> mist_send_file(path, content_type)
94
94
+
File(path) -> mist_send_file(path)
96
95
}
97
96
response
98
97
|> response.set_body(body)
99
98
}
100
99
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
-
}
100
100
+
fn mist_send_file(path: String) -> mist.ResponseData {
101
101
+
mist.send_file(path, offset: 0, limit: option.None)
102
102
+
|> result.lazy_unwrap(fn() {
103
103
+
// TODO: log error
104
104
+
mist.Bytes(bit_builder.new())
105
105
+
})
112
106
}
113
107
114
108
//
···
118
112
pub type ResponseBody {
119
113
Empty
120
114
// TODO: remove content type
121
121
-
File(path: String, content_type: String)
115
115
+
File(path: String)
122
116
Text(StringBuilder)
123
117
}
124
118
···
230
224
}
231
225
}
232
226
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)
227
227
+
fn decrement_quota(quota: Int, size: Int) -> Result(Int, Response) {
228
228
+
case quota - size {
229
229
+
quota if quota < 0 -> Error(entity_too_large())
230
230
+
quota -> Ok(quota)
238
231
}
239
232
}
240
233
···
430
423
fn read_multipart(
431
424
reader: BufferedReader,
432
425
boundary: String,
433
433
-
chunk_size: Int,
426
426
+
read_size: Int,
434
427
quotas: Quotas,
435
428
data: FormData,
436
429
) -> 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)
430
430
+
let header_parser =
431
431
+
fn_with_bad_request_error(http.parse_multipart_headers(_, boundary))
432
432
+
let result = multipart_headers(reader, header_parser, read_size, quotas)
443
433
use #(headers, reader, quotas) <- result.try(result)
444
434
use #(name, filename) <- result.try(multipart_content_disposition(headers))
445
435
436
436
+
let parse = fn_with_bad_request_error(http.parse_multipart_body(_, boundary))
446
437
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)
438
438
+
option.Some(file_name) -> {
439
439
+
let path = todo as "need to create a temp file"
440
440
+
let append = multipart_file_append
441
441
+
let q = quotas.files
442
442
+
let result =
443
443
+
multipart_body(reader, parse, boundary, read_size, q, append, path)
444
444
+
use #(reader, quota, _) <- result.map(result)
445
445
+
let quotas = Quotas(..quotas, files: quota)
446
446
+
let file = UploadedFile(path: path, file_name: file_name)
447
447
+
let data = FormData(..data, files: [#(name, file), ..data.files])
448
448
+
#(data, reader, quotas)
449
449
+
}
450
450
+
option.None -> {
451
451
+
let append = fn(data, chunk) { Ok(bit_string.append(data, chunk)) }
452
452
+
let q = quotas.body
453
453
+
let result =
454
454
+
multipart_body(reader, parse, boundary, read_size, q, append, <<>>)
455
455
+
use #(reader, quota, value) <- result.try(result)
456
456
+
let quotas = Quotas(..quotas, body: quota)
457
457
+
use value <- result.map(bit_string_to_string(value))
458
458
+
let data = FormData(..data, values: [#(name, value), ..data.values])
459
459
+
#(data, reader, quotas)
460
460
+
}
449
461
})
450
462
451
463
case reader {
452
452
-
option.None -> Ok(data)
464
464
+
option.None -> Ok(FormData(sort_keys(data.values), sort_keys(data.files)))
453
465
option.Some(reader) ->
454
454
-
read_multipart(reader, boundary, chunk_size, quotas, data)
466
466
+
read_multipart(reader, boundary, read_size, quotas, data)
467
467
+
}
468
468
+
}
469
469
+
470
470
+
fn bit_string_to_string(bits: BitString) -> Result(String, Response) {
471
471
+
bit_string.to_string(bits)
472
472
+
|> result.replace_error(bad_request())
473
473
+
}
474
474
+
475
475
+
fn multipart_file_append(
476
476
+
path: String,
477
477
+
chunk: BitString,
478
478
+
) -> Result(String, Response) {
479
479
+
case simplifile.append_bits(chunk, path) {
480
480
+
Ok(_) -> Ok(path)
481
481
+
Error(_) -> {
482
482
+
// TODO: log error
483
483
+
Error(internal_server_error())
484
484
+
}
455
485
}
456
486
}
457
487
458
488
fn multipart_body(
459
489
reader: BufferedReader,
490
490
+
parse: fn(BitString) -> Result(http.MultipartBody, Response),
460
491
boundary: String,
461
492
chunk_size: Int,
462
462
-
quotas: Quotas,
463
463
-
data: FormData,
464
464
-
) -> Result(#(FormData, Option(BufferedReader), Quotas), Response) {
465
465
-
todo
493
493
+
quota: Int,
494
494
+
append: fn(t, BitString) -> Result(t, Response),
495
495
+
data: t,
496
496
+
) -> Result(#(Option(BufferedReader), Int, t), Response) {
497
497
+
use #(chunk, reader) <- result.try(read_chunk(reader, chunk_size))
498
498
+
use output <- result.try(parse(chunk))
499
499
+
500
500
+
case output {
501
501
+
http.MultipartBody(chunk, done, remaining) -> {
502
502
+
let used = bit_string.byte_size(chunk) - bit_string.byte_size(remaining)
503
503
+
use quotas <- result.try(decrement_quota(quota, used))
504
504
+
let reader = BufferedReader(reader, remaining)
505
505
+
let reader = case done {
506
506
+
True -> option.None
507
507
+
False -> option.Some(reader)
508
508
+
}
509
509
+
use value <- result.map(append(data, chunk))
510
510
+
#(reader, quotas, value)
511
511
+
}
512
512
+
513
513
+
http.MoreRequiredForBody(chunk, parse) -> {
514
514
+
let parse = fn_with_bad_request_error(parse(_))
515
515
+
let reader = BufferedReader(reader, <<>>)
516
516
+
use data <- result.try(append(data, chunk))
517
517
+
multipart_body(reader, parse, boundary, chunk_size, quota, append, data)
518
518
+
}
519
519
+
}
466
520
}
467
521
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
522
522
+
fn fn_with_bad_request_error(
523
523
+
f: fn(a) -> Result(b, c),
524
524
+
) -> fn(a) -> Result(b, Response) {
525
525
+
fn(a) {
526
526
+
f(a)
527
527
+
|> result.replace_error(bad_request())
528
528
+
}
476
529
}
477
530
478
531
fn multipart_content_disposition(
···
554
607
}
555
608
556
609
pub type UploadedFile {
557
557
-
UploadedFile(filename: String, path: String, size: Int)
610
610
+
UploadedFile(file_name: String, path: String)
558
611
}
559
612
560
613
//
···
844
897
Ok(_) ->
845
898
response.new(200)
846
899
|> response.set_header("content-type", mime_type)
847
847
-
|> response.set_body(File(path, mime_type))
900
900
+
|> response.set_body(File(path))
848
901
}
849
902
}
850
903
_, _ -> service()
···
873
926
case body {
874
927
Empty -> string_builder.new()
875
928
Text(text) -> text
876
876
-
File(path, _) -> {
929
929
+
File(path) -> {
877
930
let assert Ok(contents) = simplifile.read(path)
878
931
string_builder.from_string(contents)
879
932
}
···
886
939
case body {
887
940
Empty -> bit_builder.new()
888
941
Text(text) -> bit_builder.from_string_builder(text)
889
889
-
File(path, _) -> {
942
942
+
File(path) -> {
890
943
let assert Ok(contents) = simplifile.read_bits(path)
891
944
bit_builder.from_bit_string(contents)
892
945
}