Rust - это язык программирования с несколькими парадигмами, ориентированный на производительность и безопасность, особенно на безопасный параллелизм. Rust синтаксически похож на C ++, но обеспечивает безопасность памяти без использования сборки мусора.
В этом руководстве мы создаем API, который позволит пользователям выгружать и скачивать файлы с сервера. Окончательный исходный код можно найти на GitHub.
Мы собираемся использовать фреймворк actix-web для обработки наших REST API. Actix web - это простой, практичный и чрезвычайно быстрый веб-фреймворк для Rust.
Предпосылки
- Мы предполагаем, что вы уже установили
Rust
. Если нет, отметьте здесь. - Вам нужно немного знать о
Cargo
. Мы собираемся использовать Cargo, потому что это официальное решение для управления зависимостями дляRust
языка.
Первые шаги
Сначала вам нужно создать новый Rust
pacakge с помощью команды Cargo new
.
cargo new rest-api
Теперь, если вы перейдете в каталог rest-api
, вы должны увидеть эту структуру
rest-api ---- src ---- ---- main.rs ---- .gitignore ---- Cargo.lock ---- Cargo.toml
На этом этапе вы должны добавить actix/actix-web
и actix/actix-rt
в зависимости в Cargo.toml
. Теперь ваш Cargo.toml
будет выглядеть примерно так.
[package]
name = "rest-api"
version = "0.1.0"
authors = ["mehrdadep <[email protected]>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "2.0"
actix-rt = "1.0"
С actix
у вас есть два варианта создания новых маршрутов для вашего веб-сервера. Первый вариант - использовать функцию route()
внутри основной функции.
use actix_web::{web, App, HttpResponse, HttpServer, Responder}; async fn index() -> impl Responder { HttpResponse::Ok().body("Hey sunshine!") }
#[actix_rt::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/hi", web::get().to(index)) }) .bind("127.0.0.1:9000")? .run() .await }
Второй вариант - использовать атрибуты макросов над каждой функцией, а затем регистрировать каждую функцию как новый маршрут в основном приложении с использованием метода service()
.
use actix_web::get; #[get("/hi")] async fn index() -> impl Responder { HttpResponse::Ok().body("Hello sunshine!") }
#[actix_rt::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(index) }) .bind("127.0.0.1:9000")? .run() .await }
Какой из этих вариантов использовать полностью зависит от вас, но мы собираемся использовать первый вариант до конца этой публикации (он несколько более управляем).
Давайте что-то делать
Хватит солнечного света, давайте создадим маршрут, который действительно что-то делает. В файле main.rs
создайте новую функцию с именем upload
. Не забудьте добавить атрибут макроса actix
для обработки основной функции . Мы отправляем данные по /files/
URL. После этого мы получаем (скачиваем) файл с /files/{name}/
URL.
Мы собираемся сгруппировать наши два основных URL-адреса под префиксом /api
. Группировка URL-адресов в actix
выполняется функцией scope
.
HttpServer::new(|| { App::new() .service( web::scope("/api") .route("/files/", web::post().to(upload)) .route("/files/{name}/", web::get().to(download)), ) ) }
Теперь нам нужны еще две фиктивные функции. Один для загрузки, а другой для загрузки. Нашей загрузке также требуется диспетчер, чтобы извлечь имя файла. actix
использует web::Path
для сопоставления переменных в URL-адресе. Наши пустые функции станут примерно такими:
use actix_web::{Responder}; async fn upload() -> impl Responder { // Body of the function goes here! } async fn download(info: web::Path<(String)>) -> HttpResponse { // Body of the function goes here!) }
Но это ничего не делает (очевидно!). Теперь нам нужно добавить немного тела к нашим основным функциям (для теста добавьте фиктивную), которые возвращают ответ json
или http
, несмотря ни на что.
Чтобы использовать ответы json
и http
, мы собираемся добавить несколько ящиков в наш груз, поэтому отредактируйте свой Cargo.toml
и добавьте в него эти ящики (мы собираемся удалить некоторые из них в окончательной версии).
serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" json = "0.12" futures = "0.3.5" bytes = "0.5.6"
Теперь измените тело загрузки и скачивания, чтобы они могли делать что-то бессмысленное!
#[derive(Serialize, Deserialize)]
struct File {
name: String,
time: u64,
err: String
}
#[derive(Deserialize)]
struct Download {
name: String,
}
async fn upload() -> impl Responder {
let u = &File {
name: "dummy data".to_string(),
time: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
err: "".to_string()
};
HttpResponse::Ok().json(u)
}
async fn download(info: web::Path<Download>) -> HttpResponse {
let name = String::from(info.name.as_str());
let body = once(ok::<_, actix_web::Error>(Bytes::from(name)));
HttpResponse::Ok()
.content_type("application/json")
.streaming(body)
}
Используйте cargo run
, чтобы запустить фиктивный REST API и протестировать его с помощью curl
curl --request GET --url http://127.0.0.1:9000/api/files/DummyName/
В результате на выходе будет DummyName
. Теперь протестируйте API загрузки.
curl --request POST --url http://127.0.0.1:9000/api/files/
Результатом будет json
, содержащий unix
текущее время вместе с фиктивным именем.
{"name":"dummy data","time":1594830653, "err": ""}
Самая интересная часть
Теперь у нас есть кое-что, что работает (не так, как мы хотели!), Мы можем добавить к нему желаемый функционал. Наша загрузка api
должна принимать составные данные и сохранять их где-нибудь на жестком диске.
Как всегда, новая функциональность требует новых ящиков (хорошо, не всегда). Добавьте эти ящики в свой Cargo.toml
actix-multipart = "0.2.0" sanitize-filename = "0.2"
Для загрузки и не только
Мы должны изменить наш main.rs
файл, чтобы что-то загрузить.
. . . use std::fs; use actix_multipart::Multipart; use futures::{StreamExt, TryStreamExt}; use std::io::Write; . . . async fn upload(mut payload: Multipart) -> Result<HttpResponse, Error> { // iterate over multipart stream fs::create_dir_all(UPLOAD_PATH)?; let mut filename = "".to_string(); while let Ok(Some(mut field)) = payload.try_next().await { let content_type = field.content_disposition().unwrap(); filename = format!("{} - {}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_micros(), content_type.get_filename().unwrap(), ); let filepath = format!("{}/{}", UPLOAD_PATH, sanitize_filename::sanitize(&filename)); // File::create is blocking operation, use thread pool let mut f = web::block(|| std::fs::File::create(filepath)) .await .unwrap(); // Field in turn is stream of *Bytes* object while let Some(chunk) = field.next().await { let data = chunk.unwrap(); // filesystem operations are blocking, we have to use thread pool f = web::block(move || f.write_all(&data).map(|_| f)).await?; } } // Create a unique name for the file let res = &File { name: filename, time: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), err: "".to_string() }; Ok(HttpResponse::Ok().json(res)) }
Обратите внимание, что мы использовали фиксированный filepath
для загрузки файлов (глобально). Вы можете изменить его или даже сделать динамичным. Просто убедитесь, что путь доступен для записи.
const UPLOAD_PATH: &str = "/tmp/rest-api/upload";
К такому результату приводит загрузка файла.
{ "name": "1594832860135992 - some-pic.jpg", "time": 1594832860, "err": "" }
Скачайте их все!
Ой, мы что-то упускаем! Давайте добавим нашу последнюю функцию и загрузим файлы.
. . . use std::path::Path; . . . async fn download(info: web::Path<Download>) -> HttpResponse { let path = format!("{}/{}", UPLOAD_PATH, info.name.to_string()); if !Path::new(path.as_str()).exists() { return HttpResponse::NotFound().json(&File { name: info.name.to_string(), time: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), err: "file does not exists".to_string(), }); } let data = fs::read(path).unwrap(); HttpResponse::Ok() .header("Content-Disposition", format!("form-data; filename={}", info.name.to_string())) .body(data) }
Мы хотим знать, существует файл или нет, поэтому сначала мы проверяем наличие файла, используя UPLOAD_PATH
, объединенный с именем файла. Это вернет ошибку 404, если файл не существует, в противном случае файл будет считан в байтах с использованием функции read()
из fs
, а затем будет отправлен с этим именем.
Строить, запускать, повторять
Мы еще не закончили. Да, мы можем запустить наш сервер REST API, используя cargo run
, когда захотим, но что, если мы захотим использовать его где-нибудь еще? Мы собираемся использовать cargo build
с флагом release
, чтобы создать исполняемый файл для нашего небольшого проекта.
cargo build --release
Эта команда сгенерирует rest-api
файл в ./target/release
каталоге, который вы можете запускать где угодно и когда угодно.
Что я не рассказывал
- Использование баз данных для хранения метаданных файла
- Промежуточное программное обеспечение и средства защиты
actix
- Обработка ошибок в
actix
- Модульные тесты