Найти в Дзене
Один Rust не п...Rust

Rust и Amazon S3

Оглавление
GitHub - nicktretyakov/s3-ml-uploader

Зачем Вам это уметь? :

🔥 Написать приложение которое:

✅ Загружает файлы в S3 (AWS) и MinIO (альтернативное S3-хранилище).
✅ Поддерживает асинхронную загрузку файлов с высокой производительностью.
✅ Оптимизировано с tokio и rayon для конкурентного выполнения.
✅ Работает с AWS SDK и прямыми HTTP-запросами для максимальной гибкости.
✅ Реализует кастомную систему подписей для прямых S3-запросов.

📜 Зависимости (Cargo.toml)

[dependencies]

aws-sdk-s3 = "1.9.0"

aws-config = "1.1.0"

tokio = { version = "1", features = ["full"] }

s3 = "0.34"

reqwest = { version = "0.11", features = ["json", "stream"] }

rayon = "1.8"

futures = "0.3"

dotenv = "0.15"

chrono = "0.4"

base64 = "0.21"

hmac = "0.12"

sha2 = "0.10"

📌 Код приложения

use aws_config::meta::region::RegionProviderChain;

use aws_sdk_s3::{Client, Config, Region};

use aws_types::credentials::Credentials;

use s3::{bucket::Bucket, creds::Credentials as S3Credentials, region::Region as S3Region};

use reqwest::{Client as ReqwestClient, Method};

use std::{env, fs::File, io::Read, sync::Arc};

use tokio::task;

use rayon::prelude::*;

use hmac::{Hmac, Mac};

use sha2::Sha256;

use base64::Engine;

use base64::engine::general_purpose::STANDARD as Base64Encoder;

use chrono::Utc;

use dotenv::dotenv;

/// AWS S3 клиент

async fn create_aws_client() -> Client {

let region_provider = RegionProviderChain::default_provider().or_else("us-east-1");

let region = Region::new(region_provider.region().await.unwrap().to_string());

let config = aws_config::from_env().region(region).load().await;

Client::new(&config)

}

/// S3 совместимый клиент (например, MinIO)

fn create_s3_client() -> Bucket {

let credentials = S3Credentials::new(

Some(&env::var("S3_ACCESS_KEY").unwrap()),

Some(&env::var("S3_SECRET_KEY").unwrap()),

None,

None,

None,

)

.unwrap();

let region = S3Region::Custom {

region: "us-east-1".to_string(),

endpoint: env::var("S3_ENDPOINT").unwrap(),

};

Bucket::new(&env::var("S3_BUCKET").unwrap(), region, credentials).unwrap()

}

/// Прямая загрузка файла через HTTP-запрос с подписью AWS V4

async fn upload_via_http(file_path: &str, bucket: &str, key: &str) -> Result<(), reqwest::Error> {

let client = ReqwestClient::new();

let mut file = File::open(file_path).unwrap();

let mut buffer = Vec::new();

file.read_to_end(&mut buffer).unwrap();

let access_key = env::var("AWS_ACCESS_KEY").unwrap();

let secret_key = env::var("AWS_SECRET_KEY").unwrap();

let region = "us-east-1";

let host = format!("{}.s3.amazonaws.com", bucket);

let url = format!("https://{}/{}", host, key);

let date = Utc::now().format("%Y%m%dT%H%M%SZ").to_string();

let scope = format!("{}/{}/s3/aws4_request", &date[..8], region);

let string_to_sign = format!(

"AWS4-HMAC-SHA256\n{}\n{}\n{}",

date,

scope,

hex::encode(Sha256::digest(buffer.as_slice()))

);

let mut hmac = Hmac::<Sha256>::new_from_slice(format!("AWS4{}", secret_key).as_bytes()).unwrap();

hmac.update(date[..8].as_bytes());

let signing_key = hmac.finalize().into_bytes();

let signature = Base64Encoder.encode(signing_key);

let authorization_header = format!(

"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders=host;x-amz-date, Signature={}",

access_key, scope, signature

);

let res = client

.request(Method::PUT, &url)

.header("Authorization", authorization_header)

.header("x-amz-date", date)

.header("Content-Length", buffer.len())

.body(buffer)

.send()

.await?;

println!("Uploaded via HTTP: {}", res.status());

Ok(())

}

/// Загрузка файла в S3 с `aws-sdk-s3`

async fn upload_to_aws_s3(client: Arc<Client>, file_path: &str, bucket: &str, key: &str) {

let mut file = File::open(file_path).unwrap();

let mut buffer = Vec::new();

file.read_to_end(&mut buffer).unwrap();

client

.put_object()

.bucket(bucket)

.key(key)

.body(buffer.into())

.send()

.await

.unwrap();

println!("Uploaded to AWS S3: {}", key);

}

/// Загрузка файла в MinIO

async fn upload_to_minio(bucket: &Bucket, file_path: &str, key: &str) {

let mut file = File::open(file_path).unwrap();

let mut buffer = Vec::new();

file.read_to_end(&mut buffer).unwrap();

bucket.put_object(key, &buffer).await.unwrap();

println!("Uploaded to MinIO: {}", key);

}

#[tokio::main]

async fn main() {

dotenv().ok();

let aws_client = Arc::new(create_aws_client().await);

let minio_bucket = create_s3_client();

let files = vec!["file1.txt", "file2.txt", "file3.txt"];

files.par_iter().for_each(|file| {

let aws_client = Arc::clone(&aws_client);

let minio_bucket = minio_bucket.clone();

let file = file.to_string();

task::spawn(async move {

upload_to_aws_s3(aws_client, &file, "aws-bucket", &file).await;

});

task::spawn(async move {

upload_to_minio(&minio_bucket, &file, &file).await;

});

task::spawn(async move {

upload_via_http(&file, "aws-bucket", &file).await.unwrap();

});

});

}

Установите переменные в .env:

AWS_ACCESS_KEY=your-aws-key

AWS_SECRET_KEY=your-aws-secret

S3_ACCESS_KEY=your-minio-key

S3_SECRET_KEY=your-minio-secret

S3_ENDPOINT=http://localhost:9000

S3_BUCKET=minio-bucket

Для чего нужна данная статья? :

Выбрать компромиссы между:

1. Официальный AWS SDK для Rust

Amazon предлагает официальный SDK для Rust – aws-sdk-s3, который поддерживает все возможности S3, включая загрузку, скачивание, управление объектами и ведение версий.

Использование:

use aws_sdk_s3::{Client, Config, Region};

use aws_types::credentials::Credentials;

use tokio;

#[tokio::main]

async fn main() -> Result<(), aws_sdk_s3::Error> {

let config = Config::builder()

.region(Region::new("us-east-1"))

.credentials_provider(Credentials::from_keys(

"ACCESS_KEY",

"SECRET_KEY",

None,

))

.build();

let client = Client::from_conf(config);

let resp = client.list_buckets().send().await?;

for bucket in resp.buckets().unwrap_or_default() {

println!("Bucket: {}", bucket.name().unwrap());

}

Ok(())

}

Плюсы:

  • Официальная поддержка от Amazon.
  • Полная совместимость со всеми возможностями S3.

Минусы:

  • SDK пока ещё в бета-версии.
  • Может быть избыточным для простых задач.

2. S3-Compatible API (minio, wasabi, etc.) через s3

Если нужен более лёгкий вариант или совместимость с S3-совместимыми сервисами (MinIO, Wasabi, DigitalOcean Spaces), можно использовать s3.

Использование:

use s3::bucket::Bucket;

use s3::credentials::Credentials;

use s3::region::Region;

#[tokio::main]

async fn main() {

let credentials = Credentials::new(Some("ACCESS_KEY"), Some("SECRET_KEY"), None, None, None).unwrap();

let region = Region::Custom {

region: "us-east-1".to_string(),

endpoint: "https://s3.amazonaws.com".to_string(),

};

let bucket = Bucket::new("my-bucket", region, credentials).unwrap();

let (data, code) = bucket.get_object("/path/to/file.txt").await.unwrap();

println!("Response Code: {}, Data: {:?}", code, String::from_utf8_lossy(&data));

}

Плюсы:

  • Простота в использовании.
  • Лёгкость и поддержка альтернативных S3-хранилищ.

Минусы:

  • Меньше возможностей, чем у официального SDK.
  • Требуется вручную указывать URL для кастомных хранилищ.

3. Прямой HTTP-запрос через reqwest

Если нужна максимальная кастомизация без зависимостей на сторонние библиотеки, можно делать запросы напрямую через reqwest.

Использование:

use reqwest::Client;

use std::env;

#[tokio::main]

async fn main() -> Result<(), reqwest::Error> {

let client = Client::new();

let url = "https://s3.amazonaws.com/my-bucket/my-file.txt";

let response = client.get(url)

.header("Authorization", "Bearer YOUR_TOKEN")

.send()

.await?;

let body = response.text().await?;

println!("{}", body);

Ok(())

}

Плюсы:

  • Полный контроль над запросами.
  • Можно использовать любые хранилища без привязки к AWS SDK.

Минусы:

  • Нужно вручную подписывать запросы (AWS использует сложный механизм подписи AWS4-HMAC-SHA256).
  • Неудобно для работы с большими файлами.

4. Через rusoto_s3 (Устарело, но рабочий)

Библиотека rusoto_s3 была популярным вариантом, но устарела в пользу официального AWS SDK.

use rusoto_core::{Region, credential::StaticProvider};

use rusoto_s3::{S3, S3Client, ListBucketsRequest};

#[tokio::main]

async fn main() {

let credentials = StaticProvider::new_minimal("ACCESS_KEY".to_string(), "SECRET_KEY".to_string());

let client = S3Client::new_with(rusoto_core::request::HttpClient::new().unwrap(), credentials, Region::UsEast1);

let result = client.list_buckets(ListBucketsRequest {}).await.unwrap();

for bucket in result.buckets.unwrap() {

println!("{:?}", bucket.name.unwrap());

}

}

Плюсы:

  • Поддерживает старый код.

Минусы:

  • Библиотека устарела и больше не поддерживается.

5. Через FUSE (S3FS)

Если хочется монтировать S3 как файловую систему, можно использовать s3fs через libfuse.

s3fs my-bucket-name /mnt/s3 -o use_cache=/tmp,iam_role=auto

Плюсы:

  • Доступ к S3 как к обычной папке в ОС.
  • Работает с любым языком, включая Rust.

Минусы:

  • Сложнее настраивать на сервере.
  • Производительность ниже, чем у API.

а так же:

Узнать как загрузить файл в бакет S3 и затем скачать его обратно

Добавьте библиотеку AWS SDK для Rust в ваш файл Cargo.toml:

[dependencies]

rusoto_core = "0.48.0"

rusoto_s3 = "0.48.0"

tokio = { version = "1", features = ["full"] }

Создайте Rust-приложение и импортируйте необходимые крейты:

use std::fs::File;

use std::io::Read;

use rusoto_core::Region;

use rusoto_s3::{CreateBucketRequest, ListBucketsRequest,

PutObjectRequest, S3, S3Client};

use tokio;

Инициализируйте клиент S3 и укажите регион:

#[tokio::main]

async fn main() -> Result<(), Box<dyn std::error::Error>> {

let region = Region::default();

let s3_client = S3Client::new(region);

Создайте бакет в Amazon S3 (если он еще не существует) и задайте имя бакета:

let bucket_name = "bucket-name";

let create_bucket_request = CreateBucketRequest {

bucket: bucket_name.to_string(),

..Default::default()

};
s3_client.create_bucket(create_bucket_request).await?;

Загрузите файл в S3. Замените "path/to/your/local/file" на путь к вашему локальному файлу и "key-in-s3" на ключ (имя) файла в S3:

let mut file = File::open(file_path)?;

let mut buffer = Vec::new();

file.read_to_end(&mut buffer)?;

let put_object_request = PutObjectRequest {

bucket: bucket_name.to_string(),

key: s3_key.to_string(),

body: Some(buffer.into()),

..Default::default()

};

s3_client.put_object(put_object_request).await?;

Скачайте файл из S3. Замените "downloaded-file" на путь и имя файла, в который будет сохранена загруженная копия файла:

let downloaded_file = "downloaded-file";

let get_object_request = rusoto_s3::GetObjectRequest {

bucket: bucket_name.to_string(),

key: s3_key.to_string(),

..Default::default()

};

let result = s3_client.get_object(get_object_request).await?;

let mut stream = result.body.unwrap().into_async_read();

let mut downloaded_data = Vec::new();

stream.read_to_end(&mut downloaded_data).await?;

let mut downloaded_file = File::create(downloaded_file)?;

downloaded_file.write_all(&downloaded_data)?;

Научиться использовать библиотеку reqwest в Rust для взаимодействия с Amazon S3 через RESTful API

Добавьте библиотеку reqwest в ваш файл Cargo.toml:

[dependencies]

reqwest = "0.11"

Создайте Rust-приложение и импортируйте необходимые крейты:

use reqwest;

use std::fs::File;

use std::io::Read;

Создайте функцию, которая отправит HTTP-запрос для загрузки файла в

Amazon S3. Замените your-access-key, your-secret-key, your-bucket-name,

key-in-s3, и path/to/your/local/file на реальные значения:

async fn upload_to_s3() -> Result<(), reqwest::Error> {

let access_key = "your-access-key";

let secret_key = "your-secret-key";

let bucket_name = "your-bucket-name";

let s3_key = "key-in-s3";

let file_path = "path/to/your/local/file";

// Чтение файла в память

let mut file = File::open(file_path)?;

let mut buffer = Vec::new();

file.read_to_end(&mut buffer)?;

// Формируем URL и подписываем запрос

let url = format!("https://{}.s3.amazonaws.com/{}", bucket_name, s3_key);

let date = chrono::Utc::now().to_rfc2822();

let signature = format!(

"PUT\n\n\n{}\n/{}",

date,

bucket_name

);
let signature = aws_signature_v4::signature(

"s3",

secret_key,

date,

&signature,

&buffer,
);

let client = reqwest::Client::new();

let response = client

.put(&url)

.header("Date", date)

.header("Authorization", format!("AWS {}:{}", access_key, signature))

.body(buffer)

.send()

.await?;

if response.status().is_success() {

println!("Загрузка файла в S3 прошла успешно.");

} else {

println!("Ошибка при загрузке файла в S3: {:?}", response.status());

}

Ok(())

}
Создайте функцию, которая отправит HTTP-запрос для скачивания файла из Amazon S3. Замените
your-access-key, your-secret-key, your-bucket-name, key-in-s3, и downloaded-file на реальные значения:

async fn download_from_s3() -> Result<(), reqwest::Error> {

let access_key = "your-access-key";

let secret_key = "your-secret-key";

let bucket_name = "your-bucket-name";

let s3_key = "key-in-s3";

let downloaded_file = "downloaded-file";

// Формируем URL и подписываем запрос

let url = format!("https://{}.s3.amazonaws.com/{}", bucket_name, s3_key);

let date = chrono::Utc::now().to_rfc2822();

let signature = format!(

"GET\n\n\n{}\n/{}",

date,

bucket_name

);

let signature = aws_signature_v4::signature(

"s3",

secret_key,

date,

&signature,

&vec![],

);

let client = reqwest::Client::new();

let mut response = client

.get(&url)

.header("Date", date)

.header("Authorization", format!("AWS {}:{}", access_key, signature))

.send()

.await?;

if response.status().is_success() {

let mut output_file = File::create(downloaded_file)?;

response.copy_to(&mut output_file)?;

println!("Файл успешно скачан из S3 и сохранен как {:?}",

downloaded_file);

} else {

println!("Ошибка при скачивании файла из S3: {:?}",

response.status());

}
Ok(())

}
В
main функции вызовите функции для загрузки и скачивания файла:

#[tokio::main]

async fn main() -> Result<(), Box<dyn std::error::Error>> {

upload_to_s3().await?;

download_from_s3().await?;

Ok(())

}

Научится использовать крейт rusoto - удобную Rust-обертку для AWS SDK

Добавьте библиотеку rusoto в ваш файл Cargo.toml:

[dependencies]

rusoto_core = "0.48"

rusoto_s3 = "0.48"
tokio = { version = "1", features = ["full"] }

Создайте Rust-приложение и импортируйте необходимые крейты:

use rusoto_core::Region;

use rusoto_s3::{S3, S3Client, PutObjectRequest, GetObjectRequest};

use tokio;
Инициализируйте клиент S3 и укажите регион:

#[tokio::main]

async fn main() -> Result<(), Box<dyn std::error::Error>> {

let region = Region::default();
let s3_client = S3Client::new(region);

Загрузите файл в S3. Замените "your-bucket-name", "key-in-s3", и "path/to/

your/local/file" на реальные значения:

let bucket_name = "your-bucket-name";

let s3_key = "key-in-s3";
let file_path = "path/to/your/local/file";

let mut file = tokio::fs::File::open(file_path).await?;

let mut buffer = Vec::new();

file.read_to_end(&mut buffer).await?;

let put_object_request = PutObjectRequest {

bucket: bucket_name.to_string(),

key: s3_key.to_string(),

body: Some(buffer.into()),

..Default::default()

};

s3_client.put_object(put_object_request).await?;

println!("Файл успешно загружен в S3.");

Скачайте файл из S3. Замените "your-bucket-name", "key-in-s3" и "downloaded-file" на реальные значения:

let bucket_name = "your-bucket-name";

let s3_key = "key-in-s3";

let downloaded_file = "downloaded-file";

let get_object_request = GetObjectRequest {

bucket: bucket_name.to_string(),

key: s3_key.to_string(),

..Default::default()

};

let result = s3_client.get_object(get_object_request).await?;

let body = result.body.unwrap();

let mut reader = body.into_async_read();

let mut downloaded_data = Vec::new();

while let Some(chunk) = reader.read_buf(&mut downloaded_data).await?

{

// Продолжайте чтение пока не достигнут конец файла.

}
let mut downloaded_file = tokio::fs::File::create(downloaded_file).await?;

downloaded_file.write_all(&downloadloaded_data).await?;

println!("Файл успешно скачан из S3 и сохранен как {:?}",

downloaded_file);

Ok(())

}

Научиться низкоуровневому взаимодействию с Amazon S3, используя стандартные HTTP-запросы и ответы через HTTPS

Добавьте библиотеку hyper и другие зависимости в ваш файл Cargo.toml:

[dependencies]

hyper = "0.14"

tokio = { version = "1", features = ["full"] }

Создайте Rust-приложение и импортируйте необходимые крейты:

use hyper::body::HttpBody as _;

use hyper::Client;

use hyper::Request;
use hyper::Method;

use hyper::Body;

use hyper_tls::HttpsConnector;
use std::error::Error;

use tokio;

Определите функцию для подписи HTTP-запроса с помощью AWS Signature

Version 4. Замените "your-access-key", "your-secret-key", "your-bucket-

name", "your-s3-key" на ваши реальные учетные данные и путь в S3:

fn sign_s3_request(

method: Method,

bucket: &str,

s3_key: &str,

access_key: &str,

secret_key: &str,

) -> Request<Body> {

let host = format!("{}.s3.amazonaws.com", bucket);

let date = chrono::Utc::now().format("%a, %d %b %Y %H:%M:%S

%z").to_string();

let amz_date =

chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string();

// Строка для подписи

let canonical_request = format!(

"{}\n{}\n{}\n\nhost:{}\n\nhost\nUNSIGNED-PAYLOAD",
method,

format!("/{}", s3_key),

"",

host

);

// Формирование подписи

let signed_headers = "host".to_string();

let scope = format!("{}/{}/s3/aws4_request", date.splitn(2, ' ').next().unwrap(), "us-east-1");

let string_to_sign = format!(

"AWS4-HMAC-SHA256\n{}\n{}\n{}",

date, scope,

hex::encode(sha2::Sha256::digest(canonical_request.as_bytes()))
);

let signing_key = aws_sigv4::get_signature_key(secret_key, date.splitn(2, ' ').next().unwrap(), "us-east-1", "s3");

let signature = aws_sigv4::sign_string(string_to_sign,

signing_key.as_ref());

let auth_header = format!(

"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",

access_key, scope, signed_headers, signature

);
let authorization_header = format!("Authorization: {}", auth_header);

let canonical_headers = format!("host:{}\n{}", host,

authorization_header);

let request = Request::builder()

.method(method)
.uri(format!("https://{}.s3.amazonaws.com/{}", bucket, s3_key))

.header("Host", host)

.header("X-Amz-Date", amz_date)

.header("X-Amz-Security-Token", "")

.header("Authorization", auth_header)

.body(Body::empty())

.unwrap();

request

}
Определите функцию для выполнения HTTP-запроса и обработки ответа:

async fn execute_s3_request(request: Request<Body>) -> Result<(), Box<dyn Error>> {

let https = HttpsConnector::new();

let client = Client::builder().build::<_, hyper::Body>(https);

let response = client.request(request).await?;

if response.status().is_success() {

println!("Запрос выполнен успешно. Код ответа: {}",

response.status());

} else {

println!("Ошибка при выполнении запроса. Код ответа: {}",

response.status());
}

Ok(())

}

В main функции вызовите функции для загрузки и скачивания файла:

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {

let access_key = "your-access-key";

let secret_key = "your-secret-key";

let bucket_name = "your-bucket-name";

let s3_key = "your-s3-key";

let upload_request = sign_s3_request(

Method::PUT,

bucket_name,

s3_key,

access_key,

secret_key,

);

let download_request = sign_s3_request(

Method::GET,

bucket_name,

s3_key,

access_key,

secret_key,

);

execute_s3_request(upload_request).await?;

execute_s3_request(download_request).await?;

Ok(())

}


Научиться использовать Docker контейнер с установленным AWS CLI внутри Rust-приложения

Создайте Dockerfile, который устанавливает AWS CLI и другие зависимости. Вот пример Dockerfile:

# Use an official AWS CLI image as the base image

FROM amazon/aws-cli

# Install additional dependencies if needed

# RUN apk add --no-cache <dependency>

# Set the working directory in the container

WORKDIR /app

# Copy your Rust application source code into the container

COPY . .
# Set the command to run your Rust application

CMD ["./your_rust_app"]

В вашем Rust-приложении, вы можете использовать std::process::Command для выполнения команд AWS CLI из контейнера. Например, вы можете выполнить команду для загрузки файла в S3:

use std::process::Command;

fn main() {

// Замените это на вашу команду AWS CLI

let aws_cli_command = Command::new("aws")

.arg("s3")

.arg("cp")

.arg("local-file.txt")

.arg("s3://your-bucket-name/remote-file.txt")
.output()

.expect("Failed to execute AWS CLI command");

if aws_cli_command.status.success() {

println!("File uploaded to S3 successfully.");

} else {

println!("Error uploading file to S3: {:?}",

String::from_utf8_lossy(&aws_cli_command.stderr));
}

}

Соберите и запустите ваш Docker контейнер, который содержит ваш Rust-приложение и AWS CLI. Замените "your_rust_app" на имя вашего Rust-приложения:

docker build -t my-rust-app .

docker run my-rust-app