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
  • Модульные тесты