🧚 A practical web framework for Gleam

Download functions

+105 -16
+13 -12
examples/10-working-with-files/src/app/router.gleam
··· 3 3 import gleam/list 4 4 import gleam/result 5 5 import gleam/string_builder 6 + import gleam/bytes_builder 6 7 import wisp.{type Request, type Response} 7 8 8 9 pub fn handle_request(req: Request) -> Response { ··· 42 43 use <- wisp.require_method(req, Get) 43 44 44 45 // In this case we have the file contents in memory as a string. 45 - let file_contents = string_builder.from_string("Hello, Joe!") 46 + // This is good if we have just made the file, but if the file already exists 47 + // on the disc then the approach in the next function is more efficient. 48 + let file_contents = bytes_builder.from_string("Hello, Joe!") 46 49 47 50 wisp.ok() 48 51 |> 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)) 52 + // The content-disposition header is set by this function to ensure this is 53 + // treated as a file download. If the file was uploaded by the user then you 54 + // want to ensure that this header is ste as otherwise the browser may try to 55 + // display the file, which could enable in cross-site scripting attacks. 56 + |> wisp.file_download_from_memory( 57 + named: "hello.txt", 58 + containing: file_contents, 59 + ) 56 60 } 57 61 58 62 fn handle_download_file_from_disc(req: Request) -> Response { ··· 65 69 66 70 wisp.ok() 67 71 |> 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 + |> wisp.file_download(named: "hello.md", from: file_path) 72 73 } 73 74 74 75 fn handle_file_upload(req: Request) -> Response {
+2 -2
examples/10-working-with-files/test/app_test.gleam
··· 32 32 response.headers 33 33 |> should.equal([ 34 34 #("content-type", "text/markdown"), 35 - #("content-disposition", "attachment; filename=hello.txt"), 35 + #("content-disposition", "attachment; filename=\"hello.md\""), 36 36 ]) 37 37 38 38 response ··· 50 50 response.headers 51 51 |> should.equal([ 52 52 #("content-type", "text/plain"), 53 - #("content-disposition", "attachment; filename=hello.txt"), 53 + #("content-disposition", "attachment; filename=\"hello.txt\""), 54 54 ]) 55 55 56 56 response
+83 -1
src/wisp.gleam
··· 1 1 import exception 2 - import gleam/bytes_builder 2 + import gleam/bytes_builder.{type BytesBuilder} 3 3 import gleam/bit_array 4 4 import gleam/bool 5 5 import gleam/dict.{type Dict} ··· 93 93 let body = case response.body { 94 94 Empty -> mist.Bytes(bytes_builder.new()) 95 95 Text(text) -> mist.Bytes(bytes_builder.from_string_builder(text)) 96 + Bytes(bytes) -> mist.Bytes(bytes) 96 97 File(path) -> mist_send_file(path) 97 98 } 98 99 response ··· 123 124 /// you can use the `string_builder.from_string` function to convert it. 124 125 /// 125 126 Text(StringBuilder) 127 + /// A body of binary data. 128 + /// 129 + /// The body is represented using a `StringBuilder`. If you have a `String` 130 + /// you can use the `string_builder.from_string` function to convert it. 131 + /// 132 + Bytes(BytesBuilder) 126 133 /// A body of the contents of a file. 127 134 /// 128 135 /// This will be sent efficiently using the `send_file` function of the ··· 170 177 pub fn set_body(response: Response, body: Body) -> Response { 171 178 response 172 179 |> response.set_body(body) 180 + } 181 + 182 + /// Send a file from the disc as a file download. 183 + /// 184 + /// The operating system `send_file` function is used to efficiently send the 185 + /// file over the network socket without reading the entire file into memory. 186 + /// 187 + /// The `content-disposition` header will be set to `attachment; 188 + /// filename="name"` to ensure the file is downloaded by the browser. This is 189 + /// especially good for files that the browser would otherwise attempt to open 190 + /// as this can result in cross-site scripting vulnerabilities. 191 + /// 192 + /// If you wish to not set the `content-disposition` header you could use the 193 + /// `set_body` function with the `File` body variant. 194 + /// 195 + /// # Examples 196 + /// 197 + /// ```gleam 198 + /// response(200) 199 + /// |> file_download(named: "myfile.txt", from: "/tmp/myfile.txt") 200 + /// // -> Response( 201 + /// // 200, 202 + /// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")], 203 + /// // File("/tmp/myfile.txt"), 204 + /// // ) 205 + /// ``` 206 + /// 207 + pub fn file_download( 208 + response: Response, 209 + named name: String, 210 + from path: String, 211 + ) -> Response { 212 + let name = uri.percent_encode(name) 213 + response 214 + |> response.set_header( 215 + "content-disposition", 216 + "attachment; filename=\"" <> name <> "\"", 217 + ) 218 + |> response.set_body(File(path)) 219 + } 220 + 221 + /// Send a file from memory as a file download. 222 + /// 223 + /// If your file is already on the disc use `file_download` instead, to avoid 224 + /// having to read the file into memory to send it. 225 + /// 226 + /// The `content-disposition` header will be set to `attachment; 227 + /// filename="name"` to ensure the file is downloaded by the browser. This is 228 + /// especially good for files that the browser would otherwise attempt to open 229 + /// as this can result in cross-site scripting vulnerabilities. 230 + /// 231 + /// # Examples 232 + /// 233 + /// ```gleam 234 + /// response(200) 235 + /// |> file_download_from_memory(named: "myfile.txt", containing: "Hello, Joe!") 236 + /// // -> Response( 237 + /// // 200, 238 + /// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")], 239 + /// // File("/tmp/myfile.txt"), 240 + /// // ) 241 + /// ``` 242 + /// 243 + pub fn file_download_from_memory( 244 + response: Response, 245 + named name: String, 246 + containing data: BytesBuilder, 247 + ) -> Response { 248 + let name = uri.percent_encode(name) 249 + response 250 + |> response.set_header( 251 + "content-disposition", 252 + "attachment; filename=\"" <> name <> "\"", 253 + ) 254 + |> response.set_body(Bytes(data)) 173 255 } 174 256 175 257 /// Create a HTML response.
+7 -1
src/wisp/testing.gleam
··· 9 9 import gleam/string_builder 10 10 import gleam/uri 11 11 import simplifile 12 - import wisp.{type Request, type Response, Empty, File, Text} 12 + import wisp.{type Request, type Response, Bytes, Empty, File, Text} 13 13 14 14 /// The default secret key base used for test requests. 15 15 /// This should never be used outside of tests. ··· 228 228 case response.body { 229 229 Empty -> "" 230 230 Text(builder) -> string_builder.to_string(builder) 231 + Bytes(bytes) -> { 232 + let data = bytes_builder.to_bit_array(bytes) 233 + let assert Ok(string) = bit_array.to_string(data) 234 + string 235 + } 231 236 File(path) -> { 232 237 let assert Ok(contents) = simplifile.read(path) 233 238 contents ··· 245 250 pub fn bit_array_body(response: Response) -> BitArray { 246 251 case response.body { 247 252 Empty -> <<>> 253 + Bytes(builder) -> bytes_builder.to_bit_array(builder) 248 254 Text(builder) -> 249 255 bytes_builder.to_bit_array(bytes_builder.from_string_builder(builder)) 250 256 File(path) -> {