···5656include comments and tests.
57575858- [Hello, World!](https://github.com/lpil/wisp/tree/main/examples/0-hello-world)
5959-- [Routing](https://github.com/lpil/wisp/tree/main/examples/1-routing)
6060-- [Working with form data](https://github.com/lpil/wisp/tree/main/examples/2-working-with-form-data)
6161-- [Working with JSON](https://github.com/lpil/wisp/tree/main/examples/3-working-with-json)
6262-- [Working with other formats](https://github.com/lpil/wisp/tree/main/examples/4-working-with-other-formats)
6363-- [Using a database](https://github.com/lpil/wisp/tree/main/examples/5-using-a-database)
6464-- [Serving static assets](https://github.com/lpil/wisp/tree/main/examples/6-serving-static-assets)
6565-- [Logging](https://github.com/lpil/wisp/tree/main/examples/7-logging)
6666-- [Working with cookies](https://github.com/lpil/wisp/tree/main/examples/8-working-with-cookies)
6767-- [Configuring default responses](https://github.com/lpil/wisp/tree/main/examples/9-configuring-default-responses)
5959+- [Routing](https://github.com/lpil/wisp/tree/main/examples/01-routing)
6060+- [Working with form data](https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data)
6161+- [Working with JSON](https://github.com/lpil/wisp/tree/main/examples/03-working-with-json)
6262+- [Working with other formats](https://github.com/lpil/wisp/tree/main/examples/04-working-with-other-formats)
6363+- [Using a database](https://github.com/lpil/wisp/tree/main/examples/05-using-a-database)
6464+- [Serving static assets](https://github.com/lpil/wisp/tree/main/examples/06-serving-static-assets)
6565+- [Logging](https://github.com/lpil/wisp/tree/main/examples/07-logging)
6666+- [Working with cookies](https://github.com/lpil/wisp/tree/main/examples/08-working-with-cookies)
6767+- [Configuring default responses](https://github.com/lpil/wisp/tree/main/examples/09-configuring-default-responses)
6868+- [Working with files](https://github.com/lpil/wisp/tree/main/examples/09-working-with-files)
68696970API documentation is available on [HexDocs](https://hexdocs.pm/wisp/).
7071
···11+# Wisp Example: Working with files
22+33+```sh
44+gleam run # Run the server
55+gleam test # Run the tests
66+```
77+88+This example shows how to accept file uploads and allow users to download files.
99+1010+This example is based off of the ["Working with form data" example][formdata],
1111+so read that first. The additions are detailed here and commented in the code.
1212+1313+[formdata]: https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data
1414+1515+### `app/router` module
1616+1717+The `handle_request` function has been updated to upload and download files.
1818+1919+### `app_test` module
2020+2121+Tests have been added that send requests with form data bodies and check that
2222+the expected response is returned.
2323+2424+### Other files
2525+2626+No changes have been made to the other files.
···11+import app/web
22+import gleam/http.{Get, Post}
33+import gleam/list
44+import gleam/result
55+import gleam/string_builder
66+import wisp.{type Request, type Response}
77+88+pub fn handle_request(req: Request) -> Response {
99+ use req <- web.middleware(req)
1010+1111+ case wisp.path_segments(req) {
1212+ [] -> show_home(req)
1313+ ["file-from-disc"] -> handle_download_file_from_disc(req)
1414+ ["file-from-memory"] -> handle_download_file_from_memory(req)
1515+ ["upload-file"] -> handle_file_upload(req)
1616+ _ -> wisp.not_found()
1717+ }
1818+}
1919+2020+// Notice how `enctype="multipart/form-data"` is used in the file-upload form.
2121+// This ensure that the file is encoded appropriately for the server to read.
2222+const html = "
2323+<p><a href='/file-from-memory'>Download file from memory</a></p>
2424+<p><a href='/file-from-disc'>Download file from disc</a></p>
2525+2626+<form method=post action='/upload-file' enctype='multipart/form-data'>
2727+ <label>Your file:
2828+ <input type='file' name='uploaded-file'>
2929+ </label>
3030+ <input type='submit' value='Submit'>
3131+</form>
3232+"
3333+3434+fn show_home(req: Request) -> Response {
3535+ use <- wisp.require_method(req, Get)
3636+ html
3737+ |> string_builder.from_string
3838+ |> wisp.html_response(200)
3939+}
4040+4141+fn handle_download_file_from_memory(req: Request) -> Response {
4242+ use <- wisp.require_method(req, Get)
4343+4444+ // In this case we have the file contents in memory as a string.
4545+ let file_contents = string_builder.from_string("Hello, Joe!")
4646+4747+ wisp.ok()
4848+ |> wisp.set_header("content-type", "text/plain")
4949+ // The content-disposition header is used to ensure this is treated as a file
5050+ // download. If the file was uploaded by the user then you want to ensure that
5151+ // this header is ste as otherwise the browser may try to display the file,
5252+ // which could enable in cross-site scripting attacks.
5353+ |> wisp.set_header("content-disposition", "attachment; filename=hello.txt")
5454+ // Use the file contents as the response body.
5555+ |> wisp.set_body(wisp.Text(file_contents))
5656+}
5757+5858+fn handle_download_file_from_disc(req: Request) -> Response {
5959+ use <- wisp.require_method(req, Get)
6060+6161+ // In this case the file exists on the disc.
6262+ // Here's we're using the project README, but in a real application you'd
6363+ // probably have an absolute path to wherever it is you keep your files.
6464+ let file_path = "./README.md"
6565+6666+ wisp.ok()
6767+ |> wisp.set_header("content-type", "text/markdown")
6868+ // Setting content-disposition header for security again.
6969+ |> wisp.set_header("content-disposition", "attachment; filename=hello.txt")
7070+ // Use the file contents as the response body.
7171+ |> wisp.set_body(wisp.File(file_path))
7272+}
7373+7474+fn handle_file_upload(req: Request) -> Response {
7575+ use <- wisp.require_method(req, Post)
7676+ use formdata <- wisp.require_form(req)
7777+7878+ // The list and result module are used here to extract the values from the
7979+ // form data.
8080+ // Alternatively you could also pattern match on the list of values (they are
8181+ // sorted into alphabetical order), or use a HTML form library.
8282+ let result = {
8383+ // Note the name of the input is used to find the value.
8484+ use file <- result.try(list.key_find(formdata.files, "uploaded-file"))
8585+8686+ // The file has been streamed to a temporary file on the disc, so there's no
8787+ // risk of large files causing memory issues.
8888+ // The `.path` field contains the path to this file, which you may choose to
8989+ // move or read using a library like `simplifile`. When the request is done the
9090+ // temporary file is deleted.
9191+ wisp.log_info("File uploaded to " <> file.path)
9292+9393+ // File uploads may include a file name. Some clients such as curl may not
9494+ // have one, so this field may be empty.
9595+ // You should never trust this field. Just because it has a particular file
9696+ // extension does not mean it is a file of that type, and it may contain
9797+ // invalid characters. Always validate the file type and do not use this
9898+ // name as the new path for the file.
9999+ wisp.log_info("The file name is reportedly " <> file.file_name)
100100+101101+ Ok(file.file_name)
102102+ }
103103+104104+ // An appropriate response is returned depending on whether the form data
105105+ // could be successfully handled or not.
106106+ case result {
107107+ Ok(name) -> {
108108+ { "<p>Thank you for your file!" <> name <> "</p>" <> html }
109109+ |> string_builder.from_string
110110+ |> wisp.html_response(200)
111111+ }
112112+ Error(_) -> {
113113+ wisp.bad_request()
114114+ }
115115+ }
116116+}
+12
examples/10-working-with-files/src/app/web.gleam
···11+import wisp
22+33+pub fn middleware(
44+ req: wisp.Request,
55+ handle_request: fn(wisp.Request) -> wisp.Response,
66+) -> wisp.Response {
77+ let req = wisp.method_override(req)
88+ use <- wisp.log_request(req)
99+ use <- wisp.rescue_crashes
1010+ use req <- wisp.handle_head(req)
1111+ handle_request(req)
1212+}