mirror of
https://github.com/actix/examples
synced 2025-06-26 17:17:42 +02:00
update multipart examples to use derive macro
This commit is contained in:
@ -8,8 +8,8 @@ actix-multipart.workspace = true
|
||||
actix-web.workspace = true
|
||||
actix-web-lab.workspace = true
|
||||
|
||||
aws-config = "0.52"
|
||||
aws-sdk-s3 = "0.22"
|
||||
aws-config = "0.54"
|
||||
aws-sdk-s3 = "0.24"
|
||||
|
||||
dotenv = "0.15"
|
||||
env_logger.workspace = true
|
||||
|
@ -71,14 +71,16 @@ impl Client {
|
||||
|
||||
async fn upload_and_remove(&self, file: TempFile, key_prefix: &str) -> UploadedFile {
|
||||
let uploaded_file = self.upload(&file, key_prefix).await;
|
||||
file.delete_from_disk().await;
|
||||
tokio::fs::remove_file(file.file.path()).await.unwrap();
|
||||
uploaded_file
|
||||
}
|
||||
|
||||
async fn upload(&self, file: &TempFile, key_prefix: &str) -> UploadedFile {
|
||||
let filename = file.name();
|
||||
let key = format!("{key_prefix}{}", file.name());
|
||||
let s3_url = self.put_object_from_file(file.path(), &key).await;
|
||||
let filename = file.file_name.as_deref().expect("TODO");
|
||||
let key = format!("{key_prefix}{filename}");
|
||||
let s3_url = self
|
||||
.put_object_from_file(file.file.path().to_str().unwrap(), &key)
|
||||
.await;
|
||||
UploadedFile::new(filename, key, s3_url)
|
||||
}
|
||||
|
||||
|
@ -19,10 +19,7 @@
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
const meta = { namespace: $form.namespace.value };
|
||||
console.log(meta);
|
||||
|
||||
formData.append("meta", JSON.stringify(meta));
|
||||
formData.append("namespace", $form.namespace.value);
|
||||
|
||||
for (const file in $files.files) {
|
||||
formData.append("file", file);
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::fs;
|
||||
|
||||
use actix_multipart::Multipart;
|
||||
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
|
||||
use actix_web::{
|
||||
body::SizedStream, delete, error, get, middleware::Logger, post, web, App, Error, HttpResponse,
|
||||
HttpServer, Responder,
|
||||
@ -8,51 +8,41 @@ use actix_web::{
|
||||
use actix_web_lab::{extract::Path, respond::Html};
|
||||
use aws_config::meta::region::RegionProviderChain;
|
||||
use dotenv::dotenv;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
mod client;
|
||||
mod temp_file;
|
||||
mod upload_file;
|
||||
mod utils;
|
||||
|
||||
use self::{client::Client, temp_file::TempFile, upload_file::UploadedFile, utils::split_payload};
|
||||
use self::{client::Client, upload_file::UploadedFile};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UploadMeta {
|
||||
namespace: String,
|
||||
}
|
||||
#[derive(Debug, MultipartForm)]
|
||||
struct UploadForm {
|
||||
namespace: Text<String>,
|
||||
|
||||
impl Default for UploadMeta {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
namespace: "default".to_owned(),
|
||||
}
|
||||
}
|
||||
#[multipart(rename = "file")]
|
||||
files: Vec<TempFile>,
|
||||
}
|
||||
|
||||
#[post("/")]
|
||||
async fn upload_to_s3(
|
||||
s3_client: web::Data<Client>,
|
||||
mut payload: Multipart,
|
||||
MultipartForm(form): MultipartForm<UploadForm>,
|
||||
) -> Result<impl Responder, Error> {
|
||||
let (data, files) = split_payload(&mut payload).await;
|
||||
log::info!("bytes = {data:?}");
|
||||
let namespace = form.namespace.into_inner();
|
||||
let files = form.files;
|
||||
|
||||
let upload_meta = serde_json::from_slice::<UploadMeta>(&data).unwrap_or_default();
|
||||
log::info!("converter_struct = {upload_meta:?}");
|
||||
log::info!("namespace = {namespace:?}");
|
||||
log::info!("tmp_files = {files:?}");
|
||||
|
||||
// make key prefix (make sure it ends with a forward slash)
|
||||
let s3_key_prefix = format!("uploads/{}/", upload_meta.namespace);
|
||||
let s3_key_prefix = format!("uploads/{namespace}/");
|
||||
|
||||
// create tmp file and upload s3 and remove tmp file
|
||||
// upload temp files to s3 and then remove them
|
||||
let uploaded_files = s3_client.upload_files(files, &s3_key_prefix).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({
|
||||
"uploadedFiles": uploaded_files,
|
||||
"meta": upload_meta,
|
||||
"meta": json!({ "namespace": namespace }),
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -1,35 +0,0 @@
|
||||
use tokio::fs;
|
||||
|
||||
/// Info for a temporary file to be uploaded to S3.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TempFile {
|
||||
path: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl TempFile {
|
||||
/// Constructs info container with sanitized file name.
|
||||
pub fn new(filename: &str) -> TempFile {
|
||||
let filename = sanitize_filename::sanitize(filename);
|
||||
|
||||
TempFile {
|
||||
path: format!("./tmp/{filename}"),
|
||||
name: filename,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns name of temp file.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Returns path to temp file.
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Deletes temp file from disk.
|
||||
pub async fn delete_from_disk(self) {
|
||||
fs::remove_file(&self.path).await.unwrap();
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
use actix_multipart::{Field, Multipart};
|
||||
use actix_web::web::{Bytes, BytesMut};
|
||||
use futures_util::StreamExt as _;
|
||||
use tokio::{fs, io::AsyncWriteExt as _};
|
||||
|
||||
use crate::TempFile;
|
||||
|
||||
/// Returns tuple of `meta` field contents and a list of temp file info to upload.
|
||||
pub async fn split_payload(payload: &mut Multipart) -> (Bytes, Vec<TempFile>) {
|
||||
let mut meta = Bytes::new();
|
||||
let mut temp_files = vec![];
|
||||
|
||||
while let Some(item) = payload.next().await {
|
||||
let mut field = item.expect("split_payload err");
|
||||
let cd = field.content_disposition();
|
||||
|
||||
if matches!(cd.get_name(), Some(name) if name == "meta") {
|
||||
// if field name is "meta", just collect those bytes in-memory and return them later
|
||||
meta = collect_meta(&mut field).await;
|
||||
} else {
|
||||
match cd.get_filename() {
|
||||
Some(filename) => {
|
||||
// if file has a file name, we stream the field contents into a temp file on
|
||||
// disk so that large uploads do not exhaust memory
|
||||
|
||||
// create file info
|
||||
let file_info = TempFile::new(filename);
|
||||
|
||||
// create file on disk from file info
|
||||
let mut file = fs::File::create(file_info.path()).await.unwrap();
|
||||
|
||||
// stream field contents to file
|
||||
while let Some(chunk) = field.next().await {
|
||||
let data = chunk.unwrap();
|
||||
file.write_all(&data).await.unwrap();
|
||||
}
|
||||
|
||||
// return file info
|
||||
temp_files.push(file_info);
|
||||
}
|
||||
|
||||
None => {
|
||||
log::warn!("field {:?} is not a file", cd.get_name());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(meta, temp_files)
|
||||
}
|
||||
|
||||
async fn collect_meta(field: &mut Field) -> Bytes {
|
||||
let mut buf = BytesMut::new();
|
||||
|
||||
while let Some(chunk) = field.next().await {
|
||||
let chunk = chunk.expect("split_payload err chunk");
|
||||
buf.extend(chunk);
|
||||
}
|
||||
|
||||
buf.freeze()
|
||||
}
|
Reference in New Issue
Block a user