Skip to content

Commit

Permalink
Multipart support (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
levkk authored Nov 12, 2024
1 parent b641ff8 commit 0e3e065
Show file tree
Hide file tree
Showing 29 changed files with 700 additions and 133 deletions.
12 changes: 10 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ members = [
"rwf-tests",
"examples/request-tracking",
"examples/engine",
"rwf-admin",
"rwf-admin", "examples/files",
]
exclude = ["examples/rails", "rwf-ruby", "examples/django"]
1 change: 1 addition & 0 deletions docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ section configures database connection settings, like the database URL, connecti
| `secret_key` | Secret key, encoded using base64, used for [encryption](security/encryption.md). | Randomly generated |
| `cache_templates` | Toggle caching of [dynamic templates](views/templates/index.md). | `false` in debug, `true` in release |
| `csrf_protection` | Validate the [CSRF](security/CSRF.md) token is present on requests that mutate your application (POST, PUT, PATCH). | `true` |
| `max_request_size` | Maximum `Content-Length` the server will process. Any requests larger than this will be rejected. | 5 MB |

#### Secret key

Expand Down
28 changes: 28 additions & 0 deletions docs/docs/controllers/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,30 @@ column names and data types to your form:
</form>
```

#### Files

Rwf supports file uploads using multipart form encoding. A POST request with `Content-Type: multipart/form-data` containing files can be retrieved by their input name:

=== "Rust"
```rust
let form = request.form_data()?;
let file = form.file("file_upload");

if let Some(file) = file {
let bytes = file.bytes();
let name = file.name();
}
```
=== "HTML"
```html
<form method="post" enctype="multipart/form-data">
<input type="file" name="file_upload">
</form>
```

!!! note
Forms that wish to upload files need to have the `enctype="multipart/form-data"` attribute. By default, HTML forms use `application/x-www-form-urlencoded` encoding which will omit any unsupported inputs like files.

### JSON

If the body is expected to be JSON, it can be read using the `json` method instead. The `json` method
Expand Down Expand Up @@ -136,3 +160,7 @@ If you don't know the schema of the JSON request, you can use [`json_raw`](https
If you use [`FormData::get_required`](https://docs.rs/rwf/latest/rwf/http/form_data/enum.FormData.html#method.get_required) or [`Request::json`](https://docs.rs/rwf/latest/rwf/http/request/struct.Request.html#method.json) methods with the `?` operator,
an error will be returned to the client automatically if the parsing of the form data fails.
Unlike other controller errors that return HTTP `500`, this type of error will return HTTP `400` (Bad Request).

## Learn more

- [examples/files](https://github.com/levkk/rwf/tree/main/examples/files)
13 changes: 11 additions & 2 deletions docs/docs/views/templates/templates-in-controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ impl Controller for Index {

The template will be loaded from the [template cache](caching.md), rendered with the provided context, and used as a body for a response with the correct `Content-Type` header.

Since this is a very common way to use templates in controllers, Rwf has the `render!` macro to make this less verbose:
## Render macro
Since it's very common to render templates inside controllers, Rwf has the `render!` macro to make it less verbose:

```rust
#[async_trait]
Expand All @@ -30,10 +31,18 @@ impl Controller for Index {
}
```

The `render!` macro takes the template path as the first argument, and optionally, a mapping of variable names and values as subsequent arguments. It creates a [`Response`](../../controllers/response.md) automatically, so there is no need to return one manually.
The `render!` macro takes the template path as the first argument, and optionally, a mapping of variable names and values as subsequent arguments. It returns a [`Response`](../../controllers/response.md) automatically.

If the template doesn't have any variables, you can use `render!` with just the template name:

```rust
render!("templates/index.html")
```

### Response code

By default, the `render!` macro returns the rendered template with HTTP code `200 OK`. If you want to return a different code, pass it as the last argument to the macro:

```rust
render!("templates/index.html", "title" => "Home page", 201)
```
8 changes: 8 additions & 0 deletions examples/files/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "files"
version = "0.1.0"
edition = "2021"

[dependencies]
rwf = { version = "0.1.7", path = "../../rwf" }
tokio = { version = "1", features = ["full"] }
Empty file.
29 changes: 29 additions & 0 deletions examples/files/src/controllers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use rwf::prelude::*;

#[derive(Default, macros::PageController)]
pub struct Upload;

#[async_trait]
impl PageController for Upload {
/// Upload page.
async fn get(&self, _req: &Request) -> Result<Response, Error> {
render!("templates/upload.html")
}

/// Handle upload file.
async fn post(&self, req: &Request) -> Result<Response, Error> {
let form_data = req.form_data()?;
let comment = form_data.get_required::<String>("comment")?;

if let Some(file) = form_data.file("file") {
render!("templates/ok.html",
"name" => file.name(),
"size" => file.body().len() as i64,
"content_type" => file.content_type(),
"comment" => comment,
201); // 201 = created
} else {
Ok(Response::bad_request())
}
}
}
14 changes: 14 additions & 0 deletions examples/files/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mod controllers;
mod models;

use rwf::http::{self, Server};
use rwf::prelude::*;

#[tokio::main]
async fn main() -> Result<(), http::Error> {
Logger::init();

Server::new(vec![route!("/" => controllers::Upload)])
.launch("0.0.0.0:8000")
.await
}
1 change: 1 addition & 0 deletions examples/files/src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Empty file added examples/files/static/.gitkeep
Empty file.
Empty file.
5 changes: 5 additions & 0 deletions examples/files/templates/head.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<head>
<%= rwf_head() %>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script async src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</head>
30 changes: 30 additions & 0 deletions examples/files/templates/ok.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en-US">
<%% "templates/head.html" %>
<body>
<div class="container my-5">
<h3 class="mb-4">Upload successful</h3>
<table>
<thead>
<tr>
<th>File name</th>
<th>File size</th>
<th>Content type</th>
<th>Comment</th>
</tr>
</thead>
<tbody>
<tr>
<td class="pe-3"><%= name %></td>
<td class="pe-3"><%= size %> bytes</td>
<td class="pe-3"><%= content_type %></td>
<td><%= comment %></td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-end">
<a href="/" class="btn btn-primary btn-lg">Back</a>
</div>
</div>
</body>
</html>
27 changes: 27 additions & 0 deletions examples/files/templates/upload.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!doctype html>
<html lang="en-US">
<%% "templates/head.html" %>
<body>
<div class="container my-5">
<h3 class="mb-4">Upload file</h3>
<form method="post" action="/" enctype="multipart/form-data">
<%= csrf_token() %>
<div class="mb-3">
<label class="form-label">Comment</label>
<input type="text" name="comment" class="form-control">
</div>

<div class="mb-3">
<label class="form-label">Upload file</label>
<input type="file" name="file" class="form-control">
</div>

<div class="d-flex justify-content-end">
<button class="btn btn-primary" type="submit">
Upload
</button>
</div>
</form>
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion rwf-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rwf-macros"
version = "0.1.6"
version = "0.1.7"
edition = "2021"
license = "MIT"
description = "Macros for the Rust Web Framework"
Expand Down
32 changes: 26 additions & 6 deletions rwf-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ extern crate proc_macro;
use proc_macro::TokenStream;

use syn::parse::{Parse, ParseStream};
use syn::LitInt;
use syn::{
parse_macro_input, punctuated::Punctuated, Attribute, Data, DeriveInput, Expr, LitStr, Meta,
Result, Token, Type,
Expand Down Expand Up @@ -437,6 +438,7 @@ struct RenderInput {
template_name: LitStr,
_comma: Option<Token![,]>,
context: Vec<ContextInput>,
code: Option<LitInt>,
}

struct ContextInput {
Expand Down Expand Up @@ -482,16 +484,22 @@ impl Parse for RenderInput {
fn parse(input: ParseStream) -> Result<Self> {
let template_name: LitStr = input.parse()?;
let _comma: Option<Token![,]> = input.parse()?;
let mut code = None;

let context = if _comma.is_some() {
let mut result = vec![];
loop {
let context: Result<ContextInput> = input.parse();

if let Ok(context) = context {
result.push(context);
if input.peek(LitInt) {
let c: LitInt = input.parse().unwrap();
code = Some(c);
} else {
break;
let context: Result<ContextInput> = input.parse();

if let Ok(context) = context {
result.push(context);
} else {
break;
}
}
}

Expand All @@ -504,6 +512,7 @@ impl Parse for RenderInput {
template_name,
_comma,
context,
code,
})
}
}
Expand Down Expand Up @@ -561,11 +570,22 @@ pub fn render(input: TokenStream) -> TokenStream {
values
};

let code = if let Some(code) = input.code {
quote! {
let response = response.code(#code);
}
} else {
quote! {}
};

quote! {
{
let template = rwf::view::template::Template::load(#template_name)?;
#(#render_call)*
return Ok(rwf::http::Response::new().html(html))

let response = rwf::http::Response::new().html(html);
#code
return Ok(response)
}
}
.into()
Expand Down
2 changes: 1 addition & 1 deletion rwf-ruby/src/.clangd
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ CompileFlags:
Add:
- "-I/usr/include/ruby-3.3.0"
- "-I/usr/include/ruby-3.3.0/x86_64-linux"
- "-I/opt/homebrew/Cellar/ruby/3.3.4/include"
- "-I/opt/homebrew/Cellar/ruby/3.3.4/include/ruby-3.3.0"
5 changes: 1 addition & 4 deletions rwf-ruby/src/bindings.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@
#include <stdlib.h>
#include <ruby.h>
#include <stdio.h>
#include <string.h>
#include "bindings.h"
#include "ruby/internal/intern/string.h"
#include "ruby/internal/special_consts.h"
#include "ruby/internal/symbol.h"


static int rwf_print_error(void);

Expand Down
4 changes: 2 additions & 2 deletions rwf/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rwf"
version = "0.1.6"
version = "0.1.7"
edition = "2021"
license = "MIT"
description = "Framework for building web applications in the Rust programming language"
Expand All @@ -26,7 +26,7 @@ parking_lot = "0.12"
once_cell = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
rwf-macros = { path = "../rwf-macros", version = "0.1.6" }
rwf-macros = { path = "../rwf-macros", version = "0.1.7" }
colored = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
9 changes: 8 additions & 1 deletion rwf/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ pub struct General {
pub tty: bool,
#[serde(default = "General::default_header_max_size")]
pub header_max_size: usize,
#[serde(default = "General::default_max_request_size")]
pub max_request_size: usize,
#[serde(skip)]
pub default_auth: AuthHandler,
#[serde(skip)]
Expand All @@ -167,6 +169,7 @@ impl Default for General {
session_duration: General::default_session_duration(),
tty: General::default_tty(),
header_max_size: General::default_header_max_size(),
max_request_size: General::default_max_request_size(),
default_auth: AuthHandler::default(),
default_middleware: MiddlewareSet::without_default(vec![]),
}
Expand Down Expand Up @@ -262,6 +265,10 @@ impl General {
fn default_header_max_size() -> usize {
16 * 1024 // 16K
}

fn default_max_request_size() -> usize {
5 * 1024 * 1024 // 5M
}
}

#[derive(Serialize, Deserialize, Clone)]
Expand Down Expand Up @@ -386,7 +393,7 @@ mod test {

#[test]
fn test_load_config() {
for config_path in ["Rwf.toml", "rwf.toml", "Rum.toml"] {
for config_path in ["rwf.toml", "Rum.toml"] {
let tmp_dir = TempDir::new("test").unwrap();
let path = tmp_dir.path();

Expand Down
Loading

0 comments on commit 0e3e065

Please sign in to comment.