Revamp pastebin tutorial for Rocket v0.5.

Closes #1756.
This commit is contained in:
Sergio Benitez 2022-05-08 01:57:49 -05:00
parent 4c8bd61c4f
commit fccb5759db
5 changed files with 327 additions and 238 deletions

View File

@ -1,7 +1,8 @@
#[macro_use] extern crate rocket; #[macro_use] extern crate rocket;
#[cfg(test)]
mod tests;
mod paste_id; mod paste_id;
#[cfg(test)] mod tests;
use std::io; use std::io;
@ -12,8 +13,8 @@ use rocket::tokio::fs::{self, File};
use crate::paste_id::PasteId; use crate::paste_id::PasteId;
// In a real application, these would be retrieved dynamically from a config.
const HOST: Absolute<'static> = uri!("http://localhost:8000"); const HOST: Absolute<'static> = uri!("http://localhost:8000");
const ID_LENGTH: usize = 3; const ID_LENGTH: usize = 3;
#[post("/", data = "<paste>")] #[post("/", data = "<paste>")]
@ -53,6 +54,5 @@ fn index() -> &'static str {
#[launch] #[launch]
fn rocket() -> _ { fn rocket() -> _ {
rocket::build() rocket::build().mount("/", routes![index, upload, delete, retrieve])
.mount("/", routes![index, upload, delete, retrieve])
} }

View File

@ -1,12 +1,8 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use rocket::http::uri::fmt;
use rocket::request::FromParam;
use rand::{self, Rng}; use rand::{self, Rng};
use rocket::request::FromParam;
/// Table to retrieve base62 values from.
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/// A _probably_ unique paste ID. /// A _probably_ unique paste ID.
#[derive(UriDisplayPath)] #[derive(UriDisplayPath)]
@ -18,6 +14,8 @@ impl PasteId<'_> {
/// probability of a collision depends on the value of `size` and the number /// probability of a collision depends on the value of `size` and the number
/// of IDs generated thus far. /// of IDs generated thus far.
pub fn new(size: usize) -> PasteId<'static> { pub fn new(size: usize) -> PasteId<'static> {
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
let mut id = String::with_capacity(size); let mut id = String::with_capacity(size);
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
for _ in 0..size { for _ in 0..size {
@ -27,6 +25,7 @@ impl PasteId<'_> {
PasteId(Cow::Owned(id)) PasteId(Cow::Owned(id))
} }
/// Returns the path to the paste in `upload/` corresponding to this ID.
pub fn file_path(&self) -> PathBuf { pub fn file_path(&self) -> PathBuf {
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload"); let root = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload");
Path::new(root).join(self.0.as_ref()) Path::new(root).join(self.0.as_ref())
@ -44,11 +43,3 @@ impl<'a> FromParam<'a> for PasteId<'a> {
.ok_or(param) .ok_or(param)
} }
} }
impl<'a> fmt::FromUriParam<fmt::Path, &'a str> for PasteId<'_> {
type Target = PasteId<'a>;
fn from_uri_param(param: &'a str) -> Self::Target {
PasteId(param.into())
}
}

View File

@ -1,6 +1,7 @@
use super::{rocket, index, PasteId}; use super::{rocket, index, PasteId};
use rocket::local::blocking::Client; use rocket::local::blocking::Client;
use rocket::http::{Status, ContentType}; use rocket::http::{Status, ContentType};
use rocket::request::FromParam;
fn extract_id(from: &str) -> Option<String> { fn extract_id(from: &str) -> Option<String> {
from.rfind('/').map(|i| &from[(i + 1)..]).map(|s| s.trim_end().to_string()) from.rfind('/').map(|i| &from[(i + 1)..]).map(|s| s.trim_end().to_string())
@ -25,6 +26,7 @@ fn upload_paste(client: &Client, body: &str) -> String {
} }
fn download_paste(client: &Client, id: &str) -> Option<String> { fn download_paste(client: &Client, id: &str) -> Option<String> {
let id = PasteId::from_param(id).expect("valid ID");
let response = client.get(uri!(super::retrieve(id))).dispatch(); let response = client.get(uri!(super::retrieve(id))).dispatch();
if response.status().class().is_success() { if response.status().class().is_success() {
Some(response.into_string().unwrap()) Some(response.into_string().unwrap())
@ -34,6 +36,7 @@ fn download_paste(client: &Client, id: &str) -> Option<String> {
} }
fn delete_paste(client: &Client, id: &str) { fn delete_paste(client: &Client, id: &str) {
let id = PasteId::from_param(id).expect("valid ID");
let response = client.delete(uri!(super::delete(id))).dispatch(); let response = client.delete(uri!(super::delete(id))).dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
} }

View File

@ -1,19 +1,22 @@
# Pastebin # Pastebin Tutorial
To give you a taste of what a real Rocket application looks like, this section This section of the guide is a tutorial intended to demonstrate how real-world
of the guide is a tutorial on how to create a Pastebin application in Rocket. A Rocket applications are crafted. We'll build a simple pastebin service that
pastebin is a simple web application that allows users to upload a text document allows users to upload a file from any HTTP client, including `curl`. The
and later retrieve it via a special URL. They're often used to share code service will respond back with a URL to the uploaded file.
snippets, configuration files, and error logs. In this tutorial, we'll build a
simple pastebin service that allows users to upload a file from their terminal. ! note: What's a pastebin?
The service will respond back with a URL to the uploaded file.
A pastebin is a simple web application that allows users to upload a document
and later retrieve it via a special URL. They're often used to share code
snippets, configuration files, and error logs.
## Finished Product ## Finished Product
A souped-up, completed version of the application you're about to build is A souped-up, completed version of the application you're about to build is
deployed live at [paste.rs](https://paste.rs). Feel free to play with the deployed live at [paste.rs](https://paste.rs). Feel free to play with the
application to get a feel for how it works. For example, to upload a text application to get a feel for how it works. For example, to upload a text
document named `test.txt`, you can do: document named `test.txt`, you can run:
```sh ```sh
curl --data-binary @test.txt https://paste.rs/ curl --data-binary @test.txt https://paste.rs/
@ -22,12 +25,18 @@ curl --data-binary @test.txt https://paste.rs/
The finished product is composed of the following routes: The finished product is composed of the following routes:
* index: **`GET /`** - returns a simple HTML page with instructions about how * `index` - `#[get("/")]`
to use the service
* upload: **`POST /`** - accepts raw data in the body of the request and returns a simple HTML page with instructions about how to use the service
responds with a URL of a page containing the body's content
* retrieve: **`GET /<id>`** - retrieves the content for the paste with id * `upload` - `#[post("/")]`
`<id>`
accepts raw data in the body of the request and responds with a URL of a
page containing the body's content
* `retrieve` - `#[get("/<id>")]`
retrieves the content for the paste with id `<id>`
## Getting Started ## Getting Started
@ -70,10 +79,10 @@ tutorial, we'll create the three routes and accompanying handlers.
## Index ## Index
The first route we'll create is the `index` route. This is the page users will The first route we'll create is `index`. This is the page users will see when
see when they first visit the service. As such, the route should field requests they first visit the service. As such, the route should handle `GET /`. We
of the form `GET /`. We declare the route and its handler by adding the `index` declare the route and its handler by adding the `index` function below to
function below to `src/main.rs`: `src/main.rs`:
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
@ -98,9 +107,11 @@ fn index() -> &'static str {
This declares the `index` route for requests to `GET /` as returning a static This declares the `index` route for requests to `GET /` as returning a static
string with the specified contents. Rocket will take the string and return it as string with the specified contents. Rocket will take the string and return it as
the body of a fully formed HTTP response with `Content-Type: text/plain`. You the body of a fully formed HTTP response with `Content-Type: text/plain`. You
can read more about how Rocket formulates responses at the [API documentation can read more about how Rocket formulates responses in the [responses section]
for the Responder of the guide or at the [API documentation for the Responder
trait](@api/rocket/response/trait.Responder.html). trait](@api/rocket/response/trait.Responder.html).
[responses section]: ../responses
Remember that routes first need to be mounted before Rocket dispatches requests Remember that routes first need to be mounted before Rocket dispatches requests
to them. To mount the `index` route, modify the main function so that it reads: to them. To mount the `index` route, modify the main function so that it reads:
@ -116,199 +127,123 @@ fn rocket() -> _ {
``` ```
You should now be able to `cargo run` the application and visit the root path You should now be able to `cargo run` the application and visit the root path
(`/`) to see the text being displayed. (`/`) to see the text.
## Uploading ## Design
The most complicated aspect of the pastebin, as you might imagine, is handling Before we continue, we'll need to make a few design decisions.
upload requests. When a user attempts to upload a pastebin, our service needs to
generate a unique ID for the upload, read the data, write it out to a file or
database, and then return a URL with the ID. We'll take each of these one step
at a time, beginning with generating IDs.
### Unique IDs * **Where should pastes be stored?**
Generating a unique and useful ID is an interesting topic, but it is outside the To keep things simple, we'll store uploaded pastes on the file system inside
scope of this tutorial. Instead, we simply provide the code for a `PasteId` of an `upload/` directory. Let's create that directory next to `src/` in our
structure that represents a _probably_ unique ID. Read through the code, then project now:
copy/paste it into a new file named `paste_id.rs` in the `src/` directory:
```rust ```sh
use std::fmt; mkdir upload
use std::borrow::Cow; ```
use rand::{self, Rng}; Our project tree now looks like:
/// Table to retrieve base62 values from. ```sh
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; .
├── Cargo.toml
├── src
│   └── main.rs
└── upload
```
/// A _probably_ unique paste ID. * **What should we name the uploaded paste files?**
pub struct PasteId<'a>(Cow<'a, str>);
impl<'a> PasteId<'a> { Similarly, we'll keep things simple by naming paste files a string of random
/// Generate a _probably_ unique ID with `size` characters. For readability, but readable characters. We'll call this random string the paste's "ID". To
/// the characters used are from the sets [0-9], [A-Z], [a-z]. The represent, generate, and store the ID, we'll create a `PasteId` structure in
/// probability of a collision depends on the value of `size` and the number a new module file named `paste_id.rs` with the following contents:
/// of IDs generated thus far.
pub fn new(size: usize) -> PasteId<'static> { ```rust
let mut id = String::with_capacity(size); use std::borrow::Cow;
let mut rng = rand::thread_rng(); use std::path::{Path, PathBuf};
for _ in 0..size {
id.push(BASE62[rng.gen::<usize>() % 62] as char); use rand::{self, Rng};
/// A _probably_ unique paste ID.
pub struct PasteId<'a>(Cow<'a, str>);
impl PasteId<'_> {
/// Generate a _probably_ unique ID with `size` characters. For readability,
/// the characters used are from the sets [0-9], [A-Z], [a-z]. The
/// probability of a collision depends on the value of `size` and the number
/// of IDs generated thus far.
pub fn new(size: usize) -> PasteId<'static> {
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
let mut id = String::with_capacity(size);
let mut rng = rand::thread_rng();
for _ in 0..size {
id.push(BASE62[rng.gen::<usize>() % 62] as char);
}
PasteId(Cow::Owned(id))
} }
PasteId(Cow::Owned(id)) /// Returns the path to the paste in `upload/` corresponding to this ID.
pub fn file_path(&self) -> PathBuf {
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload");
Path::new(root).join(self.0.as_ref())
}
} }
} ```
```
Then, in `src/main.rs`, add the following after `extern crate rocket`: We've given you the ID and path generation code for free. Our project tree
now looks like:
```rust ```sh
# /* .
mod paste_id; ├── Cargo.toml
# */ mod paste_id { pub struct PasteId; } ├── src
│   ├── main.rs
│   └── paste_id.rs # new! contains `PasteId`
└── upload
```
use paste_id::PasteId; We'll import the new module and struct in `src/main.rs`, after the `extern
``` crate rocket`:
Finally, add a dependency for the `rand` crate to the `Cargo.toml` file: ```rust
# /*
mod paste_id;
# */ mod paste_id { pub struct PasteId; }
```toml use crate::paste_id::PasteId;
[dependencies] ```
# existing Rocket dependencies...
rand = "0.8"
```
Then, ensure that your application builds with the new code: You'll notice that our code to generate paste IDs uses the `rand` crate, so
we'll need to add it as a dependency in our `Cargo.toml` file:
```sh ```toml
cargo build [dependencies]
``` ## existing Rocket dependencies...
rand = "0.8"
```
You'll likely see many "unused" warnings for the new code we've added: that's Ensure that your application builds with the new code:
okay and expected. We'll be using the new code soon.
### Processing ```sh
cargo build
```
Believe it or not, the hard part is done! (_whew!_). You'll likely see many "unused" warnings for the new code we've added: that's
okay and expected. We'll be using the new code soon.
To process the upload, we'll need a place to store the uploaded files. To With these design decisions made, we're ready to continue writing our
simplify things, we'll store the uploads in a directory named `upload/`. Create application.
an `upload` directory next to the `src` directory:
```sh
mkdir upload
```
For the `upload` route, we'll need to import `Data`:
```rust
use rocket::Data;
```
The [Data](@api/rocket/data/struct.Data.html) structure is key
here: it represents an unopened stream to the incoming request body data. We'll
use it to efficiently stream the incoming request to a file.
### Upload Route
We're finally ready to write the `upload` route. Before we show you the code,
you should attempt to write the route yourself. Here's a hint: a possible route
and handler signature look like this:
```rust
# #[macro_use] extern crate rocket;
use rocket::Data;
use rocket::response::Debug;
#[post("/", data = "<paste>")]
fn upload(paste: Data<'_>) -> std::io::Result<String> {
# unimplemented!()
/* .. */
}
```
Your code should:
1. Create a new `PasteId` of a length of your choosing.
2. Construct a filename inside `upload/` given the `PasteId`.
3. Stream the `Data` to the file with the constructed filename.
4. Construct a URL given the `PasteId`.
5. Return the URL to the client.
Here's our version (in `src/main.rs`):
```rust
# #[macro_use] extern crate rocket;
# fn main() {}
# use std::fmt;
# struct PasteId;
# impl PasteId { fn new(n: usize) -> Self { PasteId } }
# impl fmt::Display for PasteId {
# fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Ok(()) }
# }
use rocket::response::Debug;
use rocket::data::{Data, ToByteUnit};
#[post("/", data = "<paste>")]
async fn upload(paste: Data<'_>) -> Result<String, Debug<std::io::Error>> {
let id = PasteId::new(3);
let filename = format!("upload/{id}", id = id);
let url = format!("{host}/{id}\n", host = "http://localhost:8000", id = id);
// Write the paste out, limited to 128KiB, and return the URL.
paste.open(128.kibibytes()).into_file(filename).await?;
Ok(url)
}
```
Note the [`kibibytes()`] method call: this method comes from the [`ToByteUnit`]
extension trait. Ensure that the route is mounted at the root path:
```rust
# #[macro_use] extern crate rocket;
# #[get("/")] fn index() {}
# #[post("/")] fn upload() {}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index, upload])
}
```
Test that your route works via `cargo run`. From a separate terminal, upload a
file using `curl`. Then verify that the file was saved to the `upload` directory
with the correct ID:
```sh
# in the project root
cargo run
# in a separate terminal
echo "Hello, world." | curl --data-binary @- http://localhost:8000
# => http://localhost:8000/eGs
# back to the terminal running the pastebin
<ctrl-c> # kill running process
ls upload # ensure the upload is there
cat upload/* # ensure that contents are correct
```
Note that since we haven't created a `GET /<id>` route, visiting the returned URL
will result in a **404**. We'll fix that now.
[`kibibytes()`]: @api/rocket/data/trait.ToByteUnit.html#tymethod.kibibytes
[`ToByteUnit`]: @api/rocket/data/trait.ToByteUnit.html
## Retrieving Pastes ## Retrieving Pastes
The final step is to create the `retrieve` route which, given an `<id>`, will We'll proceed with a `retrieve` route which, given an `<id>`, will return the
return the corresponding paste if it exists. corresponding paste if it exists or otherwise **404**. As we now know, that
means we'll be reading the contents of the file corresponding to `<id>` in the
`upload/` directory and return them to the user.
Here's a first take at implementing the `retrieve` route. The route below takes Here's a first take at implementing the `retrieve` route. The route below takes
in an `<id>` as a dynamic path element. The handler uses the `id` to construct a in an `<id>` as a dynamic path element. The handler uses the `id` to construct a
@ -321,11 +256,13 @@ paste doesn't exist.
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
use std::path::Path;
use rocket::tokio::fs::File; use rocket::tokio::fs::File;
#[get("/<id>")] #[get("/<id>")]
async fn retrieve(id: &str) -> Option<File> { async fn retrieve(id: &str) -> Option<File> {
let filename = format!("upload/{id}", id = id); let upload_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload");
let filename = Path::new(upload_dir).join(id);
File::open(&filename).await.ok() File::open(&filename).await.ok()
} }
``` ```
@ -336,17 +273,22 @@ Make sure that the route is mounted at the root path:
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
# #[get("/")] fn index() {} # #[get("/")] fn index() {}
# #[post("/")] fn upload() {}
# #[get("/<id>")] fn retrieve(id: String) {} # #[get("/<id>")] fn retrieve(id: String) {}
#[launch] #[launch]
fn rocket() -> _ { fn rocket() -> _ {
rocket::build().mount("/", routes![index, upload, retrieve]) rocket::build().mount("/", routes![index, retrieve])
} }
``` ```
Give it a try! Create some fake pastes in the `upload/` directory, run the
application, and try to retrieve them by visiting the corresponding URL.
### A Problem
Unfortunately, there's a problem with this code. Can you spot the issue? The Unfortunately, there's a problem with this code. Can you spot the issue? The
`&str` type should tip you off! `&str` type in `retrieve` should tip you off! We've crafted a wonderful type to
represent paste IDs but have ignored it!
The issue is that the _user_ controls the value of `id`, and as a result, can The issue is that the _user_ controls the value of `id`, and as a result, can
coerce the service into opening files inside `upload/` that aren't meant to be coerce the service into opening files inside `upload/` that aren't meant to be
@ -358,20 +300,27 @@ This is a big problem; it's known as the [full path disclosure
attack](https://www.owasp.org/index.php/Full_Path_Disclosure), and Rocket attack](https://www.owasp.org/index.php/Full_Path_Disclosure), and Rocket
provides the tools to prevent this and other kinds of attacks from happening. provides the tools to prevent this and other kinds of attacks from happening.
To prevent the attack, we need to _validate_ `id` before we use it. Since the ### The Solution
`id` is a dynamic parameter, we can use Rocket's
[FromParam](@api/rocket/request/trait.FromParam.html) trait to To prevent the attack, we need to _validate_ `id` before we use it. We do so by
implement the validation and ensure that the `id` is a valid `PasteId` before using a type more specific than `&str` to represent IDs and then asking Rocket
using it. We do this by implementing `FromParam` for `PasteId` in to validate the untrusted `id` input as that type. If validation fails, Rocket
`src/paste_id.rs`, as below: will take care to not call our routes with bad input.
Typed validation for dynamic paramters like `id` is implemented via the
[`FromParam`] trait. Rocket uses `FromParam` to automatically validate and parse
dynamic path parameters like `id`. We already have a type that represents valid
paste IDs, `PasteId`, so we'll simply need to implement `FromParam` for
`PasteId`.
Here's the `FromParam` implementation for `PasteId` in `src/paste_id.rs`:
[`FromParam`]: @api/rocket/request/trait.FromParam.html
```rust ```rust
use std::borrow::Cow;
use rocket::request::FromParam; use rocket::request::FromParam;
# use std::borrow::Cow;
/// A _probably_ unique paste ID. # pub struct PasteId<'a>(Cow<'a, str>);
pub struct PasteId<'a>(Cow<'a, str>);
/// Returns an instance of `PasteId` if the path segment is a valid ID. /// Returns an instance of `PasteId` if the path segment is a valid ID.
/// Otherwise returns the invalid ID as the `Err` value. /// Otherwise returns the invalid ID as the `Err` value.
@ -379,36 +328,48 @@ impl<'a> FromParam<'a> for PasteId<'a> {
type Error = &'a str; type Error = &'a str;
fn from_param(param: &'a str) -> Result<Self, Self::Error> { fn from_param(param: &'a str) -> Result<Self, Self::Error> {
match param.chars().all(|c| c.is_ascii_alphanumeric()) { param.chars().all(|c| c.is_ascii_alphanumeric())
true => Ok(PasteId(param.into())), .then(|| PasteId(param.into()))
false => Err(param) .ok_or(param)
}
} }
} }
``` ```
Then, we simply need to change the type of `id` in the handler to `PasteId`. ! note: This implementation, while secure, could be improved.
Rocket will then ensure that `<id>` represents a valid `PasteId` before calling
the `retrieve` route, preventing attacks on the `retrieve` route: Our `from_param` function is simplistic and could be improved by, for example,
checking that the length of the `id` is within some known bound, introducing
stricter character checks, checking for the existing of a paste file, and/or
potentially blacklisting sensitive files as needed.
Given this implementation, we can change the type of `id` in `retrieve` to
`PasteId`. Rocket will then ensure that `<id>` represents a valid `PasteId`
before calling the `retrieve` route, preventing the previous attack entirely:
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
use rocket::tokio::fs::File;
# use std::borrow::Cow; # use std::borrow::Cow;
# use rocket::tokio::fs::File; # use std::path::PathBuf;
# use rocket::request::FromParam;
# type PasteId<'a> = &'a str; # pub struct PasteId<'a>(Cow<'a, str>);
# impl PasteId<'_> {
# pub fn new(size: usize) -> PasteId<'static> { todo!() }
# pub fn file_path(&self) -> PathBuf { todo!() }
# }
# impl<'a> FromParam<'a> for PasteId<'a> {
# type Error = &'a str;
# fn from_param(param: &'a str) -> Result<Self, Self::Error> { todo!() }
# }
#[get("/<id>")] #[get("/<id>")]
async fn retrieve(id: PasteId<'_>) -> Option<File> { async fn retrieve(id: PasteId<'_>) -> Option<File> {
let filename = format!("upload/{id}", id = id); File::open(id.file_path()).await.ok()
File::open(&filename).await.ok()
} }
``` ```
Note that our `from_param` function is simplistic and could be improved by, for Notice how much nicer this implementation is! And this time, it's secure.
example, checking that the length of the `id` is within some known bound or
potentially blacklisting sensitive files as needed.
The wonderful thing about using `FromParam` and other Rocket traits is that they The wonderful thing about using `FromParam` and other Rocket traits is that they
centralize policies. For instance, here, we've centralized the policy for valid centralize policies. For instance, here, we've centralized the policy for valid
@ -416,6 +377,140 @@ centralize policies. For instance, here, we've centralized the policy for valid
are added that require a `PasteId`, no further work has to be done: simply use are added that require a `PasteId`, no further work has to be done: simply use
the type in the signature and Rocket takes care of the rest. the type in the signature and Rocket takes care of the rest.
## Uploading
Now that we can retrieve pastes safely, it's time to actually store them. We'll
write an `upload` route that, according to our design, takes a paste's contents
and writes them to a file with a randomly generated ID inside of the `upload/`
directory. It'll return a URL to the client for the paste corresponding to the
`retrieve` route we just route.
### Streaming Data
To stream the incoming paste data to a file, we'll make use of [`Data`], a [data
guard] that represents an unopened stream to the incoming request body data.
Before we show you the code, you should attempt to write the route yourself.
Here's a hint: one possible route and handler signature look like this:
```rust
# #[macro_use] extern crate rocket;
use rocket::Data;
#[post("/", data = "<paste>")]
async fn upload(paste: Data<'_>) -> std::io::Result<String> {
/* .. */
# Ok("".into())
}
```
[`Data`]: @api/rocket/data/struct.Data.html
[data guard]: ../requests/#body-data
Your code should:
1. Create a new `PasteId` of a length of your choosing.
2. Construct a path to the `PasteId` inside of `upload/`.
3. Stream the `Data` to the file at the constructed path.
4. Construct a URL for the `PasteId`.
5. Return the URL to the client.
### Solution
Here's our version:
```rust
# #[macro_use] extern crate rocket;
// We derive `UriDisplayPath` for `PasteId` in `paste_id.rs`:
# use std::borrow::Cow;
# use std::path::{Path, PathBuf};
# use rocket::request::FromParam;
#[derive(UriDisplayPath)]
pub struct PasteId<'a>(Cow<'a, str>);
# impl PasteId<'_> {
# pub fn new(size: usize) -> PasteId<'static> { todo!() }
# pub fn file_path(&self) -> PathBuf { todo!() }
# }
#
# impl<'a> FromParam<'a> for PasteId<'a> {
# type Error = &'a str;
# fn from_param(param: &'a str) -> Result<Self, Self::Error> { todo!() }
# }
// We implement the `upload` route in `main.rs`:
use rocket::data::{Data, ToByteUnit};
use rocket::http::uri::Absolute;
# use rocket::tokio::fs::File;
// In a real application, these would be retrieved dynamically from a config.
const ID_LENGTH: usize = 3;
const HOST: Absolute<'static> = uri!("http://localhost:8000");
# #[get("/")] fn index() -> &'static str { "" }
# #[get("/<id>")] fn retrieve(id: PasteId<'_>) -> Option<File> { todo!() }
#[post("/", data = "<paste>")]
async fn upload(paste: Data<'_>) -> std::io::Result<String> {
let id = PasteId::new(ID_LENGTH);
paste.open(128.kibibytes()).into_file(id.file_path()).await?;
Ok(uri!(HOST, retrieve(id)).to_string())
}
```
We note the following Rocket APIs being used in our implementation:
* The [`kibibytes()`] method, which comes from the [`ToByteUnit`] trait.
* [`Data::open()`] to open [`Data`] as a [`DataStream`].
* [`DataStream::into_file()`] for writing the data stream into a file.
* The [`UriDisplayPath`] derive, allowing `PasteId` to be used in [`uri!`].
* The [`uri!`] macro to crate type-safe, URL-safe URIs.
[`Data::open()`]: @api/rocket/data/struct.Data.html#method.open
[`Data`]: @api/rocket/data/struct.Data.html
[`DataStream`]: @api/rocket/data/struct.DataStream.html
[`DataStream::into_file()`]: @api/rocket/data/struct.DataStream.html#method.into_file
[`uri!`]: @api/rocket/macro.uri.html
[`kibibytes()`]: @api/rocket/data/trait.ToByteUnit.html#tymethod.kibibytes
[`ToByteUnit`]: @api/rocket/data/trait.ToByteUnit.html
[`UriDisplayPath`]: @api/rocket/derive.UriDisplayPath.html
Ensure that the route is mounted at the root path:
```rust
# #[macro_use] extern crate rocket;
# #[get("/")] fn index() {}
# #[get("/<id>")] fn retrieve(id: &str) {}
# #[post("/")] fn upload() {}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index, retrieve, upload])
}
```
Test that your route works via `cargo run`. From a separate terminal, upload a
file using `curl` then retrieve the paste using the returned URL.
```sh
## in the project root
cargo run
## in a separate terminal
echo "Hello, Rocket!" | curl --data-binary @- http://localhost:8000
## => http://localhost:8000/eGs
## confirm we can retrieve the paste (replace with URL from above)
curl http://localhost:8000/eGs
## we can check the contents of `upload/` as well
<ctrl-c> # kill running process
ls upload # ensure the upload is there
cat upload/* # ensure that contents are correct
```
## Conclusion ## Conclusion
That's it! Ensure that all of your routes are mounted and test your application. That's it! Ensure that all of your routes are mounted and test your application.

View File

@ -28,8 +28,8 @@ aspect of Rocket. The sections are:
- **[Testing](testing/):** how to unit and integration test a Rocket - **[Testing](testing/):** how to unit and integration test a Rocket
application. application.
- **[Configuration](configuration/):** how to configure a Rocket application. - **[Configuration](configuration/):** how to configure a Rocket application.
- **[Pastebin](pastebin/):** a tutorial on how to create a pastebin with - **[Pastebin Tutorial](pastebin/):** a tutorial on how to create a pastebin
Rocket. with Rocket.
- **[Conclusion](conclusion/):** concludes the guide and discusses next steps - **[Conclusion](conclusion/):** concludes the guide and discusses next steps
for learning. for learning.
- **[FAQ](faq/):** answers to frequently asked questions about Rocket and - **[FAQ](faq/):** answers to frequently asked questions about Rocket and