mirror of
https://github.com/actix/examples
synced 2025-06-26 17:17:42 +02:00
Restructure folders (#411)
This commit is contained in:
committed by
GitHub
parent
9db98162b2
commit
c3407627d0
12
forms/form/Cargo.toml
Normal file
12
forms/form/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "form-example"
|
||||
version = "1.0.0"
|
||||
authors = ["Gorm Casper <gcasper@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1"
|
8
forms/form/README.md
Normal file
8
forms/form/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
## Form example
|
||||
|
||||
```bash
|
||||
cd form
|
||||
cargo run
|
||||
# Started http server: 127.0.0.1:8080
|
||||
```
|
||||
|
223
forms/form/src/main.rs
Normal file
223
forms/form/src/main.rs
Normal file
@ -0,0 +1,223 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use actix_web::{
|
||||
middleware, web, App, HttpRequest, HttpResponse, HttpServer, Responder, Result,
|
||||
};
|
||||
|
||||
struct AppState {
|
||||
foo: String,
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.configure(app_config)
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
fn app_config(config: &mut web::ServiceConfig) {
|
||||
config.service(
|
||||
web::scope("")
|
||||
.data(AppState {
|
||||
foo: "bar".to_string(),
|
||||
})
|
||||
.service(web::resource("/").route(web::get().to(index)))
|
||||
.service(web::resource("/post1").route(web::post().to(handle_post_1)))
|
||||
.service(web::resource("/post2").route(web::post().to(handle_post_2)))
|
||||
.service(web::resource("/post3").route(web::post().to(handle_post_3))),
|
||||
);
|
||||
}
|
||||
|
||||
async fn index() -> Result<HttpResponse> {
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(include_str!("../static/form.html")))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MyParams {
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// Simple handle POST request
|
||||
async fn handle_post_1(params: web::Form<MyParams>) -> Result<HttpResponse> {
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/plain")
|
||||
.body(format!("Your name is {}", params.name)))
|
||||
}
|
||||
|
||||
/// State and POST Params
|
||||
async fn handle_post_2(
|
||||
state: web::Data<AppState>,
|
||||
params: web::Form<MyParams>,
|
||||
) -> HttpResponse {
|
||||
HttpResponse::Ok().content_type("text/plain").body(format!(
|
||||
"Your name is {}, and in AppState I have foo: {}",
|
||||
params.name, state.foo
|
||||
))
|
||||
}
|
||||
|
||||
/// Request and POST Params
|
||||
async fn handle_post_3(req: HttpRequest, params: web::Form<MyParams>) -> impl Responder {
|
||||
println!("Handling POST request: {:?}", req);
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/plain")
|
||||
.body(format!("Your name is {}", params.name))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use actix_web::body::{Body, ResponseBody};
|
||||
use actix_web::dev::{HttpResponseBuilder, Service, ServiceResponse};
|
||||
use actix_web::http::{header::CONTENT_TYPE, HeaderValue, StatusCode};
|
||||
use actix_web::test::{self, TestRequest};
|
||||
use actix_web::web::Form;
|
||||
|
||||
trait BodyTest {
|
||||
fn as_str(&self) -> &str;
|
||||
}
|
||||
|
||||
impl BodyTest for ResponseBody<Body> {
|
||||
fn as_str(&self) -> &str {
|
||||
match self {
|
||||
ResponseBody::Body(ref b) => match b {
|
||||
Body::Bytes(ref by) => std::str::from_utf8(&by).unwrap(),
|
||||
_ => panic!(),
|
||||
},
|
||||
ResponseBody::Other(ref b) => match b {
|
||||
Body::Bytes(ref by) => std::str::from_utf8(&by).unwrap(),
|
||||
_ => panic!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn handle_post_1_unit_test() {
|
||||
let params = Form(MyParams {
|
||||
name: "John".to_string(),
|
||||
});
|
||||
let resp = handle_post_1(params).await.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("text/plain")
|
||||
);
|
||||
assert_eq!(resp.body().as_str(), "Your name is John");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn handle_post_1_integration_test() {
|
||||
let mut app = test::init_service(App::new().configure(app_config)).await;
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/post1")
|
||||
.set_form(&MyParams {
|
||||
name: "John".to_string(),
|
||||
})
|
||||
.to_request();
|
||||
let resp: ServiceResponse = app.call(req).await.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("text/plain")
|
||||
);
|
||||
assert_eq!(resp.response().body().as_str(), "Your name is John");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn handle_post_2_unit_test() {
|
||||
let state = TestRequest::default()
|
||||
.data(AppState {
|
||||
foo: "bar".to_string(),
|
||||
})
|
||||
.to_http_request();
|
||||
let data = state.app_data::<actix_web::web::Data<AppState>>().unwrap();
|
||||
let params = Form(MyParams {
|
||||
name: "John".to_string(),
|
||||
});
|
||||
let resp = handle_post_2(data.clone(), params).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("text/plain")
|
||||
);
|
||||
assert_eq!(
|
||||
resp.body().as_str(),
|
||||
"Your name is John, and in AppState I have foo: bar"
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn handle_post_2_integration_test() {
|
||||
let mut app = test::init_service(App::new().configure(app_config)).await;
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/post2")
|
||||
.set_form(&MyParams {
|
||||
name: "John".to_string(),
|
||||
})
|
||||
.to_request();
|
||||
let resp: ServiceResponse = app.call(req).await.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("text/plain")
|
||||
);
|
||||
assert_eq!(
|
||||
resp.response().body().as_str(),
|
||||
"Your name is John, and in AppState I have foo: bar"
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn handle_post_3_unit_test() {
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let params = Form(MyParams {
|
||||
name: "John".to_string(),
|
||||
});
|
||||
let result = handle_post_3(req.clone(), params).await;
|
||||
let resp = match result.respond_to(&req).await {
|
||||
Ok(t) => t,
|
||||
Err(_) => {
|
||||
HttpResponseBuilder::new(StatusCode::INTERNAL_SERVER_ERROR).finish()
|
||||
}
|
||||
};
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("text/plain")
|
||||
);
|
||||
assert_eq!(resp.body().as_str(), "Your name is John");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn handle_post_3_integration_test() {
|
||||
let mut app = test::init_service(App::new().configure(app_config)).await;
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/post3")
|
||||
.set_form(&MyParams {
|
||||
name: "John".to_string(),
|
||||
})
|
||||
.to_request();
|
||||
let resp: ServiceResponse = app.call(req).await.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("text/plain")
|
||||
);
|
||||
assert_eq!(resp.response().body().as_str(), "Your name is John");
|
||||
}
|
||||
}
|
39
forms/form/static/form.html
Normal file
39
forms/form/static/form.html
Normal file
@ -0,0 +1,39 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8>
|
||||
<title>Forms</title>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Will hit handle_post_1</h3>
|
||||
<form action=/post1 method=POST>
|
||||
<label>
|
||||
Name:
|
||||
<input name="name">
|
||||
</label>
|
||||
<button type=submit>Submit form</button>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Will hit handle_post_2</h3>
|
||||
<form action=/post2 method=POST>
|
||||
<label>
|
||||
Name:
|
||||
<input name="name">
|
||||
</label>
|
||||
<button type=submit>Submit form</button>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Will hit handle_post_3</h3>
|
||||
<form action=/post3 method=POST>
|
||||
<label>
|
||||
Name:
|
||||
<input name="name">
|
||||
</label>
|
||||
<button type=submit>Submit form</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
3
forms/multipart-async-std/.gitignore
vendored
Normal file
3
forms/multipart-async-std/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
/tmp
|
17
forms/multipart-async-std/Cargo.toml
Normal file
17
forms/multipart-async-std/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "multipart-async-std-example"
|
||||
version = "0.3.0"
|
||||
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
description = "Simple file uploader in Actix Web with Async/Await"
|
||||
keywords = ["actix", "actix-web", "multipart"]
|
||||
repository = "https://github.com/actix/examples"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "3"
|
||||
actix-multipart = "0.3"
|
||||
futures = "0.3.5"
|
||||
async-std = "1.8.0"
|
||||
sanitize-filename = "0.2"
|
21
forms/multipart-async-std/LICENSE
Normal file
21
forms/multipart-async-std/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [2019] [Bevan Hunt]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
11
forms/multipart-async-std/README.md
Normal file
11
forms/multipart-async-std/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Actix Web File Upload with Async/Await
|
||||
|
||||
### Run
|
||||
|
||||
``` open web browser to localhost:3000 and upload file(s) ```
|
||||
|
||||
### Result
|
||||
|
||||
``` file(s) will show up in ./tmp in the same directory as the running process ```
|
||||
|
||||
Note: this is a naive implementation and will panic on any error
|
58
forms/multipart-async-std/src/main.rs
Normal file
58
forms/multipart-async-std/src/main.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer};
|
||||
use async_std::prelude::*;
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
|
||||
async fn save_file(mut payload: Multipart) -> Result<HttpResponse, Error> {
|
||||
// iterate over multipart stream
|
||||
while let Ok(Some(mut field)) = payload.try_next().await {
|
||||
let content_type = field
|
||||
.content_disposition()
|
||||
.ok_or_else(|| actix_web::error::ParseError::Incomplete)?;
|
||||
let filename = content_type
|
||||
.get_filename()
|
||||
.ok_or_else(|| actix_web::error::ParseError::Incomplete)?;
|
||||
let filepath = format!("./tmp/{}", sanitize_filename::sanitize(&filename));
|
||||
let mut f = async_std::fs::File::create(filepath).await?;
|
||||
|
||||
// Field in turn is stream of *Bytes* object
|
||||
while let Some(chunk) = field.next().await {
|
||||
let data = chunk.unwrap();
|
||||
f.write_all(&data).await?;
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse::Ok().into())
|
||||
}
|
||||
|
||||
fn index() -> HttpResponse {
|
||||
let html = r#"<html>
|
||||
<head><title>Upload Test</title></head>
|
||||
<body>
|
||||
<form target="/" method="post" enctype="multipart/form-data">
|
||||
<input type="file" multiple name="file"/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>"#;
|
||||
|
||||
HttpResponse::Ok().body(html)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
|
||||
async_std::fs::create_dir_all("./tmp").await?;
|
||||
|
||||
let ip = "0.0.0.0:3000";
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new().wrap(middleware::Logger::default()).service(
|
||||
web::resource("/")
|
||||
.route(web::get().to(index))
|
||||
.route(web::post().to(save_file)),
|
||||
)
|
||||
})
|
||||
.bind(ip)?
|
||||
.run()
|
||||
.await
|
||||
}
|
4
forms/multipart-s3/.env.example
Normal file
4
forms/multipart-s3/.env.example
Normal file
@ -0,0 +1,4 @@
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_S3_BUCKET_NAME=
|
||||
AWS_REGION=
|
3
forms/multipart-s3/.gitignore
vendored
Normal file
3
forms/multipart-s3/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
.env
|
||||
/tmp
|
19
forms/multipart-s3/Cargo.toml
Normal file
19
forms/multipart-s3/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "multipart-s3"
|
||||
version = "0.1.0"
|
||||
authors = ["cheolgyu <38715510+cheolgyu@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "3"
|
||||
actix-multipart = "0.3"
|
||||
futures = "0.3.1"
|
||||
rusoto_s3 = "0.43.0"
|
||||
rusoto_core = "0.43.0"
|
||||
bytes = { version = "0.5", features = ["serde"] }
|
||||
serde = { version = "1.0.104", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
dotenv = "0.15.0"
|
||||
sanitize-filename = "0.2"
|
25
forms/multipart-s3/README.md
Normal file
25
forms/multipart-s3/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
# multipart+s3
|
||||
Upload a file in multipart form to aws s3(https://github.com/rusoto/rusoto)
|
||||
Receive multiple data in multipart form in JSON format and receive it as a struct.
|
||||
|
||||
# usage
|
||||
```
|
||||
cd examples/multipart+s3
|
||||
```
|
||||
1. copy .env.example .env
|
||||
2. edit .env AWS_ACCESS_KEY_ID=you_key
|
||||
3. edit .env AWS_SECRET_ACCESS_KEY=you_key
|
||||
4. edit .env AWS_S3_BUCKET_NAME=you_key
|
||||
|
||||
|
||||
# Running Server
|
||||
```
|
||||
cd examples/multipart+s3
|
||||
cargo run (or ``cargo watch -x run``)
|
||||
```
|
||||
http://localhost:3000
|
||||
|
||||
|
||||
# using other regions
|
||||
https://www.rusoto.org/regions.html
|
||||
https://docs.rs/rusoto_core/0.42.0/rusoto_core/enum.Region.html
|
104
forms/multipart-s3/src/main.rs
Normal file
104
forms/multipart-s3/src/main.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer};
|
||||
use dotenv::dotenv;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::BorrowMut;
|
||||
use std::env;
|
||||
use utils::upload::{save_file as upload_save_file, split_payload, UploadFile};
|
||||
|
||||
mod utils;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct InpAdd {
|
||||
pub text: String,
|
||||
pub number: i32,
|
||||
}
|
||||
|
||||
async fn save_file(mut payload: Multipart) -> Result<HttpResponse, Error> {
|
||||
let pl = split_payload(payload.borrow_mut()).await;
|
||||
println!("bytes={:#?}", pl.0);
|
||||
let inp_info: InpAdd = serde_json::from_slice(&pl.0).unwrap();
|
||||
println!("converter_struct={:#?}", inp_info);
|
||||
println!("tmpfiles={:#?}", pl.1);
|
||||
//make key
|
||||
let s3_upload_key = format!("projects/{}/", "posts_id");
|
||||
//create tmp file and upload s3 and remove tmp file
|
||||
let upload_files: Vec<UploadFile> =
|
||||
upload_save_file(pl.1, s3_upload_key).await.unwrap();
|
||||
println!("upload_files={:#?}", upload_files);
|
||||
Ok(HttpResponse::Ok().into())
|
||||
}
|
||||
|
||||
fn index() -> HttpResponse {
|
||||
let html = r#"<html>
|
||||
<head><title>Upload Test</title></head>
|
||||
<body>
|
||||
<form target="/" method="post" enctype="multipart/form-data" id="myForm" >
|
||||
<input type="text" id="text" name="text" value="test_text"/>
|
||||
<input type="number" id="number" name="number" value="123123"/>
|
||||
|
||||
<input type="button" value="Submit" onclick="myFunction()"></button>
|
||||
</form>
|
||||
<input type="file" multiple name="file" id="myFile"/>
|
||||
</body>
|
||||
<script>
|
||||
|
||||
function myFunction(){
|
||||
var myForm = document.getElementById('myForm');
|
||||
var myFile = document.getElementById('myFile');
|
||||
|
||||
let formData = new FormData();
|
||||
const obj = {
|
||||
text: document.getElementById('text').value,
|
||||
number: Number(document.getElementById('number').value)
|
||||
};
|
||||
const json = JSON.stringify(obj);
|
||||
console.log(obj);
|
||||
console.log(json);
|
||||
|
||||
|
||||
formData.append("data", json);
|
||||
formData.append("myFile", myFile.files[0]);
|
||||
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("POST", "");
|
||||
request.send(formData);
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</html>"#;
|
||||
|
||||
HttpResponse::Ok().body(html)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv().ok();
|
||||
let aws_access_key_id =
|
||||
env::var("AWS_ACCESS_KEY_ID").expect("AWS_ACCESS_KEY_ID must be set");
|
||||
let aws_secret_access_key =
|
||||
env::var("AWS_SECRET_ACCESS_KEY").expect("AWS_SECRET_ACCESS_KEY must be set");
|
||||
let aws_s3_bucket_name =
|
||||
env::var("AWS_S3_BUCKET_NAME").expect("AWS_S3_BUCKET_NAME must be set");
|
||||
|
||||
println!("{}", aws_access_key_id);
|
||||
println!("{}", aws_secret_access_key);
|
||||
println!("{}", aws_s3_bucket_name);
|
||||
|
||||
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
|
||||
std::fs::create_dir_all("./tmp").unwrap();
|
||||
|
||||
let ip = "0.0.0.0:3000";
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new().wrap(middleware::Logger::default()).service(
|
||||
web::resource("/")
|
||||
.route(web::get().to(index))
|
||||
.route(web::post().to(save_file)),
|
||||
)
|
||||
})
|
||||
.bind(ip)?
|
||||
.run()
|
||||
.await
|
||||
}
|
2
forms/multipart-s3/src/utils/mod.rs
Normal file
2
forms/multipart-s3/src/utils/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod s3;
|
||||
pub mod upload;
|
66
forms/multipart-s3/src/utils/s3.rs
Normal file
66
forms/multipart-s3/src/utils/s3.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use rusoto_core::Region;
|
||||
use rusoto_s3::S3;
|
||||
use rusoto_s3::{DeleteObjectRequest, PutObjectRequest, S3Client};
|
||||
use std::io::Read;
|
||||
|
||||
pub struct Client {
|
||||
#[allow(dead_code)]
|
||||
region: Region,
|
||||
s3: S3Client,
|
||||
bucket_name: String,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
// construct S3 testing client
|
||||
pub fn new() -> Client {
|
||||
let region = Region::default();
|
||||
|
||||
Client {
|
||||
region: region.to_owned(),
|
||||
s3: S3Client::new(region),
|
||||
bucket_name: std::env::var("AWS_S3_BUCKET_NAME").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url(&self, key: &str) -> String {
|
||||
format!(
|
||||
"https://{}.s3.{}.amazonaws.com/{}",
|
||||
std::env::var("AWS_S3_BUCKET_NAME").unwrap(),
|
||||
std::env::var("AWS_REGION").unwrap(),
|
||||
key
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn put_object(&self, localfilepath: &str, key: &str) -> String {
|
||||
let mut file = std::fs::File::open(localfilepath).unwrap();
|
||||
let mut contents: Vec<u8> = Vec::new();
|
||||
let _ = file.read_to_end(&mut contents);
|
||||
let put_request = PutObjectRequest {
|
||||
bucket: self.bucket_name.to_owned(),
|
||||
key: key.to_owned(),
|
||||
body: Some(contents.into()),
|
||||
..Default::default()
|
||||
};
|
||||
let _res = self
|
||||
.s3
|
||||
.put_object(put_request)
|
||||
.await
|
||||
.expect("Failed to put test object");
|
||||
|
||||
self.url(key)
|
||||
}
|
||||
|
||||
pub async fn delete_object(&self, key: String) {
|
||||
let delete_object_req = DeleteObjectRequest {
|
||||
bucket: self.bucket_name.to_owned(),
|
||||
key: key.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _res = self
|
||||
.s3
|
||||
.delete_object(delete_object_req)
|
||||
.await
|
||||
.expect("Couldn't delete object");
|
||||
}
|
||||
}
|
124
forms/multipart-s3/src/utils/upload.rs
Normal file
124
forms/multipart-s3/src/utils/upload.rs
Normal file
@ -0,0 +1,124 @@
|
||||
use crate::utils::s3::Client;
|
||||
use actix_multipart::{Field, Multipart};
|
||||
use actix_web::{web, Error};
|
||||
use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::From;
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct UploadFile {
|
||||
pub filename: String,
|
||||
pub key: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl From<Tmpfile> for UploadFile {
|
||||
fn from(tmp_file: Tmpfile) -> Self {
|
||||
UploadFile {
|
||||
filename: tmp_file.name,
|
||||
key: tmp_file.s3_key,
|
||||
url: tmp_file.s3_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
1. savefile
|
||||
2. s3 upload -> upload_data
|
||||
3. deletefile
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tmpfile {
|
||||
pub name: String,
|
||||
pub tmp_path: String,
|
||||
pub s3_key: String,
|
||||
pub s3_url: String,
|
||||
}
|
||||
impl Tmpfile {
|
||||
fn new(filename: &str) -> Tmpfile {
|
||||
Tmpfile {
|
||||
name: filename.to_string(),
|
||||
tmp_path: format!("./tmp/{}", filename),
|
||||
s3_key: "".to_string(),
|
||||
s3_url: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn s3_upload_and_tmp_remove(&mut self, s3_upload_key: String) {
|
||||
self.s3_upload(s3_upload_key).await;
|
||||
self.tmp_remove();
|
||||
}
|
||||
|
||||
async fn s3_upload(&mut self, s3_upload_key: String) {
|
||||
let key = format!("{}{}", &s3_upload_key, &self.name);
|
||||
self.s3_key = key.clone();
|
||||
let url: String = Client::new().put_object(&self.tmp_path, &key.clone()).await;
|
||||
self.s3_url = url;
|
||||
}
|
||||
|
||||
fn tmp_remove(&self) {
|
||||
std::fs::remove_file(&self.tmp_path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn split_payload(payload: &mut Multipart) -> (bytes::Bytes, Vec<Tmpfile>) {
|
||||
let mut tmp_files: Vec<Tmpfile> = Vec::new();
|
||||
let mut data = Bytes::new();
|
||||
|
||||
while let Some(item) = payload.next().await {
|
||||
let mut field: Field = item.expect(" split_payload err");
|
||||
let content_type = field.content_disposition().unwrap();
|
||||
let name = content_type.get_name().unwrap();
|
||||
if name == "data" {
|
||||
while let Some(chunk) = field.next().await {
|
||||
data = chunk.expect(" split_payload err chunk");
|
||||
}
|
||||
} else {
|
||||
match content_type.get_filename() {
|
||||
Some(filename) => {
|
||||
let tmp_file = Tmpfile::new(&sanitize_filename::sanitize(&filename));
|
||||
let tmp_path = tmp_file.tmp_path.clone();
|
||||
let mut f = web::block(move || std::fs::File::create(&tmp_path))
|
||||
.await
|
||||
.unwrap();
|
||||
while let Some(chunk) = field.next().await {
|
||||
let data = chunk.unwrap();
|
||||
f = web::block(move || f.write_all(&data).map(|_| f))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
tmp_files.push(tmp_file.clone());
|
||||
}
|
||||
None => {
|
||||
println!("file none");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(data, tmp_files)
|
||||
}
|
||||
|
||||
pub async fn save_file(
|
||||
tmp_files: Vec<Tmpfile>,
|
||||
s3_upload_key: String,
|
||||
) -> Result<Vec<UploadFile>, Error> {
|
||||
let mut arr: Vec<UploadFile> = Vec::with_capacity(tmp_files.len());
|
||||
|
||||
for item in tmp_files {
|
||||
let mut tmp_file: Tmpfile = item.clone();
|
||||
tmp_file
|
||||
.s3_upload_and_tmp_remove(s3_upload_key.clone())
|
||||
.await;
|
||||
arr.push(UploadFile::from(tmp_file));
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub async fn delete_object(list: Vec<String>) {
|
||||
for key in list {
|
||||
Client::new().delete_object(key).await;
|
||||
}
|
||||
}
|
3
forms/multipart/.gitignore
vendored
Normal file
3
forms/multipart/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
/tmp
|
16
forms/multipart/Cargo.toml
Normal file
16
forms/multipart/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "multipart-example"
|
||||
version = "0.3.0"
|
||||
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
description = "Simple file uploader in Actix Web with Async/Await"
|
||||
keywords = ["actix", "actix-web", "multipart"]
|
||||
repository = "https://github.com/actix/examples"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
actix-multipart = "0.3"
|
||||
actix-web = "3"
|
||||
futures = "0.3.1"
|
||||
sanitize-filename = "0.2"
|
21
forms/multipart/LICENSE
Normal file
21
forms/multipart/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [2019] [Bevan Hunt]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
11
forms/multipart/README.md
Normal file
11
forms/multipart/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Actix Web File Upload with Async/Await
|
||||
|
||||
### Run
|
||||
|
||||
``` open web browser to localhost:3000 and upload file(s) ```
|
||||
|
||||
### Result
|
||||
|
||||
``` file(s) will show up in ./tmp in the same directory as the running process ```
|
||||
|
||||
Note: this is a naive implementation and will panic on any error
|
60
forms/multipart/src/main.rs
Normal file
60
forms/multipart/src/main.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use std::io::Write;
|
||||
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
|
||||
async fn save_file(mut payload: Multipart) -> Result<HttpResponse, Error> {
|
||||
// iterate over multipart stream
|
||||
while let Ok(Some(mut field)) = payload.try_next().await {
|
||||
let content_type = field.content_disposition().unwrap();
|
||||
let filename = content_type.get_filename().unwrap();
|
||||
let filepath = format!("./tmp/{}", sanitize_filename::sanitize(&filename));
|
||||
|
||||
// File::create is blocking operation, use threadpool
|
||||
let mut f = web::block(|| std::fs::File::create(filepath))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Field in turn is stream of *Bytes* object
|
||||
while let Some(chunk) = field.next().await {
|
||||
let data = chunk.unwrap();
|
||||
// filesystem operations are blocking, we have to use threadpool
|
||||
f = web::block(move || f.write_all(&data).map(|_| f)).await?;
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse::Ok().into())
|
||||
}
|
||||
|
||||
fn index() -> HttpResponse {
|
||||
let html = r#"<html>
|
||||
<head><title>Upload Test</title></head>
|
||||
<body>
|
||||
<form target="/" method="post" enctype="multipart/form-data">
|
||||
<input type="file" multiple name="file"/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>"#;
|
||||
|
||||
HttpResponse::Ok().body(html)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
|
||||
std::fs::create_dir_all("./tmp").unwrap();
|
||||
|
||||
let ip = "0.0.0.0:3000";
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new().wrap(middleware::Logger::default()).service(
|
||||
web::resource("/")
|
||||
.route(web::get().to(index))
|
||||
.route(web::post().to(save_file)),
|
||||
)
|
||||
})
|
||||
.bind(ip)?
|
||||
.run()
|
||||
.await
|
||||
}
|
Reference in New Issue
Block a user