Зачем Вам это уметь? :
🔥 Написать приложение которое:
✅ Загружает файлы в 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