🧚 A practical web framework for Gleam

Working with files

+264 -9
+10 -9
README.md
··· 56 56 include comments and tests. 57 57 58 58 - [Hello, World!](https://github.com/lpil/wisp/tree/main/examples/0-hello-world) 59 - - [Routing](https://github.com/lpil/wisp/tree/main/examples/1-routing) 60 - - [Working with form data](https://github.com/lpil/wisp/tree/main/examples/2-working-with-form-data) 61 - - [Working with JSON](https://github.com/lpil/wisp/tree/main/examples/3-working-with-json) 62 - - [Working with other formats](https://github.com/lpil/wisp/tree/main/examples/4-working-with-other-formats) 63 - - [Using a database](https://github.com/lpil/wisp/tree/main/examples/5-using-a-database) 64 - - [Serving static assets](https://github.com/lpil/wisp/tree/main/examples/6-serving-static-assets) 65 - - [Logging](https://github.com/lpil/wisp/tree/main/examples/7-logging) 66 - - [Working with cookies](https://github.com/lpil/wisp/tree/main/examples/8-working-with-cookies) 67 - - [Configuring default responses](https://github.com/lpil/wisp/tree/main/examples/9-configuring-default-responses) 59 + - [Routing](https://github.com/lpil/wisp/tree/main/examples/01-routing) 60 + - [Working with form data](https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data) 61 + - [Working with JSON](https://github.com/lpil/wisp/tree/main/examples/03-working-with-json) 62 + - [Working with other formats](https://github.com/lpil/wisp/tree/main/examples/04-working-with-other-formats) 63 + - [Using a database](https://github.com/lpil/wisp/tree/main/examples/05-using-a-database) 64 + - [Serving static assets](https://github.com/lpil/wisp/tree/main/examples/06-serving-static-assets) 65 + - [Logging](https://github.com/lpil/wisp/tree/main/examples/07-logging) 66 + - [Working with cookies](https://github.com/lpil/wisp/tree/main/examples/08-working-with-cookies) 67 + - [Configuring default responses](https://github.com/lpil/wisp/tree/main/examples/09-configuring-default-responses) 68 + - [Working with files](https://github.com/lpil/wisp/tree/main/examples/09-working-with-files) 68 69 69 70 API documentation is available on [HexDocs](https://hexdocs.pm/wisp/). 70 71
examples/0-hello-world/README.md examples/00-hello-world/README.md
examples/0-hello-world/gleam.toml examples/00-hello-world/gleam.toml
examples/0-hello-world/src/app.gleam examples/00-hello-world/src/app.gleam
examples/0-hello-world/src/app/router.gleam examples/00-hello-world/src/app/router.gleam
examples/0-hello-world/src/app/web.gleam examples/00-hello-world/src/app/web.gleam
examples/0-hello-world/test/app_test.gleam examples/00-hello-world/test/app_test.gleam
examples/1-routing/README.md examples/01-routing/README.md
examples/1-routing/gleam.toml examples/01-routing/gleam.toml
examples/1-routing/src/app.gleam examples/01-routing/src/app.gleam
examples/1-routing/src/app/router.gleam examples/01-routing/src/app/router.gleam
examples/1-routing/src/app/web.gleam examples/01-routing/src/app/web.gleam
examples/1-routing/test/app_test.gleam examples/01-routing/test/app_test.gleam
+26
examples/10-working-with-files/README.md
··· 1 + # Wisp Example: Working with files 2 + 3 + ```sh 4 + gleam run # Run the server 5 + gleam test # Run the tests 6 + ``` 7 + 8 + This example shows how to accept file uploads and allow users to download files. 9 + 10 + This example is based off of the ["Working with form data" example][formdata], 11 + so read that first. The additions are detailed here and commented in the code. 12 + 13 + [formdata]: https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data 14 + 15 + ### `app/router` module 16 + 17 + The `handle_request` function has been updated to upload and download files. 18 + 19 + ### `app_test` module 20 + 21 + Tests have been added that send requests with form data bodies and check that 22 + the expected response is returned. 23 + 24 + ### Other files 25 + 26 + No changes have been made to the other files.
+14
examples/10-working-with-files/gleam.toml
··· 1 + name = "app" 2 + version = "1.0.0" 3 + description = "A Wisp example" 4 + gleam = ">= 0.32.0" 5 + 6 + [dependencies] 7 + gleam_stdlib = "~> 0.30" 8 + wisp = { path = "../.." } 9 + gleam_erlang = "~> 0.23" 10 + mist = "~> 0.14" 11 + gleam_http = "~> 3.5" 12 + 13 + [dev-dependencies] 14 + gleeunit = "~> 1.0"
+17
examples/10-working-with-files/src/app.gleam
··· 1 + import gleam/erlang/process 2 + import mist 3 + import wisp 4 + import app/router 5 + 6 + pub fn main() { 7 + wisp.configure_logger() 8 + let secret_key_base = wisp.random_string(64) 9 + 10 + let assert Ok(_) = 11 + wisp.mist_handler(router.handle_request, secret_key_base) 12 + |> mist.new 13 + |> mist.port(8000) 14 + |> mist.start_http 15 + 16 + process.sleep_forever() 17 + }
+116
examples/10-working-with-files/src/app/router.gleam
··· 1 + import app/web 2 + import gleam/http.{Get, Post} 3 + import gleam/list 4 + import gleam/result 5 + import gleam/string_builder 6 + import wisp.{type Request, type Response} 7 + 8 + pub fn handle_request(req: Request) -> Response { 9 + use req <- web.middleware(req) 10 + 11 + case wisp.path_segments(req) { 12 + [] -> show_home(req) 13 + ["file-from-disc"] -> handle_download_file_from_disc(req) 14 + ["file-from-memory"] -> handle_download_file_from_memory(req) 15 + ["upload-file"] -> handle_file_upload(req) 16 + _ -> wisp.not_found() 17 + } 18 + } 19 + 20 + // Notice how `enctype="multipart/form-data"` is used in the file-upload form. 21 + // This ensure that the file is encoded appropriately for the server to read. 22 + const html = " 23 + <p><a href='/file-from-memory'>Download file from memory</a></p> 24 + <p><a href='/file-from-disc'>Download file from disc</a></p> 25 + 26 + <form method=post action='/upload-file' enctype='multipart/form-data'> 27 + <label>Your file: 28 + <input type='file' name='uploaded-file'> 29 + </label> 30 + <input type='submit' value='Submit'> 31 + </form> 32 + " 33 + 34 + fn show_home(req: Request) -> Response { 35 + use <- wisp.require_method(req, Get) 36 + html 37 + |> string_builder.from_string 38 + |> wisp.html_response(200) 39 + } 40 + 41 + fn handle_download_file_from_memory(req: Request) -> Response { 42 + use <- wisp.require_method(req, Get) 43 + 44 + // In this case we have the file contents in memory as a string. 45 + let file_contents = string_builder.from_string("Hello, Joe!") 46 + 47 + wisp.ok() 48 + |> wisp.set_header("content-type", "text/plain") 49 + // The content-disposition header is used to ensure this is treated as a file 50 + // download. If the file was uploaded by the user then you want to ensure that 51 + // this header is ste as otherwise the browser may try to display the file, 52 + // which could enable in cross-site scripting attacks. 53 + |> wisp.set_header("content-disposition", "attachment; filename=hello.txt") 54 + // Use the file contents as the response body. 55 + |> wisp.set_body(wisp.Text(file_contents)) 56 + } 57 + 58 + fn handle_download_file_from_disc(req: Request) -> Response { 59 + use <- wisp.require_method(req, Get) 60 + 61 + // In this case the file exists on the disc. 62 + // Here's we're using the project README, but in a real application you'd 63 + // probably have an absolute path to wherever it is you keep your files. 64 + let file_path = "./README.md" 65 + 66 + wisp.ok() 67 + |> wisp.set_header("content-type", "text/markdown") 68 + // Setting content-disposition header for security again. 69 + |> wisp.set_header("content-disposition", "attachment; filename=hello.txt") 70 + // Use the file contents as the response body. 71 + |> wisp.set_body(wisp.File(file_path)) 72 + } 73 + 74 + fn handle_file_upload(req: Request) -> Response { 75 + use <- wisp.require_method(req, Post) 76 + use formdata <- wisp.require_form(req) 77 + 78 + // The list and result module are used here to extract the values from the 79 + // form data. 80 + // Alternatively you could also pattern match on the list of values (they are 81 + // sorted into alphabetical order), or use a HTML form library. 82 + let result = { 83 + // Note the name of the input is used to find the value. 84 + use file <- result.try(list.key_find(formdata.files, "uploaded-file")) 85 + 86 + // The file has been streamed to a temporary file on the disc, so there's no 87 + // risk of large files causing memory issues. 88 + // The `.path` field contains the path to this file, which you may choose to 89 + // move or read using a library like `simplifile`. When the request is done the 90 + // temporary file is deleted. 91 + wisp.log_info("File uploaded to " <> file.path) 92 + 93 + // File uploads may include a file name. Some clients such as curl may not 94 + // have one, so this field may be empty. 95 + // You should never trust this field. Just because it has a particular file 96 + // extension does not mean it is a file of that type, and it may contain 97 + // invalid characters. Always validate the file type and do not use this 98 + // name as the new path for the file. 99 + wisp.log_info("The file name is reportedly " <> file.file_name) 100 + 101 + Ok(file.file_name) 102 + } 103 + 104 + // An appropriate response is returned depending on whether the form data 105 + // could be successfully handled or not. 106 + case result { 107 + Ok(name) -> { 108 + { "<p>Thank you for your file!" <> name <> "</p>" <> html } 109 + |> string_builder.from_string 110 + |> wisp.html_response(200) 111 + } 112 + Error(_) -> { 113 + wisp.bad_request() 114 + } 115 + } 116 + }
+12
examples/10-working-with-files/src/app/web.gleam
··· 1 + import wisp 2 + 3 + pub fn middleware( 4 + req: wisp.Request, 5 + handle_request: fn(wisp.Request) -> wisp.Response, 6 + ) -> wisp.Response { 7 + let req = wisp.method_override(req) 8 + use <- wisp.log_request(req) 9 + use <- wisp.rescue_crashes 10 + use req <- wisp.handle_head(req) 11 + handle_request(req) 12 + }
+69
examples/10-working-with-files/test/app_test.gleam
··· 1 + import gleeunit 2 + import gleeunit/should 3 + import gleam/string 4 + import wisp/testing 5 + import app/router 6 + 7 + pub fn main() { 8 + gleeunit.main() 9 + } 10 + 11 + pub fn home_test() { 12 + let response = router.handle_request(testing.get("/", [])) 13 + 14 + response.status 15 + |> should.equal(200) 16 + 17 + response.headers 18 + |> should.equal([#("content-type", "text/html")]) 19 + 20 + response 21 + |> testing.string_body 22 + |> string.contains("<form method") 23 + |> should.equal(True) 24 + } 25 + 26 + pub fn file_from_disc_test() { 27 + let response = router.handle_request(testing.get("/file-from-disc", [])) 28 + 29 + response.status 30 + |> should.equal(200) 31 + 32 + response.headers 33 + |> should.equal([ 34 + #("content-type", "text/markdown"), 35 + #("content-disposition", "attachment; filename=hello.txt"), 36 + ]) 37 + 38 + response 39 + |> testing.string_body 40 + |> string.starts_with("# Wisp Example: ") 41 + |> should.equal(True) 42 + } 43 + 44 + pub fn file_from_memory_test() { 45 + let response = router.handle_request(testing.get("/file-from-memory", [])) 46 + 47 + response.status 48 + |> should.equal(200) 49 + 50 + response.headers 51 + |> should.equal([ 52 + #("content-type", "text/plain"), 53 + #("content-disposition", "attachment; filename=hello.txt"), 54 + ]) 55 + 56 + response 57 + |> testing.string_body 58 + |> should.equal("Hello, Joe!") 59 + } 60 + 61 + pub fn upload_file_test() { 62 + // Oh no! What's this? There's no test here! 63 + // 64 + // The helper for constructing a multipart form request in tests has not yet 65 + // been implemented. If this is something you need for your project, please 66 + // let us know and we'll bump it up the list of priorities. 67 + // 68 + Nil 69 + }
examples/2-working-with-form-data/README.md examples/02-working-with-form-data/README.md
examples/2-working-with-form-data/gleam.toml examples/02-working-with-form-data/gleam.toml
examples/2-working-with-form-data/src/app.gleam examples/02-working-with-form-data/src/app.gleam
examples/2-working-with-form-data/src/app/router.gleam examples/02-working-with-form-data/src/app/router.gleam
examples/2-working-with-form-data/src/app/web.gleam examples/02-working-with-form-data/src/app/web.gleam
examples/2-working-with-form-data/test/app_test.gleam examples/02-working-with-form-data/test/app_test.gleam
examples/3-working-with-json/README.md examples/03-working-with-json/README.md
examples/3-working-with-json/gleam.toml examples/03-working-with-json/gleam.toml
examples/3-working-with-json/src/app.gleam examples/03-working-with-json/src/app.gleam
examples/3-working-with-json/src/app/router.gleam examples/03-working-with-json/src/app/router.gleam
examples/3-working-with-json/src/app/web.gleam examples/03-working-with-json/src/app/web.gleam
examples/3-working-with-json/test/app_test.gleam examples/03-working-with-json/test/app_test.gleam
examples/4-working-with-other-formats/README.md examples/04-working-with-other-formats/README.md
examples/4-working-with-other-formats/gleam.toml examples/04-working-with-other-formats/gleam.toml
examples/4-working-with-other-formats/src/app.gleam examples/04-working-with-other-formats/src/app.gleam
examples/4-working-with-other-formats/src/app/router.gleam examples/04-working-with-other-formats/src/app/router.gleam
examples/4-working-with-other-formats/src/app/web.gleam examples/04-working-with-other-formats/src/app/web.gleam
examples/4-working-with-other-formats/test/app_test.gleam examples/04-working-with-other-formats/test/app_test.gleam
examples/5-using-a-database/README.md examples/05-using-a-database/README.md
examples/5-using-a-database/gleam.toml examples/05-using-a-database/gleam.toml
examples/5-using-a-database/src/app.gleam examples/05-using-a-database/src/app.gleam
examples/5-using-a-database/src/app/router.gleam examples/05-using-a-database/src/app/router.gleam
examples/5-using-a-database/src/app/web.gleam examples/05-using-a-database/src/app/web.gleam
examples/5-using-a-database/src/app/web/people.gleam examples/05-using-a-database/src/app/web/people.gleam
examples/5-using-a-database/test/app_test.gleam examples/05-using-a-database/test/app_test.gleam
examples/6-serving-static-assets/README.md examples/06-serving-static-assets/README.md
examples/6-serving-static-assets/gleam.toml examples/06-serving-static-assets/gleam.toml
examples/6-serving-static-assets/priv/static/main.js examples/06-serving-static-assets/priv/static/main.js
examples/6-serving-static-assets/priv/static/styles.css examples/06-serving-static-assets/priv/static/styles.css
examples/6-serving-static-assets/src/app.gleam examples/06-serving-static-assets/src/app.gleam
examples/6-serving-static-assets/src/app/router.gleam examples/06-serving-static-assets/src/app/router.gleam
examples/6-serving-static-assets/src/app/web.gleam examples/06-serving-static-assets/src/app/web.gleam
examples/6-serving-static-assets/test/app_test.gleam examples/06-serving-static-assets/test/app_test.gleam
examples/7-logging/README.md examples/07-logging/README.md
examples/7-logging/gleam.toml examples/07-logging/gleam.toml
examples/7-logging/src/app.gleam examples/07-logging/src/app.gleam
examples/7-logging/src/app/router.gleam examples/07-logging/src/app/router.gleam
examples/7-logging/src/app/web.gleam examples/07-logging/src/app/web.gleam
examples/7-logging/test/app_test.gleam examples/07-logging/test/app_test.gleam
examples/8-working-with-cookies/README.md examples/08-working-with-cookies/README.md
examples/8-working-with-cookies/gleam.toml examples/08-working-with-cookies/gleam.toml
examples/8-working-with-cookies/src/app.gleam examples/08-working-with-cookies/src/app.gleam
examples/8-working-with-cookies/src/app/router.gleam examples/08-working-with-cookies/src/app/router.gleam
examples/8-working-with-cookies/src/app/web.gleam examples/08-working-with-cookies/src/app/web.gleam
examples/8-working-with-cookies/test/app_test.gleam examples/08-working-with-cookies/test/app_test.gleam
examples/9-configuring-default-responses/README.md examples/09-configuring-default-responses/README.md
examples/9-configuring-default-responses/gleam.toml examples/09-configuring-default-responses/gleam.toml
examples/9-configuring-default-responses/src/app.gleam examples/09-configuring-default-responses/src/app.gleam
examples/9-configuring-default-responses/src/app/router.gleam examples/09-configuring-default-responses/src/app/router.gleam
examples/9-configuring-default-responses/src/app/web.gleam examples/09-configuring-default-responses/src/app/web.gleam
examples/9-configuring-default-responses/test/app_test.gleam examples/09-configuring-default-responses/test/app_test.gleam