{"id":358667,"date":"2024-05-21T00:16:42","date_gmt":"2024-05-21T00:16:42","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=358667"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=358667","title":{"rendered":"<span>\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Axum \u0441 S3-\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435\u043c<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041d\u0435\u0434\u0430\u0432\u043d\u043e \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u043f\u043e\u0434\u0440\u0443\u0436\u0438\u0442\u044c \u0432\u0435\u0431-\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u00a0<a href=\"https:\/\/crates.io\/crates\/axum\" rel=\"noopener noreferrer nofollow\">Axum<\/a>\u00a0\u0438 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443\u00a0<a href=\"https:\/\/crates.io\/crates\/rust-s3\" rel=\"noopener noreferrer nofollow\">rust-s3<\/a>. \u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u0437\u0430\u0434\u0430\u0447\u0430 \u0441\u0434\u0435\u043b\u0430\u0442\u044c 2 \u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442\u0430:<\/p>\n<ul>\n<li>\n<p>\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0444\u0430\u0439\u043b\u0430 \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0441\u0441\u044b\u043b\u043a\u0438<\/p>\n<\/li>\n<li>\n<p>\u0421\u043a\u0430\u0447\u043a\u0430 \u0444\u0430\u0439\u043b\u0430 \u0438\u0437 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435<\/p>\n<\/li>\n<\/ul>\n<p>\u0420\u0430\u0437\u0443\u043c\u0435\u0435\u0442\u0441\u044f, \u0431\u0435\u0437 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u0444\u0430\u0439\u043b\u043e\u0432 \u0438 \u0431\u0435\u0437 \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f \u0446\u0435\u043b\u0438\u043a\u043e\u043c \u0432\u0441\u0435\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0444\u0430\u0439\u043b\u0430 \u0432 \u043f\u0430\u043c\u044f\u0442\u0438.<\/p>\n<p>\u0422\u0430\u043a \u043a\u0430\u043a \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 S3 \u043d\u0443\u0436\u043d\u044b \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b (\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u043c\u0443 bucket), \u0432\u044b\u043d\u0435\u0441\u0435\u043c \u043d\u0435\u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u0443\u044e \u0440\u0430\u0431\u043e\u0442\u0443 \u0432 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443\u00a0<code>UploadService<\/code>:<\/p>\n<pre><code class=\"rust\">#[derive(Clone)] pub struct UploadService { bucket: Arc&lt;s3::Bucket> }<\/code><\/pre>\n<p>\u0414\u043b\u044f \u0432\u043d\u0435\u0434\u0440\u0435\u043d\u0438\u044f \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 (DI) \u0432 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442\u0430, \u043d\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0448\u0430 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u044b\u0432\u0430\u043b\u0430 \u0442\u0440\u0435\u0439\u0442\u00a0<code>Clone<\/code>. \u0422\u0430\u043a \u043a\u0430\u043a \u0441\u0435\u0440\u0432\u0438\u0441 \u0431\u0443\u0434\u0435\u0442 \u043a\u043b\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u043a\u0430\u0436\u0434\u044b\u0439 \u0437\u0430\u043f\u0440\u043e\u0441, \u043e\u0431\u0435\u0440\u043d\u0451\u043c\u00a0<code>s3::Bucket<\/code>\u00a0\u0432\u00a0<code>Arc<\/code>, \u0447\u0442\u043e\u0431\u044b \u043a\u043b\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0431\u044b\u043b\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u0434\u0435\u0448\u0451\u0432\u044b\u043c.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u043e\u0440 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 \u0441\u0435\u0440\u0432\u0438\u0441\u0430:<\/p>\n<pre><code class=\"rust\">use s3::{Bucket, Region}; use s3::creds::Credentials;  ...  impl UploadService { pub fn new() -> Self { let bucket_name = std::env::var(\"UPLOAD_BUCKET_NAME\") .expect(\"Expected UPLOAD_BUCKET_NAME environment variable\"); let region = Region::Custom { region: std::env::var(\"UPLOAD_BUCKET_REGION\") .expect(\"Expected UPLOAD_BUCKET_REGION environment variable\"), endpoint: std::env::var(\"UPLOAD_BUCKET_ENDPOINT\") .expect(\"Expected UPLOAD_BUCKET_ENDPOINT environment variable\") }; let credentials = Credentials::new( Some( &amp;std::env::var(\"UPLOAD_BUCKET_ACCESS_KEY\") .expect(\"Expected UPLOAD_BUCKET_ACCESS_KEY environment variable\") ), Some( &amp;std::env::var(\"UPLOAD_BUCKET_SECRET_KEY\") .expect(\"Expected UPLOAD_BUCKET_SECRET_KEY environment variable\") ), None, None, None ).unwrap();  let bucket = Bucket::new(&amp;bucket_name, region, credentials).unwrap() .with_path_style();  Self { bucket: Arc::new(bucket) } }  ...<\/code><\/pre>\n<p>\u0421\u0435\u0440\u0432\u0438\u0441 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f\u00a0<code>UPLOAD_BUCKET_NAME<\/code>,\u00a0<code>UPLOAD_BUCKET_REGION<\/code>,\u00a0<code>UPLOAD_BUCKET_ACCESS_KEY<\/code>,\u00a0<code>UPLOAD_BUCKET_SECRET_KEY<\/code>\u00a0\u0438\u00a0<code>UPLOAD_BUCKET_ENDPOINT<\/code>. \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0442\u0430\u043a \u043a\u0430\u043a \u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e \u043d\u0435 Amazon S3, \u0430 \u0434\u0440\u0443\u0433\u043e\u0433\u043e S3-\u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 (Scaleway). \u041f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Amazon S3 \u043c\u043e\u0436\u043d\u043e \u044f\u0432\u043d\u043e \u0437\u0430\u0434\u0430\u0442\u044c \u043d\u0443\u0436\u043d\u044b\u0439 \u0440\u0435\u0433\u0438\u043e\u043d \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0437 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0438\u0437 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f\u00a0<code>s3::Region<\/code>\u00a0(\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440,\u00a0<code>s3::Region::UsWest1<\/code>), \u043b\u0438\u0431\u043e \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f\u00a0<code>s3::Region::from_str<\/code>\u00a0\u0434\u043b\u044f \u043f\u0430\u0440\u0441\u0438\u043d\u0433\u0430 \u0440\u0435\u0433\u0438\u043e\u043d\u0430 \u0438\u0437 \u0441\u0442\u0440\u043e\u043a\u0438 \u0442\u0438\u043f\u0430\u00a0<code>us-west-1<\/code>. \u041a\u0441\u0442\u0430\u0442\u0438, \u0432 \u043d\u0430\u0431\u043e\u0440\u0435 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0439 \u0440\u0435\u0433\u0438\u043e\u043d\u0430 \u043f\u043e\u043c\u0438\u043c\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0445 \u0440\u0435\u0433\u0438\u043e\u043d\u043e\u0432 Amazon \u0435\u0441\u0442\u044c \u0435\u0449\u0451 Digital Ocean, Wasabi \u0438 Yandex.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0441\u0430\u043c\u043e\u0435 \u0441\u043b\u043e\u0436\u043d\u043e\u0435 \u2013 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435:<\/p>\n<pre><code class=\"rust\">use std::sync::{Arc, Mutex}; use std::path::Path; use std::ffi::OsStr; use axum::http::StatusCode; use axum::extract::multipart::Field; use async_hash::{Sha256, Digest}; use async_compat::CompatExt; use futures::TryStreamExt; use uuid::Uuid;  ...  pub async fn upload&lt;'a>(&amp;self, field: Field&lt;'a>) -> Result&lt;String, StatusCode> { let orig_filename = field.file_name() .unwrap_or(\"file\") .to_owned(); let mimetype = field.content_type() .unwrap_or(\"application\/octet-stream\") .to_owned(); let digest = Arc::new(Mutex::new(Sha256::new())); let mut reader = field .map_ok(|chunk| { if let Ok(mut digest) = digest.lock() { digest.update(&amp;chunk); } chunk }) .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) .into_async_read() .compat(); let tmp_filename = format!(\"tmp\/{}.bin\", Uuid::new_v4()); self.bucket.put_object_stream_with_content_type( &amp;mut reader, &amp;tmp_filename, &amp;mimetype ) .await .map_err(|err| { log::error!(\"S3 upload error: {:?}\", err); StatusCode::INTERNAL_SERVER_ERROR })?; drop(reader); \/\/ Release digest borrow let mut result = Err(StatusCode::INTERNAL_SERVER_ERROR); if let Some(digest) = Arc::into_inner(digest).and_then(|m| m.into_inner().ok()) { let digest = hex::encode(digest.finalize()); let ext = Path::new(&amp;orig_filename).extension().and_then(OsStr::to_str); let mut filename = if let Some(ext) = ext { format!(\"{}.{}\", digest, ext) } else { digest }; filename.make_ascii_lowercase(); match self.bucket.copy_object_internal(&amp;tmp_filename, &amp;filename).await { Ok(_) => result = Ok(format!(\"\/uploads\/{}\", &amp;filename)), Err(err) => log::error!(\"S3 copy error: {:?}\", err) } } if let Err(err) = self.bucket.delete_object(&amp;tmp_filename).await { log::error!(\"S3 delete error: {:?}\", err); } result }  ...<\/code><\/pre>\n<p>\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u043e\u0434\u043d\u043e \u043f\u043e\u043b\u0435 \u0438\u0437\u00a0<code>multipart\/form-data<\/code>\u00a0\u0437\u0430\u043f\u0440\u043e\u0441\u0430 (\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u0430 \u043d\u0438\u0436\u0435), \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0435 \u0438\u043c\u044f \u0444\u0430\u0439\u043b\u0430 \u0438 mime-\u0442\u0438\u043f (\u0435\u0441\u043b\u0438 \u044d\u0442\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u201c<code>file<\/code>\u201d \u0438 \u201c<code>application\/octet-stream<\/code>\u201d \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e). \u0417\u0430\u0442\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044f \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u044e\u0442\u0441\u044f \u0432\u00a0<code>AsyncRead<\/code>\u00a0\u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 async-compat. \u041f\u0440\u0438 \u044d\u0442\u043e\u043c \u043d\u0430\u0448 \u0447\u0438\u0442\u0430\u0442\u0435\u043b\u044c \u043f\u043e\u0442\u043e\u043a\u0430 \u043f\u043e \u043c\u0435\u0440\u0435 \u0447\u0442\u0435\u043d\u0438\u044f \u043f\u043e\u0442\u043e\u043a\u0430 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u0435\u0433\u043e SHA256 \u0445\u0435\u0448 (\u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0431\u0443\u0434\u0443\u0449\u0435\u043c).<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u0432 S3-\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u043f\u043e\u0434 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u043c \u0438\u043c\u0435\u043d\u0435\u043c \u201c<code>tmp\/&lt;UUID>.bin<\/code>\u201d (UUID \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c). \u0415\u0441\u043b\u0438 \u0432 \u044d\u0442\u043e\u0442 \u043c\u043e\u043c\u0435\u043d\u0442 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0430, \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043a\u043e\u0434 Internal Server Error.<\/p>\n<p>\u041c\u044b \u0438\u043c\u0435\u0435\u043c \u0444\u0430\u0439\u043b \u0432 S3-\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0438 \u043f\u043e\u0441\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u0439 SHA256 \u043e\u0442 \u0435\u0433\u043e \u0434\u0430\u043d\u043d\u044b\u0445. \u0422\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b \u0432 \u0435\u0433\u043e \u043e\u043a\u043e\u043d\u0447\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0438\u043c\u044f (\u044f \u0445\u043e\u0447\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SHA256 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0438\u043c\u0435\u043d\u0438 \u0444\u0430\u0439\u043b\u0430, \u0447\u0442\u043e\u0431\u044b \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u0435 \u0444\u0430\u0439\u043b\u044b \u043d\u0435 \u0434\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u043b\u0438\u0441\u044c \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435). \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u044f \u0431\u0435\u0440\u0443 HEX-\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 SHA256 \u0438 \u043f\u0440\u0438\u043f\u0438\u0441\u044b\u0432\u0430\u044e \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u0430 \u0432\u0437\u044f\u0442\u043e\u0435 \u0438\u0437 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u043c\u0435\u043d\u0438 (\u0435\u0441\u043b\u0438 \u043e\u043d\u043e \u0442\u0430\u043c \u0431\u044b\u043b\u043e). \u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u043a \u043d\u0438\u0436\u043d\u0435\u043c\u0443 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0443 (\u043d\u0430 \u0441\u043b\u0443\u0447\u0430\u0439 \u0435\u0441\u043b\u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u0430 \u0431\u044b\u043b\u043e \u043d\u0435 \u0432 \u043d\u0438\u0436\u043d\u0435\u043c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0435) \u0438 \u0434\u0430\u043b\u0435\u0435 \u043c\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 S3-\u043e\u0431\u044a\u0435\u043a\u0442\u0430 (\u0442\u0430\u043a \u043a\u0430\u043a API S3 \u043d\u0435 \u0438\u043c\u0435\u0435\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u044f). \u0415\u0441\u043b\u0438 \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u0442\u043e \u0443 \u043d\u0430\u0441 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0438\u0440\u0443\u044e\u0449\u0438\u0439 URL-\u0444\u0430\u0439\u043b\u0430.<\/p>\n<p>\u041d\u0430\u043a\u043e\u043d\u0435\u0446, \u043c\u043e\u0436\u043d\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442 \u0438\u0437 S3. \u042d\u0442\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0432 \u043b\u044e\u0431\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u2013 \u0438 \u0435\u0441\u043b\u0438 \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0431\u044b\u043b\u043e \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u043c, \u0438 \u0435\u0441\u043b\u0438 \u043d\u0435\u0442.<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u044f\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043d\u0430\u0448\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u2013 \u043e\u0442\u0434\u0430\u0447\u0430 \u0444\u0430\u0439\u043b\u0430 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 (\u0442\u0435\u043e\u0440\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u044d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0434\u0435\u043b\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0435\u0431-\u0441\u0435\u0440\u0432\u0435\u0440\u0443, \u043d\u043e \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u0443\u0434\u043e\u0431\u043d\u043e \u0438\u043c\u0435\u0442\u044c \u044d\u0442\u0443 \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043f\u0440\u0438 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0435, \u043a\u0430\u043a \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c \u043d\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u043a\u0430\u043a\u0443\u044e-\u043d\u0438\u0431\u0443\u0434\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443 \u0432\u0440\u043e\u0434\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u0440\u0430\u0432 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0444\u0430\u0439\u043b\u0443):<\/p>\n<pre><code class=\"rust\">use axum::response::IntoResponse; use axum::body::StreamBody; use s3::error::S3Error;  ...  pub async fn download( &amp;self, filename: &amp;str ) -> Result&lt;impl IntoResponse, StatusCode> { let stream = self.bucket.get_object_stream(filename) .await .map_err(|err| match err { S3Error::HttpFailWithBody(status_code, body) => match status_code { 404 => StatusCode::NOT_FOUND, _ => { log::error!( \"S3 download HTTP error with code={} and body={:?}\", status_code, body ); StatusCode::INTERNAL_SERVER_ERROR } } err => { log::error!(\"S3 download error: {:?}\", err); StatusCode::INTERNAL_SERVER_ERROR } })?; Ok(StreamBody::from(stream.bytes)) } }<\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u0432\u0441\u0451 \u043f\u0440\u043e\u0441\u0442\u043e \u2013 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0441\u0442\u0440\u0438\u043c S3-\u043e\u0431\u044a\u0435\u043a\u0442\u0430, \u043c\u0430\u043f\u043f\u0438\u043c \u043e\u0448\u0438\u0431\u043a\u0443 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u044f \u0444\u0430\u0439\u043b\u0430 \u043d\u0430 404-\u0443\u044e \u043e\u0448\u0438\u0431\u043a\u0443 Axum, \u0430 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043e\u0448\u0438\u0431\u043a\u0438 \u043d\u0430 500-\u0443\u044e, \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u00a0<code>StreamBody<\/code>.<\/p>\n<p>\u041e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u0441\u0430\u043c\u0438\u0445 \u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442\u043e\u0432:<\/p>\n<pre><code class=\"rust\">use axum::{Extension, Json}; use axum::extract::{Multipart, Path}; use axum::http::{HeaderMap, HeaderValue, StatusCode}; use axum::http::header::CACHE_CONTROL; use axum::response::IntoResponse;  #[derive(Debug, serde::Serialize)] pub struct UploadResponse { pub url: String }  pub async fn upload_file( Extension(upload_service): Extension&lt;UploadService>, mut multipart: Multipart ) -> Result&lt;impl IntoResponse, StatusCode> { while let Some(field) = multipart.next_field().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR )? { if let Some(\"upload\") = field.name() { let url = upload_service.upload(field).await?; return Ok(Json(UploadResponse { url })); } } Err(StatusCode::BAD_REQUEST) }  pub async fn download_file( Path(path): Path&lt;String>, Extension(upload_service): Extension&lt;UploadService> ) -> Result&lt;impl IntoResponse, StatusCode> { let body = upload_service.download(&amp;path).await?; let headers = HeaderMap::from_iter([ (CACHE_CONTROL, HeaderValue::from_str(\"max-age=31536000\").unwrap()) \/\/ One year ]); Ok((headers, body)) }<\/code><\/pre>\n<p>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u0444\u0430\u0439\u043b\u0443 \u0437\u0430 \u0440\u0430\u0437, \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0438\u043c\u044f \u043f\u043e\u043b\u044f \u0444\u0430\u0439\u043b\u0430 \u0432 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0439 \u0444\u043e\u0440\u043c\u0435 \u043e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f \u201cupload\u201d. \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0441\u043a\u0430\u0447\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u0440\u043e\u043a \u0436\u0438\u0437\u043d\u0438 \u0444\u0430\u0439\u043b\u0430 \u0432 \u043a\u0435\u0448\u0435 \u043e\u0434\u0438\u043d \u0433\u043e\u0434, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0444\u0430\u0439\u043b\u0430 \u043d\u0435 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u044e\u0442\u0441\u044f (\u0435\u0441\u043b\u0438 \u0444\u0430\u0439\u043b \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u0441\u044f, \u043e\u043d \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u0435\u0442\u044c \u0434\u0440\u0443\u0433\u043e\u0439 SHA256 \u0438 \u0434\u0440\u0443\u0433\u043e\u0435 \u0438\u043c\u044f).<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435, \u0447\u0442\u043e \u043d\u0430\u043c \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u2013 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0440\u043e\u0443\u0442\u0435\u0440 \u0438 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440:<\/p>\n<pre><code class=\"rust\">use std::str::FromStr; use axum::extract::DefaultBodyLimit;  #[tokio::main] async fn main() -> Result&lt;(), Box&lt;dyn std::error::Error>> { env_logger::init_from_env(env_logger::Env::default().default_filter_or(\"info\")); let upload_service = UploadService::new(); let router = axum::Router::new() .route(\"\/uploads\", axum::routing::post(upload_file)) .route(\"\/uploads\/*path\", axum::routing::get(download_file)) .layer(Extension(upload_service)) .layer(DefaultBodyLimit::max(8 * 1024 * 1024)); let address = std::env::var(\"HOST\").expect(\"Expected HOST environment variable\"); let port = std::env::var(\"PORT\").expect(\"Expected PORT environment variable\") .parse::&lt;u16>().expect(\"PORT environment variable must be an integer\"); log::info!(\"Listening on http:\/\/{}:{}\/\", address, port); axum::Server::bind( &amp;std::net::SocketAddr::new( std::net::IpAddr::from_str(&amp;address).unwrap(), port ) ).serve(router.into_make_service()).await?; Ok(()) }<\/code><\/pre>\n<p>\u042d\u043a\u0437\u0435\u043c\u043b\u044f\u0440\u00a0<code>UploadService<\/code>\u00a0\u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437\u00a0<code>Extension<\/code>\u00a0(\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c DI \u0432 Axum), \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u043b\u0435\u0437\u043d\u043e \u0437\u0430\u0434\u0430\u0442\u044c\u00a0<code>DefaultBodyLimit<\/code>, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 1 \u041c\u0411 \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442\u044c \u043d\u0435 \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u0439. \u0425\u043e\u0441\u0442 \u0438 \u043f\u043e\u0440\u0442 \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442\u0441\u044f \u0438\u0437 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f.<\/p>\n<p>\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u043e, \u043d\u0430\u043c \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u0442 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043a\u0430\u043a\u0443\u044e-\u043d\u0438\u0431\u0443\u0434\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432 \u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 (\u0430, \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0438 \u0441\u043a\u0430\u0447\u043a\u0438), \u043d\u043e \u044d\u0442\u043e \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430.<\/p>\n<p>\u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0432 Cargo.toml:<\/p>\n<pre><code>[package] name = \"uploader\" version = \"0.1.0\" edition = \"2021\"  [dependencies] log = \"0.4.20\" env_logger = \"0.10.0\" tokio = { version = \"1\", features = [\"macros\", \"rt-multi-thread\"] } axum = { version = \"0.6.20\", features = [\"multipart\"] } serde = \"1.0.188\" uuid = { version = \"1.4.1\", features = [\"v4\"] } rust-s3 = \"0.34.0-rc1\" futures = \"0.3.28\" async-compat = \"0.2.2\" async-hash = \"0.5.1\" hex = \"0.4.3\"<\/code><\/pre>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0431\u043e\u043d\u0443\u0441\u0430 \u043f\u0440\u0438\u043c\u0435\u0440 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u043d\u0430 TypeScript \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0449\u0435\u0433\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u0444\u0430\u0439\u043b\u0430:<\/p>\n<pre><code class=\"typescript\">interface UploadResponse { url: string; }  async function uploadFile(file: Blob, filename?: string): Promise&lt;UploadResponse | \"error\"> { const data = new FormData(); data.append(\"upload\", file, filename); const response = await fetch(\"\/uploads\", { method: \"post\", body: data }); if (response.status >= 200 &amp;&amp; response.status &lt;= 299) { return await response.json(); } else { return \"error\"; } }<\/code><\/pre>\n<p>\u041d\u0430\u0448 \u0441\u0435\u0440\u0432\u0438\u0441 \u0433\u043e\u0442\u043e\u0432.<\/p>\n<p><a href=\"https:\/\/github.com\/KivApple\/axum_s3_example\" rel=\"noopener noreferrer nofollow\">\u041f\u043e\u043b\u043d\u044b\u0439 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043d\u0430 GitHub<\/a><\/p>\n<p><a href=\"https:\/\/t.me\/kiv_apple_blog\/25\" rel=\"noopener noreferrer nofollow\">\u041e\u0431\u0441\u0443\u0434\u0438\u0442\u044c \u0432 \u043c\u043e\u0451\u043c \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u043c \u0431\u043b\u043e\u0433\u0435 \u0432 Telegram<\/a><\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/770332\/\"> https:\/\/habr.com\/ru\/articles\/770332\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041d\u0435\u0434\u0430\u0432\u043d\u043e \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u043f\u043e\u0434\u0440\u0443\u0436\u0438\u0442\u044c \u0432\u0435\u0431-\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u00a0<a href=\"https:\/\/crates.io\/crates\/axum\" rel=\"noopener noreferrer nofollow\">Axum<\/a>\u00a0\u0438 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443\u00a0<a href=\"https:\/\/crates.io\/crates\/rust-s3\" rel=\"noopener noreferrer nofollow\">rust-s3<\/a>. \u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u0437\u0430\u0434\u0430\u0447\u0430 \u0441\u0434\u0435\u043b\u0430\u0442\u044c 2 \u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442\u0430:<\/p>\n<ul>\n<li>\n<p>\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0444\u0430\u0439\u043b\u0430 \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0441\u0441\u044b\u043b\u043a\u0438<\/p>\n<\/li>\n<li>\n<p>\u0421\u043a\u0430\u0447\u043a\u0430 \u0444\u0430\u0439\u043b\u0430 \u0438\u0437 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435<\/p>\n<\/li>\n<\/ul>\n<p>\u0420\u0430\u0437\u0443\u043c\u0435\u0435\u0442\u0441\u044f, \u0431\u0435\u0437 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u0444\u0430\u0439\u043b\u043e\u0432 \u0438 \u0431\u0435\u0437 \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f \u0446\u0435\u043b\u0438\u043a\u043e\u043c \u0432\u0441\u0435\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0444\u0430\u0439\u043b\u0430 \u0432 \u043f\u0430\u043c\u044f\u0442\u0438.<\/p>\n<p>\u0422\u0430\u043a \u043a\u0430\u043a \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 S3 \u043d\u0443\u0436\u043d\u044b \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b (\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u043c\u0443 bucket), \u0432\u044b\u043d\u0435\u0441\u0435\u043c \u043d\u0435\u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u0443\u044e \u0440\u0430\u0431\u043e\u0442\u0443 \u0432 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443\u00a0<code>UploadService<\/code>:<\/p>\n<pre><code class=\"rust\">#[derive(Clone)] pub struct UploadService { bucket: Arc&lt;s3::Bucket> }<\/code><\/pre>\n<p>\u0414\u043b\u044f \u0432\u043d\u0435\u0434\u0440\u0435\u043d\u0438\u044f \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 (DI) \u0432 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442\u0430, \u043d\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0448\u0430 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u044b\u0432\u0430\u043b\u0430 \u0442\u0440\u0435\u0439\u0442\u00a0<code>Clone<\/code>. \u0422\u0430\u043a \u043a\u0430\u043a \u0441\u0435\u0440\u0432\u0438\u0441 \u0431\u0443\u0434\u0435\u0442 \u043a\u043b\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u043a\u0430\u0436\u0434\u044b\u0439 \u0437\u0430\u043f\u0440\u043e\u0441, \u043e\u0431\u0435\u0440\u043d\u0451\u043c\u00a0<code>s3::Bucket<\/code>\u00a0\u0432\u00a0<code>Arc<\/code>, \u0447\u0442\u043e\u0431\u044b \u043a\u043b\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0431\u044b\u043b\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u0434\u0435\u0448\u0451\u0432\u044b\u043c.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0442\u043e\u0440 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 \u0441\u0435\u0440\u0432\u0438\u0441\u0430:<\/p>\n<pre><code class=\"rust\">use s3::{Bucket, Region}; use s3::creds::Credentials;  ...  impl UploadService { pub fn new() -> Self { let bucket_name = std::env::var(\"UPLOAD_BUCKET_NAME\") .expect(\"Expected UPLOAD_BUCKET_NAME environment variable\"); let region = Region::Custom { region: std::env::var(\"UPLOAD_BUCKET_REGION\") .expect(\"Expected UPLOAD_BUCKET_REGION environment variable\"), endpoint: std::env::var(\"UPLOAD_BUCKET_ENDPOINT\") .expect(\"Expected UPLOAD_BUCKET_ENDPOINT environment variable\") }; let credentials = Credentials::new( Some( &amp;std::env::var(\"UPLOAD_BUCKET_ACCESS_KEY\") .expect(\"Expected UPLOAD_BUCKET_ACCESS_KEY environment variable\") ), Some( &amp;std::env::var(\"UPLOAD_BUCKET_SECRET_KEY\") .expect(\"Expected UPLOAD_BUCKET_SECRET_KEY environment variable\") ), None, None, None ).unwrap();  let bucket = Bucket::new(&amp;bucket_name, region, credentials).unwrap() .with_path_style();  Self { bucket: Arc::new(bucket) } }  ...<\/code><\/pre>\n<p>\u0421\u0435\u0440\u0432\u0438\u0441 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f\u00a0<code>UPLOAD_BUCKET_NAME<\/code>,\u00a0<code>UPLOAD_BUCKET_REGION<\/code>,\u00a0<code>UPLOAD_BUCKET_ACCESS_KEY<\/code>,\u00a0<code>UPLOAD_BUCKET_SECRET_KEY<\/code>\u00a0\u0438\u00a0<code>UPLOAD_BUCKET_ENDPOINT<\/code>. \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0442\u0430\u043a \u043a\u0430\u043a \u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e \u043d\u0435 Amazon S3, \u0430 \u0434\u0440\u0443\u0433\u043e\u0433\u043e S3-\u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 (Scaleway). \u041f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Amazon S3 \u043c\u043e\u0436\u043d\u043e \u044f\u0432\u043d\u043e \u0437\u0430\u0434\u0430\u0442\u044c \u043d\u0443\u0436\u043d\u044b\u0439 \u0440\u0435\u0433\u0438\u043e\u043d \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0437 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0438\u0437 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f\u00a0<code>s3::Region<\/code>\u00a0(\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440,\u00a0<code>s3::Region::UsWest1<\/code>), \u043b\u0438\u0431\u043e \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f\u00a0<code>s3::Region::from_str<\/code>\u00a0\u0434\u043b\u044f \u043f\u0430\u0440\u0441\u0438\u043d\u0433\u0430 \u0440\u0435\u0433\u0438\u043e\u043d\u0430 \u0438\u0437 \u0441\u0442\u0440\u043e\u043a\u0438 \u0442\u0438\u043f\u0430\u00a0<code>us-west-1<\/code>. \u041a\u0441\u0442\u0430\u0442\u0438, \u0432 \u043d\u0430\u0431\u043e\u0440\u0435 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0439 \u0440\u0435\u0433\u0438\u043e\u043d\u0430 \u043f\u043e\u043c\u0438\u043c\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0445 \u0440\u0435\u0433\u0438\u043e\u043d\u043e\u0432 Amazon \u0435\u0441\u0442\u044c \u0435\u0449\u0451 Digital Ocean, Wasabi \u0438 Yandex.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0441\u0430\u043c\u043e\u0435 \u0441\u043b\u043e\u0436\u043d\u043e\u0435 \u2013 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435:<\/p>\n<pre><code class=\"rust\">use std::sync::{Arc, Mutex}; use std::path::Path; use std::ffi::OsStr; use axum::http::StatusCode; use axum::extract::multipart::Field; use async_hash::{Sha256, Digest}; use async_compat::CompatExt; use futures::TryStreamExt; use uuid::Uuid;  ...  pub async fn upload&lt;'a>(&amp;self, field: Field&lt;'a>) -> Result&lt;String, StatusCode> { let orig_filename = field.file_name() .unwrap_or(\"file\") .to_owned(); let mimetype = field.content_type() .unwrap_or(\"application\/octet-stream\") .to_owned(); let digest = Arc::new(Mutex::new(Sha256::new())); let mut reader = field .map_ok(|chunk| { if let Ok(mut digest) = digest.lock() { digest.update(&amp;chunk); } chunk }) .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) .into_async_read() .compat(); let tmp_filename = format!(\"tmp\/{}.bin\", Uuid::new_v4()); self.bucket.put_object_stream_with_content_type( &amp;mut reader, &amp;tmp_filename, &amp;mimetype ) .await .map_err(|err| { log::error!(\"S3 upload error: {:?}\", err); StatusCode::INTERNAL_SERVER_ERROR })?; drop(reader); \/\/ Release digest borrow let mut result = Err(StatusCode::INTERNAL_SERVER_ERROR); if let Some(digest) = Arc::into_inner(digest).and_then(|m| m.into_inner().ok()) { let digest = hex::encode(digest.finalize()); let ext = Path::new(&amp;orig_filename).extension().and_then(OsStr::to_str); let mut filename = if let Some(ext) = ext { format!(\"{}.{}\", digest, ext) } else { digest }; filename.make_ascii_lowercase(); match self.bucket.copy_object_internal(&amp;tmp_filename, &amp;filename).await { Ok(_) => result = Ok(format!(\"\/uploads\/{}\", &amp;filename)), Err(err) => log::error!(\"S3 copy error: {:?}\", err) } } if let Err(err) = self.bucket.delete_object(&amp;tmp_filename).await { log::error!(\"S3 delete error: {:?}\", err); } result }  ...<\/code><\/pre>\n<p>\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u043e\u0434\u043d\u043e \u043f\u043e\u043b\u0435 \u0438\u0437\u00a0<code>multipart\/form-data<\/code>\u00a0\u0437\u0430\u043f\u0440\u043e\u0441\u0430 (\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u0430 \u043d\u0438\u0436\u0435), \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0435 \u0438\u043c\u044f \u0444\u0430\u0439\u043b\u0430 \u0438 mime-\u0442\u0438\u043f (\u0435\u0441\u043b\u0438 \u044d\u0442\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u201c<code>file<\/code>\u201d \u0438 \u201c<code>application\/octet-stream<\/code>\u201d \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e). \u0417\u0430\u0442\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044f \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u044e\u0442\u0441\u044f \u0432\u00a0<code>AsyncRead<\/code>\u00a0\u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 async-compat. \u041f\u0440\u0438 \u044d\u0442\u043e\u043c \u043d\u0430\u0448 \u0447\u0438\u0442\u0430\u0442\u0435\u043b\u044c \u043f\u043e\u0442\u043e\u043a\u0430 \u043f\u043e \u043c\u0435\u0440\u0435 \u0447\u0442\u0435\u043d\u0438\u044f \u043f\u043e\u0442\u043e\u043a\u0430 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u0435\u0433\u043e SHA256 \u0445\u0435\u0448 (\u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0431\u0443\u0434\u0443\u0449\u0435\u043c).<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u0432 S3-\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u043f\u043e\u0434 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u043c \u0438\u043c\u0435\u043d\u0435\u043c \u201c<code>tmp\/&lt;UUID>.bin<\/code>\u201d (UUID \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c). \u0415\u0441\u043b\u0438 \u0432 \u044d\u0442\u043e\u0442 \u043c\u043e\u043c\u0435\u043d\u0442 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0430, \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043a\u043e\u0434 Internal Server Error.<\/p>\n<p>\u041c\u044b \u0438\u043c\u0435\u0435\u043c \u0444\u0430\u0439\u043b \u0432 S3-\u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0438 \u043f\u043e\u0441\u0447\u0438\u0442\u0430\u043d\u043d\u044b\u0439 SHA256 \u043e\u0442 \u0435\u0433\u043e \u0434\u0430\u043d\u043d\u044b\u0445. \u0422\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b \u0432 \u0435\u0433\u043e \u043e\u043a\u043e\u043d\u0447\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0438\u043c\u044f (\u044f \u0445\u043e\u0447\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SHA256 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0438\u043c\u0435\u043d\u0438 \u0444\u0430\u0439\u043b\u0430, \u0447\u0442\u043e\u0431\u044b \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u0435 \u0444\u0430\u0439\u043b\u044b \u043d\u0435 \u0434\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u043b\u0438\u0441\u044c \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435). \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u044f \u0431\u0435\u0440\u0443 HEX-\u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 SHA256 \u0438 \u043f\u0440\u0438\u043f\u0438\u0441\u044b\u0432\u0430\u044e \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u0430 \u0432\u0437\u044f\u0442\u043e\u0435 \u0438\u0437 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u043c\u0435\u043d\u0438 (\u0435\u0441\u043b\u0438 \u043e\u043d\u043e \u0442\u0430\u043c \u0431\u044b\u043b\u043e). \u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u043a \u043d\u0438\u0436\u043d\u0435\u043c\u0443 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0443 (\u043d\u0430 \u0441\u043b\u0443\u0447\u0430\u0439 \u0435\u0441\u043b\u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u0430 \u0431\u044b\u043b\u043e \u043d\u0435 \u0432 \u043d\u0438\u0436\u043d\u0435\u043c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0435) \u0438 \u0434\u0430\u043b\u0435\u0435 \u043c\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 S3-\u043e\u0431\u044a\u0435\u043a\u0442\u0430 (\u0442\u0430\u043a \u043a\u0430\u043a API S3 \u043d\u0435 \u0438\u043c\u0435\u0435\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u044f). \u0415\u0441\u043b\u0438 \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u0442\u043e \u0443 \u043d\u0430\u0441 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0438\u0440\u0443\u044e\u0449\u0438\u0439 URL-\u0444\u0430\u0439\u043b\u0430.<\/p>\n<p>\u041d\u0430\u043a\u043e\u043d\u0435\u0446, \u043c\u043e\u0436\u043d\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442 \u0438\u0437 S3. \u042d\u0442\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0432 \u043b\u044e\u0431\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u2013 \u0438 \u0435\u0441\u043b\u0438 \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0431\u044b\u043b\u043e \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u043c, \u0438 \u0435\u0441\u043b\u0438 \u043d\u0435\u0442.<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u044f\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043d\u0430\u0448\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u2013 \u043e\u0442\u0434\u0430\u0447\u0430 \u0444\u0430\u0439\u043b\u0430 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 (\u0442\u0435\u043e\u0440\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u044d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0434\u0435\u043b\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0435\u0431-\u0441\u0435\u0440\u0432\u0435\u0440\u0443, \u043d\u043e \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u0443\u0434\u043e\u0431\u043d\u043e \u0438\u043c\u0435\u0442\u044c \u044d\u0442\u0443 \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043f\u0440\u0438 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0435, \u043a\u0430\u043a \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c \u043d\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u043a\u0430\u043a\u0443\u044e-\u043d\u0438\u0431\u0443\u0434\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443 \u0432\u0440\u043e\u0434\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u0440\u0430\u0432 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0444\u0430\u0439\u043b\u0443):<\/p>\n<pre><code class=\"rust\">use axum::response::IntoResponse; use axum::body::StreamBody; use s3::error::S3Error;  ...  pub async fn download( &amp;self, filename: &amp;str ) -> Result&lt;impl IntoResponse, StatusCode> { let stream = self.bucket.get_object_stream(filename) .await .map_err(|err| match err { S3Error::HttpFailWithBody(status_code, body) => match status_code { 404 => StatusCode::NOT_FOUND, _ => { log::error!( \"S3 download HTTP error with code={} and body={:?}\", status_code, body ); StatusCode::INTERNAL_SERVER_ERROR } } err => { log::error!(\"S3 download error: {:?}\", err); StatusCode::INTERNAL_SERVER_ERROR } })?; Ok(StreamBody::from(stream.bytes)) } }<\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u0432\u0441\u0451 \u043f\u0440\u043e\u0441\u0442\u043e \u2013 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0441\u0442\u0440\u0438\u043c S3-\u043e\u0431\u044a\u0435\u043a\u0442\u0430, \u043c\u0430\u043f\u043f\u0438\u043c \u043e\u0448\u0438\u0431\u043a\u0443 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u044f \u0444\u0430\u0439\u043b\u0430 \u043d\u0430 404-\u0443\u044e \u043e\u0448\u0438\u0431\u043a\u0443 Axum, \u0430 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043e\u0448\u0438\u0431\u043a\u0438 \u043d\u0430 500-\u0443\u044e, \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u00a0<code>StreamBody<\/code>.<\/p>\n<p>\u041e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u0441\u0430\u043c\u0438\u0445 \u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442\u043e\u0432:<\/p>\n<pre><code class=\"rust\">use axum::{Extension, Json}; use axum::extract::{Multipart, Path}; use axum::http::{HeaderMap, HeaderValue, StatusCode}; use axum::http::header::CACHE_CONTROL; use axum::response::IntoResponse;  #[derive(Debug, serde::Serialize)] pub struct UploadResponse { pub url: String }  pub async fn upload_file( Extension(upload_service): Extension&lt;UploadService>, mut multipart: Multipart ) -> Result&lt;impl IntoResponse, StatusCode> { while let Some(field) = multipart.next_field().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR )? { if let Some(\"upload\") = field.name() { let url = upload_service.upload(field).await?; return Ok(Json(UploadResponse { url })); } } Err(StatusCode::BAD_REQUEST) }  pub async fn download_file( Path(path): Path&lt;String>, Extension(upload_service): Extension&lt;UploadService> ) -> Result&lt;impl IntoResponse, StatusCode> { let body = upload_service.download(&amp;path).await?; let headers = HeaderMap::from_iter([ (CACHE_CONTROL, HeaderValue::from_str(\"max-age=31536000\").unwrap()) \/\/ One year ]); Ok((headers, body)) }<\/code><\/pre>\n<p>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u0444\u0430\u0439\u043b\u0443 \u0437\u0430 \u0440\u0430\u0437, \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0438\u043c\u044f \u043f\u043e\u043b\u044f \u0444\u0430\u0439\u043b\u0430 \u0432 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0439 \u0444\u043e\u0440\u043c\u0435 \u043e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f \u201cupload\u201d. \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0441\u043a\u0430\u0447\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u0440\u043e\u043a \u0436\u0438\u0437\u043d\u0438 \u0444\u0430\u0439\u043b\u0430 \u0432 \u043a\u0435\u0448\u0435 \u043e\u0434\u0438\u043d \u0433\u043e\u0434, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0444\u0430\u0439\u043b\u0430 \u043d\u0435 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u044e\u0442\u0441\u044f (\u0435\u0441\u043b\u0438 \u0444\u0430\u0439\u043b \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u0441\u044f, \u043e\u043d \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u0435\u0442\u044c \u0434\u0440\u0443\u0433\u043e\u0439 SHA256 \u0438 \u0434\u0440\u0443\u0433\u043e\u0435 \u0438\u043c\u044f).<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435, \u0447\u0442\u043e \u043d\u0430\u043c \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u2013 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0440\u043e\u0443\u0442\u0435\u0440 \u0438 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440:<\/p>\n<pre><code class=\"rust\">use std::str::FromStr; use axum::extract::DefaultBodyLimit;  #[tokio::main] async fn main() -> Result&lt;(), Box&lt;dyn std::error::Error>> { env_logger::init_from_env(env_logger::Env::default().default_filter_or(\"info\")); let upload_service = UploadService::new(); let router = axum::Router::new() .route(\"\/uploads\", axum::routing::post(upload_file)) .route(\"\/uploads\/*path\", axum::routing::get(download_file)) .layer(Extension(upload_service)) .layer(DefaultBodyLimit::max(8 * 1024 * 1024)); let address = std::env::var(\"HOST\").expect(\"Expected HOST environment variable\"); let port = std::env::var(\"PORT\").expect(\"Expected PORT environment variable\") .parse::&lt;u16>().expect(\"PORT environment variable must be an integer\"); log::info!(\"Listening on http:\/\/{}:{}\/\", address, port); axum::Server::bind( &amp;std::net::SocketAddr::new( std::net::IpAddr::from_str(&amp;address).unwrap(), port ) ).serve(router.into_make_service()).await?; Ok(()) }<\/code><\/pre>\n<p>\u042d\u043a\u0437\u0435\u043c\u043b\u044f\u0440\u00a0<code>UploadService<\/code>\u00a0\u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437\u00a0<code>Extension<\/code>\u00a0(\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c DI \u0432 Axum), \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u043b\u0435\u0437\u043d\u043e \u0437\u0430\u0434\u0430\u0442\u044c\u00a0<code>DefaultBodyLimit<\/code>, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 1 \u041c\u0411 \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442\u044c \u043d\u0435 \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u0439. \u0425\u043e\u0441\u0442 \u0438 \u043f\u043e\u0440\u0442 \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442\u0441\u044f \u0438\u0437 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f.<\/p>\n<p>\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u043e, \u043d\u0430\u043c \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u0442 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043a\u0430\u043a\u0443\u044e-\u043d\u0438\u0431\u0443\u0434\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432 \u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 (\u0430, \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0438 \u0441\u043a\u0430\u0447\u043a\u0438), \u043d\u043e \u044d\u0442\u043e \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430.<\/p>\n<p>\u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0432 Cargo.toml:<\/p>\n<pre><code>[package] name = \"uploader\" version = \"0.1.0\" edition = \"2021\"  [dependencies] log = \"0.4.20\" env_logger = \"0.10.0\" tokio = { version = \"1\", features = [\"macros\", \"rt-multi-thread\"] } axum = { version = \"0.6.20\", features = [\"multipart\"] } serde = \"1.0.188\" uuid = { version = \"1.4.1\", features = [\"v4\"] } rust-s3 = \"0.34.0-rc1\" futures = \"0.3.28\" async-compat = \"0.2.2\" async-hash = \"0.5.1\" hex = \"0.4.3\"<\/code><\/pre>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0431\u043e\u043d\u0443\u0441\u0430 \u043f\u0440\u0438\u043c\u0435\u0440 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u043d\u0430 TypeScript \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0449\u0435\u0433\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u0444\u0430\u0439\u043b\u0430:<\/p>\n<pre><code class=\"typescript\">interface UploadResponse {<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-358667","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/358667","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=358667"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/358667\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=358667"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=358667"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=358667"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}