Building a PasteBin in Rust: A Step-by-Step Tutorial
Eleftheria Batsou
Posted on March 30, 2024
Hello, Rust friends! Today, we're going to build our very own PasteBin clone using Rust, Actix for web server functionality, Rustqlite for the database operations, and HTML/CSS for the user interface. If you're new to Rust or web development, you're in for a treat. This project is a fantastic way to get hands-on experience while learning some core concepts of Rust and web programming.
By the end of this tutorial, you'll have a fully functional PasteBin clone that you can run locally on your machine.
What is PasteBin, and Why Rust?
PasteBin is a simple online service that allows users to upload and share text or code snippets. It's a handy tool for developers to quickly share code or configuration files.
Rust, known for its safety and performance, is an excellent choice for web services. It offers memory safety without a garbage collector, making it suitable for high-performance web applications.
Note : In this tutorial, we'll focus more on Rust rather than HTML/CSS. You can find the code here.
Setting Up
Before we dive into the code, make sure you have Rust and Cargo installed on your machine. You'll also need to add the following dependencies to your Cargo.toml file:
actix-web for our web server and routing.
rusqlite for SQLite database interactions.
rand for generating unique tokens for each paste.
actix-files for serving static files.
serde for serializing and deserializing data.
[dependencies]
actix-web = "3.0"
rusqlite = { version = "0.29.0", features = ["bundled"] }
rand = "0.8"
actix-files = "0.5"
serde = { version = "1.0", features = ["derive"] }
Main.rs
The main function initializes our SQLite database and sets up our web server with Actix-web. It maps our routes to their handlers and starts the server, listening for requests on localhost:8080.
When a user submits a paste, a unique token is generated using the rand crate. This token acts as the identifier for the paste, stored alongside the paste content in our SQLite database.
Retrieving a paste is straightforward; we query the database for the content associated with the token provided in the URL. If found, we display it; otherwise, we show a "Paste not found" message.
Let's break down the key parts:
AppState : This struct holds our application state, in this case, a connection to the SQLite database wrapped in a Mutex for safe access across threads.
struct AppState {
db: Mutex<Connection>,
}
Routes and Handlers : We define several routes and their corresponding handlers:
The / route serves our index.html file, presenting users with a simple form to submit their pastes.
The /submit route handles form submissions, generating a unique token for each paste, storing it in the database, and redirecting the user to their paste's URL.
The /paste/{token} route retrieves and displays the content associated with a given token.
async fn index() -> impl Responder { // sends the response body for index.html
HttpResponse::Ok().body(include_str!("index.html"))
}
async fn submit(content: web::Form<FormData>, data: web::Data<AppState>) -> impl Responder { // it takes some content
.
.
.
.
}
async fn get_paste(token: web::Path<String>, data: web::Data<AppState>) -> impl Responder { // this function helps to retrieve the value someone is sending to us
.
.
.
.
}
Diving Into the Code
Imports : These are the necessary imports for our project:
actix_web: Provides tools and utilities for building web applications with Actix.
rusqlite: Allows interaction with SQLite databases.
rand: Provides random number generation capabilities.
std::sync::Mutex: Provides mutual exclusion for accessing shared resources across threads.
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use rusqlite::{params, Connection};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use std::sync::Mutex;
use actix_files::NamedFile;
index Function : This will be an asynchronous function. It handles requests to the root URL ("/") and returns an HTTP response with the content of the index.html file.
submit Function : This is also an asynchronous function that handles POST requests to the "/submit" endpoint. It receives form data containing the content to be submitted. It generates a random token, inserts the content and token into the SQLite database, and returns a redirect response to the URL containing the generated token.
async fn submit(content: web::Form<FormData>, data: web::Data<AppState>) -> impl Responder {
// Generating a random token
let token: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(10)
.map(char::from)
.collect();
let conn = data.db.lock().unwrap();
conn.execute(
"INSERT INTO pastes (token, content) VALUES (?, ?)",
params![&token, &content.content],
).expect("Failed to insert into database");
HttpResponse::SeeOther()
.header("Location", format!("/paste/{}", token))
.finish()
}
get_paste Function : This is also an asynchronous function that handles GET requests to the "/paste/{token}" endpoint. It receives a token as a path parameter, retrieves the content associated with the token from the SQLite database, and returns an HTTP response with the retrieved content.
async fn get_paste(token: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
let conn = data.db.lock().unwrap();
let content = conn
.query_row(
"SELECT content FROM pastes WHERE token = ?",
params![token.to_string()],
|row| row.get::<_, String>(0),
)
.unwrap_or_else(|_| "Paste not found".to_string());
HttpResponse::Ok().body(format!("<pre>{}</pre>", content))
}
FormData Struct : Let's define a struct to represent form data received from the client. It will include a single field content to store the submitted content.
main Function : Finally! This is the entry point of the program. It performs the following tasks:
Opens or creates the SQLite database file named "pastes.db".
Creates the "pastes" table if it does not exist, with columns for token (primary key) and content.
Defines the application state with the database connection.
Configures the Actix web server routes, including serving static files ("/style.css"), handling index requests ("/"), submitting form data ("/submit"), and retrieving paste content ("/paste/{token}").
Binds the server to the address "127.0.0.1:8080" and starts the server.
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let db = Connection::open("pastes.db").expect("Failed to open database");
db.execute(
"CREATE TABLE IF NOT EXISTS pastes (token TEXT PRIMARY KEY, content TEXT)",
params![],
)
.expect("Failed to create table");
let app_state = web::Data::new(AppState {
db: Mutex::new(db),
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.service(web::resource("/style.css").to(|| {
async { NamedFile::open("src/style.css") }
}))
.route("/", web::get().to(index))
.route("/submit", web::post().to(submit))
.route("/paste/{token}", web::get().to(get_paste))
})
.bind("127.0.0.1:8080")? // Binding the server to the specified address
.run()
.await // Starting the server and awaiting its completion
}
That's it folks! The Rust part is done :)
I'd suggest at the same time you're building the main.rs to have a look at the HTML and CSS files too.
Run the Program
To run the program, hit cargo run , then open your browser at localhost:8080, you should see your page! Add some text or code, click submit, and then copy the unique URL. That's it!
If it wasn't on localhost but on a server you could share it with your friends and have a truly unique way to talk to each other :)
Imports: The code imports necessary modules and traits from Actix, Rusqlite, and other libraries to handle HTTP requests, database operations, and generate random tokens.
Data Structures: Defines a FormData struct to represent form data received from the client and an AppState struct to hold the database connection.
Route Handlers:
index: Handles GET requests to the root URL ("/") and returns the HTML content of the index page.
submit: Handles POST requests to "/submit" and inserts the submitted content into the database with a randomly generated token.
get_paste: Handles GET requests to "/paste/{token}" and retrieves the content associated with the provided token from the database.
Database Operations:
Opens or creates a SQLite database file named "pastes.db".
Creates a table named "pastes" if it does not exist, with columns for token (primary key) and content.
Congratulations! 🙌 You've just built your own PasteBin clone using Rust. This project not only gave you a practical introduction to Rust and web development but also demonstrated how Rust's safety features and performance make it an excellent choice for building web applications.
Through this tutorial, we've learned how to set up a Rust project, use external crates, handle web requests, interact with a database, and dynamically generate and serve content.
I encourage you to experiment with your PasteBin clone, perhaps by adding new features such as syntax highlighting or user authentication. The possibilities are endless, and with Rust, you're well-equipped to tackle them.
Happy coding. 🦀
👋 Hello, I'm Eleftheria, Community Manager, developer, public speaker, and content creator.