🧚 A practical web framework for Gleam

Begin parsing multipart

+205 -18
+205 -18
src/wisp.gleam
··· 3 3 // - [ ] Form data 4 4 // - [ ] Multipart 5 5 // - [ ] Json 6 - // - [ ] String 7 - // - [ ] Bit string 6 + // - [x] String 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 + 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 - File(path, content_type) -> { 95 - let path = <<path:utf8>> 96 - case mist_file.open(path) { 97 - Error(_) -> { 98 - // TODO: log error 99 - mist.Bytes(bit_builder.new()) 100 - } 101 - Ok(descriptor) -> { 102 - mist.File(descriptor, content_type, 0, mist_file.size(path)) 103 - } 104 - } 105 - } 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 + 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 + } 112 + } 113 + 111 114 // 112 115 // Responses 113 116 // ··· 123 126 pub type Response = 124 127 HttpResponse(ResponseBody) 125 128 129 + // TODO: document 130 + pub fn response(status: Int) -> Response { 131 + HttpResponse(status, [], Empty) 132 + } 133 + 134 + pub fn set_body(response: Response, body: ResponseBody) -> Response { 135 + response 136 + |> response.set_body(body) 137 + } 138 + 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 + type BufferedReader { 218 + BufferedReader(reader: Reader, buffer: BitString) 219 + } 220 + 221 + type Quotas { 222 + Quotas(body: Int, files: Int) 223 + } 224 + 225 + fn decrement_body_quota(quotas: Quotas, size: Int) -> Result(Quotas, Response) { 226 + let quotas = Quotas(..quotas, body: quotas.body - size) 227 + case quotas.body < 0 { 228 + True -> Error(entity_too_large()) 229 + False -> Ok(quotas) 230 + } 231 + } 232 + 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) 238 + } 239 + } 240 + 241 + fn buffered_read(reader: BufferedReader, chunk_size: Int) -> Result(Read, Nil) { 242 + case reader.buffer { 243 + <<>> -> reader.reader(chunk_size) 244 + _ -> Ok(Chunk(reader.buffer, reader.reader)) 245 + } 246 + } 247 + 203 248 type Reader = 204 249 fn(Int) -> Result(Read, Nil) 205 250 ··· 257 302 } 258 303 } 259 304 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 - // TODO: replace with a function that also supports multipart forms 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 - next: fn(List(#(String, String))) -> Response, 401 + next: fn(FormData) -> Response, 356 402 ) -> Response { 357 403 use body <- require_string_body(request) 358 - require(uri.parse_query(body), next) 404 + use pairs <- require(uri.parse_query(body)) 405 + let pairs = sort_keys(pairs) 406 + next(FormData(values: pairs, files: [])) 407 + } 408 + 409 + // TODO: make private and replace with a generic require_form function 410 + // TODO: test 411 + // TODO: document 412 + pub fn require_multipart_body( 413 + request: Request, 414 + boundary: String, 415 + next: fn(FormData) -> Response, 416 + ) -> Response { 417 + let quotas = 418 + Quotas(files: request.body.max_files_size, body: request.body.max_body_size) 419 + let chunk_size = request.body.read_chunk_size 420 + let reader = BufferedReader(request.body.reader, <<>>) 421 + 422 + let result = 423 + read_multipart(reader, boundary, chunk_size, quotas, FormData([], [])) 424 + case result { 425 + Ok(form_data) -> next(form_data) 426 + Error(response) -> response 427 + } 428 + } 429 + 430 + fn read_multipart( 431 + reader: BufferedReader, 432 + boundary: String, 433 + chunk_size: Int, 434 + quotas: Quotas, 435 + data: FormData, 436 + ) -> 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) 443 + use #(headers, reader, quotas) <- result.try(result) 444 + use #(name, filename) <- result.try(multipart_content_disposition(headers)) 445 + 446 + 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) 449 + }) 450 + 451 + case reader { 452 + option.None -> Ok(data) 453 + option.Some(reader) -> 454 + read_multipart(reader, boundary, chunk_size, quotas, data) 455 + } 456 + } 457 + 458 + fn multipart_body( 459 + reader: BufferedReader, 460 + boundary: String, 461 + chunk_size: Int, 462 + quotas: Quotas, 463 + data: FormData, 464 + ) -> Result(#(FormData, Option(BufferedReader), Quotas), Response) { 465 + todo 466 + } 467 + 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 476 + } 477 + 478 + fn multipart_content_disposition( 479 + headers: List(http.Header), 480 + ) -> Result(#(String, Option(String)), Response) { 481 + { 482 + use header <- result.try(list.key_find(headers, "content-disposition")) 483 + use header <- result.try(http.parse_content_disposition(header)) 484 + use name <- result.map(list.key_find(header.parameters, "name")) 485 + let filename = 486 + option.from_result(list.key_find(header.parameters, "filename")) 487 + #(name, filename) 488 + } 489 + |> result.replace_error(bad_request()) 490 + } 491 + 492 + fn read_chunk( 493 + reader: BufferedReader, 494 + chunk_size: Int, 495 + ) -> Result(#(BitString, Reader), Response) { 496 + buffered_read(reader, chunk_size) 497 + |> result.replace_error(bad_request()) 498 + |> result.try(fn(chunk) { 499 + case chunk { 500 + Chunk(chunk, next) -> Ok(#(chunk, next)) 501 + ReadingFinished -> Error(bad_request()) 502 + } 503 + }) 504 + } 505 + 506 + fn multipart_headers( 507 + reader: BufferedReader, 508 + parse: fn(BitString) -> Result(http.MultipartHeaders, Response), 509 + chunk_size: Int, 510 + quotas: Quotas, 511 + ) -> Result(#(List(http.Header), BufferedReader, Quotas), Response) { 512 + use #(chunk, reader) <- result.try(read_chunk(reader, chunk_size)) 513 + use headers <- result.try(parse(chunk)) 514 + 515 + case headers { 516 + http.MultipartHeaders(headers, remaining) -> { 517 + let used = bit_string.byte_size(chunk) - bit_string.byte_size(remaining) 518 + use quotas <- result.map(decrement_body_quota(quotas, used)) 519 + let reader = BufferedReader(reader, remaining) 520 + #(headers, reader, quotas) 521 + } 522 + http.MoreRequiredForHeaders(parse) -> { 523 + let parse = fn(chunk) { 524 + parse(chunk) 525 + |> result.replace_error(bad_request()) 526 + } 527 + let reader = BufferedReader(reader, <<>>) 528 + multipart_headers(reader, parse, chunk_size, quotas) 529 + } 530 + } 531 + } 532 + 533 + fn sort_keys(pairs: List(#(String, t))) -> List(#(String, t)) { 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 + } 548 + 549 + pub type FormData { 550 + FormData( 551 + values: List(#(String, String)), 552 + files: List(#(String, UploadedFile)), 553 + ) 554 + } 555 + 556 + pub type UploadedFile { 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 - "js" -> "application/javascript" 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"