
Введение
В этой статье мы создадим полнофункциональный сервис краткосрочных ссылок на языке Rust. Это веб-приложение, которое принимает длинные URL и возвращает короткие ссылки с ограниченным сроком действия.
Шаг 1: Подготовка системы
1.1 Обновление системы
Откройте терминал и выполните обновление системы:
sudo apt update
sudo apt upgrade -y
1.2 Установка необходимых зависимостей
Установим все необходимые пакеты:
sudo apt install build-essential libssl-dev pkg-config sqlite3 curl -y
Объяснение:
build-essential— содержит компиляторы и инструменты сборкиlibssl-dev— библиотеки для SSL/TLSpkg-config— инструмент для определения параметров компиляцииsqlite3— легковесная встраиваемая база данныхcurl— утилита для выполнения HTTP-запросов
Шаг 2: Установка Rust
2.1 Установка Rust и Cargo
Выполните команду для установки Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
При появлении запроса нажмите Enter для выбора стандартной установки.
2.2 Настройка окружения
Загрузите переменные окружения:
source ~/.cargo/env
2.3 Проверка установки
Проверьте, что Rust установлен правильно:
rustc --version
cargo --version
Вы должны увидеть версии компилятора и пакетного менеджера.
Шаг 3: Создание проекта
3.1 Создание нового проекта
Создайте новый проект на Rust:
cargo new short_url_service
cd short_url_service
3.2 Проверка структуры проекта
Посмотрите, что создалось:
ls -la
Вы должны увидеть:
Cargo.toml— файл конфигурации проектаsrc/— директория с исходным кодомsrc/main.rs— главный файл программы
Шаг 4: Настройка зависимостей
4.1 Редактирование Cargo.toml
Откройте файл Cargo.toml в текстовом редакторе:
nano Cargo.toml
Замените его содержимое следующим текстом:
[package]
name = "short_url_service"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7", features = ["macros"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] }
uuid = { version = "1.0", features = ["v4", "fast-rng", "macro-diagnostics"] }
chrono = { version = "0.4", features = ["serde"] }
tower-http = { version = "0.5", features = ["cors", "fs"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
rand = "0.8"
dotenv = "0.15"
4.2 Установка sqlx-cli (инструмент для работы с базой данных)
Установите инструмент для работы с SQLX:
cargo install sqlx-cli --no-default-features --features sqlite
Шаг 5: Создание структуры данных
5.1 Создание файла моделей
Создайте файл для определения структур данных:
nano src/models.rs
Добавьте следующий код:
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
#[derive(Deserialize)]
pub struct CreateUrlRequest {
pub url: String,
#[serde(default = "default_ttl")]
pub ttl_hours: i64,
}
fn default_ttl() -> i64 {
24 // По умолчанию срок действия 24 часа
}
#[derive(Serialize)]
pub struct Url {
pub id: i64,
pub short_code: String,
pub original_url: String,
pub created_at: DateTime,
pub expires_at: DateTime,
}
Шаг 6: Создание работы с базой данных
6.1 Создание файла работы с базой
nano src/database.rs
Добавьте следующий код:
use sqlx::{SqlitePool, Row};
use crate::models::{Url, CreateUrlRequest};
use chrono::{Utc, Duration};
use uuid::Uuid;
#[derive(Clone)]
pub struct Database {
pub pool: SqlitePool,
}
impl Database {
pub async fn new(database_url: &str) -> Result {
let pool = SqlitePool::connect(database_url).await?;
// Создаем таблицы если их нет
sqlx::query(
"CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
short_code TEXT UNIQUE NOT NULL,
original_url TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL
)"
).execute(&pool).await?;
Ok(Database { pool })
}
pub async fn create_url(&self, request: CreateUrlRequest) -> Result {
let short_code = self.generate_unique_code().await?;
let now = Utc::now();
let expires_at = now + Duration::hours(request.ttl_hours);
sqlx::query("INSERT INTO urls (short_code, original_url, expires_at) VALUES (?, ?, ?)")
.bind(&short_code)
.bind(&request.url)
.bind(expires_at)
.execute(&self.pool)
.await?;
let row = sqlx::query("SELECT id, short_code, original_url, created_at, expires_at FROM urls WHERE short_code = ?")
.bind(&short_code)
.fetch_one(&self.pool)
.await?;
let url = Url {
id: row.get("id"),
short_code: row.get("short_code"),
original_url: row.get("original_url"),
created_at: row.get("created_at"),
expires_at: row.get("expires_at"),
};
Ok(url)
}
pub async fn get_url_by_code(&self, code: &str) -> Result, sqlx::Error> {
let row = sqlx::query("SELECT id, short_code, original_url, created_at, expires_at FROM urls WHERE short_code = ? AND expires_at > ?")
.bind(code)
.bind(Utc::now())
.fetch_optional(&self.pool)
.await?;
if let Some(row) = row {
let url = Url {
id: row.get("id"),
short_code: row.get("short_code"),
original_url: row.get("original_url"),
created_at: row.get("created_at"),
expires_at: row.get("expires_at"),
};
Ok(Some(url))
} else {
Ok(None)
}
}
pub async fn delete_expired_urls(&self) -> Result {
let result = sqlx::query("DELETE FROM urls WHERE expires_at <= ?")
.bind(Utc::now())
.execute(&self.pool)
.await?;
Ok(result.rows_affected())
}
async fn generate_unique_code(&self) -> Result {
loop {
let code = Uuid::new_v4().to_string()[..8].to_uppercase();
let row = sqlx::query("SELECT 1 as count FROM urls WHERE short_code = ?")
.bind(&code)
.fetch_optional(&self.pool)
.await?;
if row.is_none() {
return Ok(code);
}
}
}
}
Шаг 7: Создание обработчиков HTTP-запросов
7.1 Создание файла обработчиков
nano src/handlers.rs
Добавьте следующий код:
use axum::{
extract::{Path, State, Json},
http::StatusCode,
response::{Html, Redirect},
};
use serde_json::json;
use crate::database::Database;
use crate::models::{CreateUrlRequest, Url};
pub async fn index() -> Html<&'static str> {
Html(include_str!("../templates/index.html"))
}
pub async fn create_short_url(
State(db): State,
Json(payload): Json,
) -> Result, StatusCode> {
if !is_valid_url(&payload.url) {
return Err(StatusCode::BAD_REQUEST);
}
match db.create_url(payload).await {
Ok(url) => Ok(Json(json!({
"short_url": format!("/{}", url.short_code),
"expires_at": url.expires_at,
"message": "URL created successfully"
}))),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub async fn redirect_to_original(
Path(code): Path,
State(db): State,
) -> Result {
match db.get_url_by_code(&code).await {
Ok(Some(url_obj)) => Ok(Redirect::permanent(&url_obj.original_url)),
Ok(None) => Err(StatusCode::NOT_FOUND),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub async fn health_check() -> Json {
Json(json!({"status": "ok"}))
}
fn is_valid_url(url: &str) -> bool {
url.starts_with("http://") || url.starts_with("https://")
}
Шаг 8: Создание HTML шаблона
8.1 Создание директории для шаблонов
mkdir templates
8.2 Создание HTML файла
nano templates/index.html
Добавьте следующий HTML код:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Сервис краткосрочных ссылок</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="url"], input[type="number"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
box-sizing: border-box;
}
button {
background-color: #007bff;
color: white;
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #0056b3;
}
.result {
margin-top: 20px;
padding: 15px;
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 5px;
display: none;
}
.error {
background-color: #f8d7da;
border-color: #f5c6cb;
}
.input-row {
display: flex;
gap: 10px;
}
.input-row > div {
flex: 1;
}
</style>
</head>
<body>
<div class="container">
<h1>Создание краткосрочной ссылки</h1>
<form id="urlForm">
<div class="input-row">
<div class="form-group">
<label for="url">Длинная ссылка:</label>
<input type="url" id="url" name="url" required placeholder="https://example.com">
</div>
<div class="form-group">
<label for="ttl">Срок действия (часы):</label>
<input type="number" id="ttl" name="ttl" min="1" max="168" value="24" required>
</div>
</div>
<button type="submit">Создать короткую ссылку</button>
</form>
<div id="result" class="result"></div>
</div>
<script>
document.getElementById('urlForm').addEventListener('submit', async (e) => {
e.preventDefault();
const url = document.getElementById('url').value;
const ttl = parseInt(document.getElementById('ttl').value) || 24;
try {
const response = await fetch('/api/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: url,
ttl_hours: ttl
})
});
const data = await response.json();
const resultDiv = document.getElementById('result');
if (response.ok) {
resultDiv.innerHTML = `
<strong>Короткая ссылка создана!</strong><br>
<a href="${data.short_url}" target="_blank">${window.location.origin}${data.short_url}</a><br>
Срок действия: ${new Date(data.expires_at).toLocaleString('ru-RU')}
`;
resultDiv.className = 'result';
resultDiv.style.display = 'block';
} else {
resultDiv.innerHTML = `<strong>Ошибка:</strong> ${data.message || 'Не удалось создать ссылку'}`;
resultDiv.className = 'result error';
resultDiv.style.display = 'block';
}
} catch (error) {
console.error('Error:', error);
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = '<strong>Ошибка:</strong> Произошла ошибка при создании ссылки';
resultDiv.className = 'result error';
resultDiv.style.display = 'block';
}
});
</script>
</body>
</html>
Шаг 9: Редактирование основного файла
9.1 Замена содержимого main.rs
nano src/main.rs
Замените содержимое на следующий код:
use axum::{
extract::State,
http::StatusCode,
response::Html,
routing::{get, post},
Router,
};
use tracing_subscriber;
use dotenv::dotenv;
mod models;
mod database;
mod handlers;
use database::Database;
use handlers::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Загрузим переменные окружения
dotenv().ok();
// Настройка логирования
tracing_subscriber::fmt::init();
// Создание пула подключения к базе данных
let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite:urls.db".to_string());
let db = Database::new(&database_url).await?;
// Запуск фоновой задачи для очистки истекших ссылок
start_cleanup_task(db.clone());
// Создание роутера
let app = Router::new()
.route("/", get(index))
.route("/api/create", post(create_short_url))
.route("/health", get(health_check))
.route("/:code", get(redirect_to_original))
.with_state(db);
// Запуск сервера
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://0.0.0.0:3000");
axum::serve(listener, app).await.unwrap();
Ok(())
}
fn start_cleanup_task(db: Database) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(600)); // каждые 10 минут
loop {
interval.tick().await;
match db.delete_expired_urls().await {
Ok(count) => println!("Очищено {} истекших ссылок", count),
Err(e) => eprintln!("Ошибка при очистке: {}", e),
}
}
});
}
Шаг 10: Создание файла переменных окружения
10.1 Создание .env файла
echo "DATABASE_URL=sqlite:urls.db" > .env
Шаг 11: Создание файла базы данных
11.1 Создание пустой базы данных
Перед запуском приложения создадим файл базы данных:
touch urls.db
chmod 666 urls.db
Шаг 12: Сборка и запуск приложения
12.1 Сборка проекта
cargo build
Если сборка проходит успешно (без ошибок), переходим к следующему шагу.
12.2 Запуск приложения
cargo run
Вы должны увидеть сообщение: Server running on http://0.0.0.0:3000
Шаг 13: Тестирование работы
13.1 В новом терминале проверьте здоровье сервиса
curl http://localhost:3000/health
Должен вернуться JSON: {"status":"ok"}
13.2 Создайте тестовую короткую ссылку
curl -X POST http://localhost:3000/api/create \
-H "Content-Type: application/json" \
-d '{"url": "https://www.rust-lang.org", "ttl_hours": 24}'
13.3 Проверьте, что файл базы данных создался
ls -la urls.db
Шаг 14: Использование сервиса
14.1 Через веб-интерфейс
Откройте в браузере: http://ваш-сервер-ип:3000




14.2 Через API
# Создание короткой ссылки
curl -X POST http://ваш-сервер-ип:3000/api/create \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "ttl_hours": 12}'
# Проверка состояния
curl http://ваш-сервер-ип:3000/health

Шаг 15: Запуск в фоне (опционально)
15.1 Запуск в фоне
nohup cargo run > app.log 2>&1 &
15.2 Проверка запущенного процесса
jobs
Заключение
Поздравляем! Вы успешно создали сервис краткосрочных ссылок на Rust. Приложение:
- Принимает длинные URL и создает короткие ссылки
- Автоматически удаляет истекшие ссылки
- Имеет веб-интерфейс для удобства использования
- Обеспечивает высокую производительность благодаря Rust
- Использует SQLite для хранения данных
Этот проект отлично подходит для изучения:
- Языка программирования Rust
- Веб-разработки с использованием Axum
- Работы с базами данных через SQLX
- Асинхронного программирования в Rust