1
0
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:
Rob Ede
2023-02-26 21:56:55 +00:00
parent 9b77b033b0
commit 080db60175
10 changed files with 309 additions and 241 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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);

View 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 }),
})))
}

View File

@ -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();
}
}

View File

@ -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()
}