···33import gleam/list
44import gleam/result
55import gleam/string_builder
66+import gleam/bytes_builder
67import wisp.{type Request, type Response}
7889pub fn handle_request(req: Request) -> Response {
···4243 use <- wisp.require_method(req, Get)
43444445 // In this case we have the file contents in memory as a string.
4545- let file_contents = string_builder.from_string("Hello, Joe!")
4646+ // This is good if we have just made the file, but if the file already exists
4747+ // on the disc then the approach in the next function is more efficient.
4848+ let file_contents = bytes_builder.from_string("Hello, Joe!")
46494750 wisp.ok()
4851 |> 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))
5252+ // The content-disposition header is set by this function to ensure this is
5353+ // treated as a file download. If the file was uploaded by the user then you
5454+ // want to ensure that this header is ste as otherwise the browser may try to
5555+ // display the file, which could enable in cross-site scripting attacks.
5656+ |> wisp.file_download_from_memory(
5757+ named: "hello.txt",
5858+ containing: file_contents,
5959+ )
5660}
57615862fn handle_download_file_from_disc(req: Request) -> Response {
···65696670 wisp.ok()
6771 |> 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+ |> wisp.file_download(named: "hello.md", from: file_path)
7273}
73747475fn handle_file_upload(req: Request) -> Response {
···11import exception
22-import gleam/bytes_builder
22+import gleam/bytes_builder.{type BytesBuilder}
33import gleam/bit_array
44import gleam/bool
55import gleam/dict.{type Dict}
···9393 let body = case response.body {
9494 Empty -> mist.Bytes(bytes_builder.new())
9595 Text(text) -> mist.Bytes(bytes_builder.from_string_builder(text))
9696+ Bytes(bytes) -> mist.Bytes(bytes)
9697 File(path) -> mist_send_file(path)
9798 }
9899 response
···123124 /// you can use the `string_builder.from_string` function to convert it.
124125 ///
125126 Text(StringBuilder)
127127+ /// A body of binary data.
128128+ ///
129129+ /// The body is represented using a `StringBuilder`. If you have a `String`
130130+ /// you can use the `string_builder.from_string` function to convert it.
131131+ ///
132132+ Bytes(BytesBuilder)
126133 /// A body of the contents of a file.
127134 ///
128135 /// This will be sent efficiently using the `send_file` function of the
···170177pub fn set_body(response: Response, body: Body) -> Response {
171178 response
172179 |> response.set_body(body)
180180+}
181181+182182+/// Send a file from the disc as a file download.
183183+///
184184+/// The operating system `send_file` function is used to efficiently send the
185185+/// file over the network socket without reading the entire file into memory.
186186+///
187187+/// The `content-disposition` header will be set to `attachment;
188188+/// filename="name"` to ensure the file is downloaded by the browser. This is
189189+/// especially good for files that the browser would otherwise attempt to open
190190+/// as this can result in cross-site scripting vulnerabilities.
191191+///
192192+/// If you wish to not set the `content-disposition` header you could use the
193193+/// `set_body` function with the `File` body variant.
194194+///
195195+/// # Examples
196196+///
197197+/// ```gleam
198198+/// response(200)
199199+/// |> file_download(named: "myfile.txt", from: "/tmp/myfile.txt")
200200+/// // -> Response(
201201+/// // 200,
202202+/// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")],
203203+/// // File("/tmp/myfile.txt"),
204204+/// // )
205205+/// ```
206206+///
207207+pub fn file_download(
208208+ response: Response,
209209+ named name: String,
210210+ from path: String,
211211+) -> Response {
212212+ let name = uri.percent_encode(name)
213213+ response
214214+ |> response.set_header(
215215+ "content-disposition",
216216+ "attachment; filename=\"" <> name <> "\"",
217217+ )
218218+ |> response.set_body(File(path))
219219+}
220220+221221+/// Send a file from memory as a file download.
222222+///
223223+/// If your file is already on the disc use `file_download` instead, to avoid
224224+/// having to read the file into memory to send it.
225225+///
226226+/// The `content-disposition` header will be set to `attachment;
227227+/// filename="name"` to ensure the file is downloaded by the browser. This is
228228+/// especially good for files that the browser would otherwise attempt to open
229229+/// as this can result in cross-site scripting vulnerabilities.
230230+///
231231+/// # Examples
232232+///
233233+/// ```gleam
234234+/// response(200)
235235+/// |> file_download_from_memory(named: "myfile.txt", containing: "Hello, Joe!")
236236+/// // -> Response(
237237+/// // 200,
238238+/// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")],
239239+/// // File("/tmp/myfile.txt"),
240240+/// // )
241241+/// ```
242242+///
243243+pub fn file_download_from_memory(
244244+ response: Response,
245245+ named name: String,
246246+ containing data: BytesBuilder,
247247+) -> Response {
248248+ let name = uri.percent_encode(name)
249249+ response
250250+ |> response.set_header(
251251+ "content-disposition",
252252+ "attachment; filename=\"" <> name <> "\"",
253253+ )
254254+ |> response.set_body(Bytes(data))
173255}
174256175257/// Create a HTML response.
+7-1
src/wisp/testing.gleam
···99import gleam/string_builder
1010import gleam/uri
1111import simplifile
1212-import wisp.{type Request, type Response, Empty, File, Text}
1212+import wisp.{type Request, type Response, Bytes, Empty, File, Text}
13131414/// The default secret key base used for test requests.
1515/// This should never be used outside of tests.
···228228 case response.body {
229229 Empty -> ""
230230 Text(builder) -> string_builder.to_string(builder)
231231+ Bytes(bytes) -> {
232232+ let data = bytes_builder.to_bit_array(bytes)
233233+ let assert Ok(string) = bit_array.to_string(data)
234234+ string
235235+ }
231236 File(path) -> {
232237 let assert Ok(contents) = simplifile.read(path)
233238 contents
···245250pub fn bit_array_body(response: Response) -> BitArray {
246251 case response.body {
247252 Empty -> <<>>
253253+ Bytes(builder) -> bytes_builder.to_bit_array(builder)
248254 Text(builder) ->
249255 bytes_builder.to_bit_array(bytes_builder.from_string_builder(builder))
250256 File(path) -> {