🧚 A practical web framework for Gleam

Write to temp files

+76 -22
framework/tmp/.keep

This is a binary file and will not be displayed.

+76 -22
src/wisp.gleam
··· 50 50 ) -> fn(HttpRequest(mist.Connection)) -> HttpResponse(mist.ResponseData) { 51 51 fn(request: HttpRequest(_)) { 52 52 let connection = make_connection(mist_body_reader(request)) 53 - request 54 - |> request.set_body(connection) 55 - |> service 56 - |> mist_response 53 + let request = request.set_body(request, connection) 54 + let response = 55 + request 56 + |> service 57 + |> mist_response 58 + 59 + // TODO: use some FFI to ensure this always happens, even if there is a crash 60 + let assert Ok(_) = delete_temporary_files(request) 61 + 62 + response 57 63 } 58 - } 59 - 60 - fn make_connection(body_reader: Reader) -> Connection { 61 - Connection( 62 - reader: body_reader, 63 - max_body_size: 8_000_000, 64 - max_files_size: 32_000_000, 65 - read_chunk_size: 1_000_000, 66 - ) 67 64 } 68 65 69 66 fn mist_body_reader(request: HttpRequest(mist.Connection)) -> Reader { ··· 205 202 max_body_size: Int, 206 203 max_files_size: Int, 207 204 read_chunk_size: Int, 205 + temporary_directory: String, 206 + ) 207 + } 208 + 209 + fn make_connection(body_reader: Reader) -> Connection { 210 + Connection( 211 + reader: body_reader, 212 + max_body_size: 8_000_000, 213 + max_files_size: 32_000_000, 214 + read_chunk_size: 1_000_000, 215 + // TODO: replace with random string in suitable location 216 + temporary_directory: "./tmp/123", 208 217 ) 209 218 } 210 219 ··· 409 418 ) -> Response { 410 419 let quotas = 411 420 Quotas(files: request.body.max_files_size, body: request.body.max_body_size) 412 - let chunk_size = request.body.read_chunk_size 413 421 let reader = BufferedReader(request.body.reader, <<>>) 414 422 415 423 let result = 416 - read_multipart(reader, boundary, chunk_size, quotas, FormData([], [])) 424 + read_multipart(request, reader, boundary, quotas, FormData([], [])) 417 425 case result { 418 426 Ok(form_data) -> next(form_data) 419 427 Error(response) -> response ··· 421 429 } 422 430 423 431 fn read_multipart( 432 + request: Request, 424 433 reader: BufferedReader, 425 434 boundary: String, 426 - read_size: Int, 427 435 quotas: Quotas, 428 436 data: FormData, 429 437 ) -> Result(FormData, Response) { 438 + let read_size = request.body.read_chunk_size 439 + 440 + // First we read the headers of the multipart part. 430 441 let header_parser = 431 442 fn_with_bad_request_error(http.parse_multipart_headers(_, boundary)) 432 443 let result = multipart_headers(reader, header_parser, read_size, quotas) 433 444 use #(headers, reader, quotas) <- result.try(result) 434 445 use #(name, filename) <- result.try(multipart_content_disposition(headers)) 435 446 447 + // Then we read the body of the part. 436 448 let parse = fn_with_bad_request_error(http.parse_multipart_body(_, boundary)) 437 449 use #(data, reader, quotas) <- result.try(case filename { 450 + // There is a file name, so we treat this as a file upload, streaming the 451 + // contents to a temporary file and using the dedicated files size quota. 438 452 option.Some(file_name) -> { 439 - let path = todo as "need to create a temp file" 453 + use path <- result.try(or_500(new_temporary_file(request))) 440 454 let append = multipart_file_append 441 455 let q = quotas.files 442 456 let result = ··· 447 461 let data = FormData(..data, files: [#(name, file), ..data.files]) 448 462 #(data, reader, quotas) 449 463 } 464 + 465 + // No file name, this is a regular form value that we hold in memory. 450 466 option.None -> { 451 467 let append = fn(data, chunk) { Ok(bit_string.append(data, chunk)) } 452 468 let q = quotas.body ··· 461 477 }) 462 478 463 479 case reader { 480 + // There's at least one more part, read it. 481 + option.Some(reader) -> 482 + read_multipart(request, reader, boundary, quotas, data) 483 + // There are no more parts, we're done. 464 484 option.None -> Ok(FormData(sort_keys(data.values), sort_keys(data.files))) 465 - option.Some(reader) -> 466 - read_multipart(reader, boundary, read_size, quotas, data) 467 485 } 468 486 } 469 487 ··· 476 494 path: String, 477 495 chunk: BitString, 478 496 ) -> Result(String, Response) { 479 - case simplifile.append_bits(chunk, path) { 480 - Ok(_) -> Ok(path) 481 - Error(_) -> { 497 + chunk 498 + |> simplifile.append_bits(path) 499 + |> or_500 500 + |> result.replace(path) 501 + } 502 + 503 + fn or_500(result: Result(a, b)) -> Result(a, Response) { 504 + case result { 505 + Ok(value) -> Ok(value) 506 + Error(error) -> { 482 507 // TODO: log error 508 + io.debug(error) 483 509 Error(internal_server_error()) 484 510 } 485 511 } ··· 902 928 } 903 929 _, _ -> service() 904 930 } 931 + } 932 + 933 + // 934 + // File uploads 935 + // 936 + 937 + // TODO: test 938 + // TODO: document 939 + // TODO: document that you need to call `remove_temporary_files` when you're 940 + // done, unless you're using `mist_service` which will do it for you. 941 + pub fn new_temporary_file( 942 + request: Request, 943 + ) -> Result(String, simplifile.FileError) { 944 + let directory = request.body.temporary_directory 945 + use _ <- result.try(simplifile.make_directory(directory)) 946 + // TODO: use a random filename 947 + let path = directory <> "file.tmp" 948 + // TODO: use create_file when simplifile has it 949 + use _ <- result.map(simplifile.write_bits(<<>>, to: path)) 950 + path 951 + } 952 + 953 + // TODO: test 954 + // TODO: document 955 + pub fn delete_temporary_files( 956 + request: Request, 957 + ) -> Result(Nil, simplifile.FileError) { 958 + simplifile.delete_directory(request.body.temporary_directory) 905 959 } 906 960 907 961 @external(erlang, "file", "read_file_info")