🧚 A practical web framework for Gleam

Collect bodies

+104 -51
+1 -1
gleam.toml
··· 11 11 12 12 [dependencies] 13 13 gleam_stdlib = "~> 0.29" 14 - gleam_http = "~> 3.2" 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 - { name = "gleam_http", version = "3.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "B6EB76D304E0E66267485983E6B7BC28F3BFA6795BB2BF90FC411F6903AF6A1A" }, 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 - { 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 - { 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 + { 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 + { 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 - gleam_http = { version = "~> 3.2" } 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 - 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 - File(path, content_type) -> mist_send_file(path, content_type) 94 + File(path) -> mist_send_file(path) 96 95 } 97 96 response 98 97 |> response.set_body(body) 99 98 } 100 99 101 - fn mist_send_file(path: String, content_type: String) -> mist.ResponseData { 102 - let path = <<path:utf8>> 103 - case mist_file.open(path) { 104 - Error(_) -> { 105 - // TODO: log error 106 - mist.Bytes(bit_builder.new()) 107 - } 108 - Ok(descriptor) -> { 109 - mist.File(descriptor, content_type, 0, mist_file.size(path)) 110 - } 111 - } 100 + fn mist_send_file(path: String) -> mist.ResponseData { 101 + mist.send_file(path, offset: 0, limit: option.None) 102 + |> result.lazy_unwrap(fn() { 103 + // TODO: log error 104 + mist.Bytes(bit_builder.new()) 105 + }) 112 106 } 113 107 114 108 // ··· 118 112 pub type ResponseBody { 119 113 Empty 120 114 // TODO: remove content type 121 - File(path: String, content_type: String) 115 + File(path: String) 122 116 Text(StringBuilder) 123 117 } 124 118 ··· 230 224 } 231 225 } 232 226 233 - fn decrement_files_quota(quotas: Quotas, size: Int) -> Result(Quotas, Response) { 234 - let quotas = Quotas(..quotas, files: quotas.files - size) 235 - case quotas.files < 0 { 236 - True -> Error(entity_too_large()) 237 - False -> Ok(quotas) 227 + fn decrement_quota(quota: Int, size: Int) -> Result(Int, Response) { 228 + case quota - size { 229 + quota if quota < 0 -> Error(entity_too_large()) 230 + quota -> Ok(quota) 238 231 } 239 232 } 240 233 ··· 430 423 fn read_multipart( 431 424 reader: BufferedReader, 432 425 boundary: String, 433 - chunk_size: Int, 426 + read_size: Int, 434 427 quotas: Quotas, 435 428 data: FormData, 436 429 ) -> Result(FormData, Response) { 437 - let header_parser = fn(chunk) { 438 - http.parse_multipart_headers(chunk, boundary) 439 - |> result.replace_error(bad_request()) 440 - } 441 - 442 - let result = multipart_headers(reader, header_parser, chunk_size, quotas) 430 + let header_parser = 431 + fn_with_bad_request_error(http.parse_multipart_headers(_, boundary)) 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 + let parse = fn_with_bad_request_error(http.parse_multipart_body(_, boundary)) 446 437 use #(data, reader, quotas) <- result.try(case filename { 447 - option.Some(_) -> multipart_body(reader, boundary, chunk_size, quotas, data) 448 - option.None -> multipart_file(reader, boundary, chunk_size, quotas, data) 438 + option.Some(file_name) -> { 439 + let path = todo as "need to create a temp file" 440 + let append = multipart_file_append 441 + let q = quotas.files 442 + let result = 443 + multipart_body(reader, parse, boundary, read_size, q, append, path) 444 + use #(reader, quota, _) <- result.map(result) 445 + let quotas = Quotas(..quotas, files: quota) 446 + let file = UploadedFile(path: path, file_name: file_name) 447 + let data = FormData(..data, files: [#(name, file), ..data.files]) 448 + #(data, reader, quotas) 449 + } 450 + option.None -> { 451 + let append = fn(data, chunk) { Ok(bit_string.append(data, chunk)) } 452 + let q = quotas.body 453 + let result = 454 + multipart_body(reader, parse, boundary, read_size, q, append, <<>>) 455 + use #(reader, quota, value) <- result.try(result) 456 + let quotas = Quotas(..quotas, body: quota) 457 + use value <- result.map(bit_string_to_string(value)) 458 + let data = FormData(..data, values: [#(name, value), ..data.values]) 459 + #(data, reader, quotas) 460 + } 449 461 }) 450 462 451 463 case reader { 452 - option.None -> Ok(data) 464 + option.None -> Ok(FormData(sort_keys(data.values), sort_keys(data.files))) 453 465 option.Some(reader) -> 454 - read_multipart(reader, boundary, chunk_size, quotas, data) 466 + read_multipart(reader, boundary, read_size, quotas, data) 467 + } 468 + } 469 + 470 + fn bit_string_to_string(bits: BitString) -> Result(String, Response) { 471 + bit_string.to_string(bits) 472 + |> result.replace_error(bad_request()) 473 + } 474 + 475 + fn multipart_file_append( 476 + path: String, 477 + chunk: BitString, 478 + ) -> Result(String, Response) { 479 + case simplifile.append_bits(chunk, path) { 480 + Ok(_) -> Ok(path) 481 + Error(_) -> { 482 + // TODO: log error 483 + Error(internal_server_error()) 484 + } 455 485 } 456 486 } 457 487 458 488 fn multipart_body( 459 489 reader: BufferedReader, 490 + parse: fn(BitString) -> Result(http.MultipartBody, Response), 460 491 boundary: String, 461 492 chunk_size: Int, 462 - quotas: Quotas, 463 - data: FormData, 464 - ) -> Result(#(FormData, Option(BufferedReader), Quotas), Response) { 465 - todo 493 + quota: Int, 494 + append: fn(t, BitString) -> Result(t, Response), 495 + data: t, 496 + ) -> Result(#(Option(BufferedReader), Int, t), Response) { 497 + use #(chunk, reader) <- result.try(read_chunk(reader, chunk_size)) 498 + use output <- result.try(parse(chunk)) 499 + 500 + case output { 501 + http.MultipartBody(chunk, done, remaining) -> { 502 + let used = bit_string.byte_size(chunk) - bit_string.byte_size(remaining) 503 + use quotas <- result.try(decrement_quota(quota, used)) 504 + let reader = BufferedReader(reader, remaining) 505 + let reader = case done { 506 + True -> option.None 507 + False -> option.Some(reader) 508 + } 509 + use value <- result.map(append(data, chunk)) 510 + #(reader, quotas, value) 511 + } 512 + 513 + http.MoreRequiredForBody(chunk, parse) -> { 514 + let parse = fn_with_bad_request_error(parse(_)) 515 + let reader = BufferedReader(reader, <<>>) 516 + use data <- result.try(append(data, chunk)) 517 + multipart_body(reader, parse, boundary, chunk_size, quota, append, data) 518 + } 519 + } 466 520 } 467 521 468 - fn multipart_file( 469 - reader: BufferedReader, 470 - boundary: String, 471 - chunk_size: Int, 472 - quotas: Quotas, 473 - data: FormData, 474 - ) -> Result(#(FormData, Option(BufferedReader), Quotas), Response) { 475 - todo 522 + fn fn_with_bad_request_error( 523 + f: fn(a) -> Result(b, c), 524 + ) -> fn(a) -> Result(b, Response) { 525 + fn(a) { 526 + f(a) 527 + |> result.replace_error(bad_request()) 528 + } 476 529 } 477 530 478 531 fn multipart_content_disposition( ··· 554 607 } 555 608 556 609 pub type UploadedFile { 557 - UploadedFile(filename: String, path: String, size: Int) 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 - |> response.set_body(File(path, mime_type)) 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 - File(path, _) -> { 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 - File(path, _) -> { 942 + File(path) -> { 890 943 let assert Ok(contents) = simplifile.read_bits(path) 891 944 bit_builder.from_bit_string(contents) 892 945 }