Upload file in Rust e Axum
In questo articolo vediamo come fare un upload in Rust e Axum.
Vi metterò solo il minimo indispensabile, quindi potrebbe essere che dobbiate integrare qualcosa da voi.
Partiamo dal Cargo.toml:
[dependencies]
axum = { version = "0.8.6", features = ["multipart"] }
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.47.1", features = ["full"] }
tower-http = { version = "0.6.6", features = ["cors"] }
sqlx = { version = "0.8.6", features = ["mysql", "runtime-tokio-rustls", "macros", "chrono", "rust_decimal"] }
anyhow = "1.0.99"
dotenvy = "0.15.7"
serde_json = "1.0.145"
chrono = { version = "0.4.42", features = ["serde"] }
jsonwebtoken = { version = "10", features = ["rust_crypto"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.20"
tracing-appender = "0.2.3"
rust_decimal = "1.38.0"
rust_decimal_macros = "1.38.0"
bytes = "1.10.1"
Io vi ho messo tutte le dipendenze che ho, ma in realtà dovete attivare il multipart di Axum e installare bytes per l'upload in se.
Poi dentro utils.rs ho creato due funzioni, di cui una per l'upload e l'altra per impostare il nome del file:
use bytes::Bytes;
use std::path::Path;
use std::{fs, io::Write};
use tokio::task;
pub async fn save_uploaded_file(
base_dir: &str,
original_name: &str,
file_bytes: &Bytes,
allowed_types: &[&str],
max_size: usize,
) -> Result<String, String> {
let ext = Path::new(&original_name)
.extension()
.and_then(|s| s.to_str())
.ok_or_else(|| "File senza estensione".to_string())?
.to_lowercase();
if !allowed_types.contains(&ext.as_str()) {
return Err(format!("Tipo file non consentito: {}", ext));
}
if file_bytes.len() > max_size {
return Err(format!(
"File troppo grande: {} byte, massimo {} byte",
file_bytes.len(),
max_size
));
}
fs::create_dir_all(base_dir).map_err(|e| format!("Errore creazione cartella: {}", e))?;
let save_path = format!("{}/{}", base_dir, original_name);
let bytes = file_bytes.clone();
task::spawn_blocking(move || -> Result<String, String> {
let mut file =
fs::File::create(&save_path).map_err(|e| format!("Errore creazione file: {}", e))?;
file.write_all(&bytes)
.map_err(|e| format!("Errore scrittura file: {}", e))?;
Ok(save_path)
})
.await
.map_err(|e| format!("Errore thread: {}", e))?
}
pub fn sanitize_name(name: &str) -> String {
let mut s = name.trim().to_lowercase();
let cerca = [
"à", "è", "é", "ì", "ò", "ù", "'", "?", " ", "__", "&", "%", "#", "(", ")", "/", "+", "°",
];
let sostituisci = [
"a",
"e",
"e",
"i",
"o",
"u",
"-",
"-",
"-",
"-",
"e",
"-per-cento-",
"-",
"",
"",
"-",
"_",
"_",
];
for (c, r) in cerca.iter().zip(sostituisci.iter()) {
s = s.replace(c, r);
}
s = s.replace("---", "-");
s
}
Poi ho creato un service:
pub async fn upload_logo(
original_name: &str,
bytes: &Bytes,
) -> Result<String, String> {
let base_dir = "IMG_UTENTI_GEST/ANAG_GEN";
let file_name = utils::sanitize_name(original_name);
utils::save_uploaded_file(
base_dir,
&file_name,
bytes,
&["png", "jpg", "jpeg"],
2 * 1024 * 1024,
)
.await
}
E questo il controller:
pub async fn upload_logo(
State(_state): State<AppState>,
mut multipart: Multipart,
) -> impl IntoResponse {
let mut id: Option<String> = None;
let mut file_bytes: Option<(String, bytes::Bytes)> = None;
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
let name = field.name().unwrap_or("").to_string();
if name == "id" {
id = Some(field.text().await.unwrap_or_default());
} else if name == "file" {
let file_name = field.file_name().unwrap_or("upload.bin").to_string();
let bytes = field.bytes().await.unwrap_or_default();
file_bytes = Some((file_name, bytes));
}
}
if id.is_none() || file_bytes.is_none() {
let response = UploadLogoResponse::Ko {
res: "ko".to_string(),
message: "Manca id o file".to_string(),
error: "Parametri incompleti".to_string(),
};
return Json(response);
}
let id = id.unwrap();
let (file_name, bytes) = file_bytes.unwrap();
match anagrafiche_service::upload_logo(&id, &file_name, &bytes).await {
Ok(path) => {
let response = UploadLogoResponse::Ok {
res: "ok".to_string(),
message: "File caricato correttamente".to_string(),
img: Some(path),
};
Json(response)
}
Err(e) => {
error!("Errore in upload_logo {:?}", e);
let response = UploadLogoResponse::Ko {
res: "ko".to_string(),
message: "Si è verificato un errore durante il caricamento".to_string(),
error: e.to_string(),
};
Json(response)
}
}
}
Che risponde con una enum:
#[derive(Serialize)]
#[serde(untagged)]
pub enum UploadLogoResponse {
Ok {
res: String,
message: String,
img: Option,
},
Ko {
res: String,
message: String,
error: String,
},
}
Enjoy!
rust cargo axum multipart
Commentami!