File Upload Guide
File Upload Guide for Ignitia
📋 Table of Contents
File Uploads🔗
Learn how to handle file uploads in Ignitia using multipart form data processing.
Overview🔗
Ignitia provides robust support for handling file uploads through multipart/form-data requests. The framework includes a streaming multipart parser that can handle large files efficiently, with configurable limits and automatic memory management.
Key Features🔗
- Streaming Processing: Handle large files without loading everything into memory
- Configurable Limits: Set maximum file sizes, request sizes, and field counts
- Automatic File Management: Smart handling of small vs large files
- Type Safety: Strong typing with extractors and error handling
- Async Support: Non-blocking file operations throughout
Basic Usage🔗
Simple File Upload Handler🔗
use ignitia::{Router, Response, Result, multipart::Multipart};
async fn upload_handler(mut multipart: Multipart) -> Result<Response> {
while let Some(field) = multipart.next_field().await? {
if field.is_file() {
let file_name = field.file_name().unwrap_or("unknown");
let content_type = field.content_type().unwrap_or("application/octet-stream");
println!("Uploading file: {} ({})", file_name, content_type);
// Save file to disk
let file_path = format!("./uploads/{}", file_name);
let saved_file = field.save_to_file(&file_path).await?;
println!("File saved to: {:?} ({} bytes)", saved_file.path, saved_file.size);
} else {
// Handle text fields
let field_name = field.name();
let text_value = field.text().await?;
println!("Text field '{}': {}", field_name, text_value);
}
}
Ok(Response::text("Upload successful!"))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new()
.post("/upload", upload_handler);
ignitia::Server::new(app, "127.0.0.1:3000".parse()?)
.ignitia()
.await?;
Ok(())
}
Configuration🔗
MultipartConfig🔗
Customize upload behavior with MultipartConfig
:
use ignitia::multipart::{Multipart, MultipartConfig};
// Create custom configuration
let config = MultipartConfig {
max_request_size: 50 * 1024 * 1024, // 50MB total request
max_field_size: 10 * 1024 * 1024, // 10MB per field
file_size_threshold: 1024 * 1024, // 1MB before writing to disk
max_fields: 50, // Maximum 50 fields
};
// Use in handler
async fn upload_with_config(request: Request) -> Result<Response> {
// Extract boundary manually for custom config
let content_type = request.header("content-type")
.ok_or_else(|| Error::BadRequest("Missing Content-Type".into()))?;
let boundary = extract_boundary(content_type)
.ok_or_else(|| Error::BadRequest("Missing boundary".into()))?;
let mut multipart = Multipart::new(request.body, boundary, config);
// Process fields...
Ok(Response::text("Upload complete"))
}
Default Limits🔗
// Default configuration values:
MultipartConfig {
max_request_size: 10 * 1024 * 1024, // 10MB
max_field_size: 1 * 1024 * 1024, // 1MB
file_size_threshold: 256 * 1024, // 256KB
max_fields: 100, // 100 fields
}
Working with Fields🔗
Field Types🔗
use ignitia::multipart::{Field, FileField, TextField};
async fn process_field(field: Field) -> Result<()> {
println!("Field name: {}", field.name());
if let Some(filename) = field.file_name() {
println!("Filename: {}", filename);
}
if let Some(content_type) = field.content_type() {
println!("Content-Type: {}", content_type);
}
// Check field type
if field.is_file() {
// Handle as file
let bytes = field.bytes().await?;
println!("File size: {} bytes", bytes.len());
} else {
// Handle as text
let text = field.text().await?;
println!("Text value: {}", text);
}
Ok(())
}
Saving Files🔗
async fn save_upload(field: Field) -> Result<FileField> {
let filename = field.file_name()
.unwrap_or("unknown")
.to_string();
// Create safe filename
let safe_filename = sanitize_filename(&filename);
let file_path = format!("./uploads/{}", safe_filename);
// Save to disk
let saved_file = field.save_to_file(&file_path).await?;
println!("Saved {} ({} bytes) to {:?}",
filename, saved_file.size, saved_file.path);
Ok(saved_file)
}
fn sanitize_filename(filename: &str) -> String {
filename.chars()
.filter(|c| c.is_alphanumeric() || *c == '.' || *c == '_' || *c == '-')
.collect()
}
Advanced Examples🔗
Image Upload with Validation🔗
use ignitia::{Response, Result, multipart::Multipart};
use std::path::Path;
async fn upload_image(mut multipart: Multipart) -> Result<Response> {
while let Some(field) = multipart.next_field().await? {
if field.is_file() {
// Validate image type
let content_type = field.content_type().unwrap_or("");
if !is_image_type(content_type) {
return Ok(Response::new(400)
.with_body("Only image files are allowed"));
}
let filename = field.file_name().unwrap_or("image");
let extension = get_extension(content_type);
let safe_name = format!("{}_{}.{}",
chrono::Utc::now().timestamp(),
sanitize_filename(filename),
extension
);
let file_path = format!("./images/{}", safe_name);
let saved_file = field.save_to_file(&file_path).await?;
return Ok(Response::json(serde_json::json!({
"success": true,
"filename": safe_name,
"size": saved_file.size,
"url": format!("/images/{}", safe_name)
}))?);
}
}
Ok(Response::new(400).with_body("No image file found"))
}
fn is_image_type(content_type: &str) -> bool {
matches!(content_type,
"image/jpeg" | "image/jpg" | "image/png" |
"image/gif" | "image/webp"
)
}
fn get_extension(content_type: &str) -> &str {
match content_type {
"image/jpeg" | "image/jpg" => "jpg",
"image/png" => "png",
"image/gif" => "gif",
"image/webp" => "webp",
_ => "bin"
}
}
Multiple File Upload🔗
async fn upload_multiple(mut multipart: Multipart) -> Result<Response> {
let mut uploaded_files = Vec::new();
let mut text_fields = std::collections::HashMap::new();
while let Some(field) = multipart.next_field().await? {
if field.is_file() {
let filename = field.file_name().unwrap_or("unknown").to_string();
let file_path = format!("./uploads/{}", sanitize_filename(&filename));
let saved_file = field.save_to_file(&file_path).await?;
uploaded_files.push(serde_json::json!({
"original_name": filename,
"saved_path": saved_file.path,
"size": saved_file.size,
"content_type": saved_file.content_type
}));
} else {
let name = field.name().to_string();
let value = field.text().await?;
text_fields.insert(name, value);
}
}
Ok(Response::json(serde_json::json!({
"files_uploaded": uploaded_files.len(),
"files": uploaded_files,
"form_data": text_fields
}))?)
}
Streaming Large Files🔗
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
async fn stream_large_file(mut multipart: Multipart) -> Result<Response> {
while let Some(mut field) = multipart.next_field().await? {
if field.is_file() {
let filename = field.file_name().unwrap_or("large_file");
let file_path = format!("./large_uploads/{}", sanitize_filename(filename));
// Create file for streaming
let mut file = File::create(&file_path).await
.map_err(|e| Error::Internal(format!("Failed to create file: {}", e)))?;
let mut total_bytes = 0u64;
// Stream file chunks directly to disk
while let Some(chunk) = field.chunk().await
.map_err(|e| Error::Internal(format!("Stream error: {}", e)))?
{
file.write_all(&chunk).await
.map_err(|e| Error::Internal(format!("Write error: {}", e)))?;
total_bytes += chunk.len() as u64;
}
file.sync_all().await
.map_err(|e| Error::Internal(format!("Sync error: {}", e)))?;
return Ok(Response::json(serde_json::json!({
"success": true,
"filename": filename,
"bytes_written": total_bytes,
"path": file_path
}))?);
}
}
Ok(Response::new(400).with_body("No file found"))
}
Error Handling🔗
Common Upload Errors🔗
use ignitia::multipart::MultipartError;
async fn upload_with_errors(mut multipart: Multipart) -> Result<Response> {
match process_upload(&mut multipart).await {
Ok(result) => Ok(Response::json(result)?),
Err(e) => {
let (status, message) = match e {
Error::Custom(custom_err) => {
if let Some(multipart_err) = custom_err.downcast_ref::<MultipartError>() {
match multipart_err {
MultipartError::FieldTooLarge { field_name, max_size } => {
(413, format!("Field '{}' exceeds {}MB limit", field_name, max_size / 1024 / 1024))
},
MultipartError::RequestTooLarge { max_size } => {
(413, format!("Request exceeds {}MB limit", max_size / 1024 / 1024))
},
MultipartError::TooManyFields { max_fields } => {
(400, format!("Too many fields, maximum {}", max_fields))
},
MultipartError::InvalidBoundary => {
(400, "Invalid multipart boundary".to_string())
},
_ => (400, "Upload processing failed".to_string())
}
} else {
(500, "Internal server error".to_string())
}
},
_ => (500, "Unexpected error".to_string())
};
Ok(Response::new(status).with_body(message))
}
}
}
HTML Form Example🔗
Client-Side Form🔗
<!DOCTYPE html>
<html>
<head>
<title>File Upload</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<div>
<label for="title">Title:</label>
<input type="text" id="title" name="title" required>
</div>
<div>
<label for="description">Description:</label>
<textarea id="description" name="description"></textarea>
</div>
<div>
<label for="file">Choose file:</label>
<input type="file" id="file" name="file" required>
</div>
<div>
<label for="multiple">Multiple files:</label>
<input type="file" id="multiple" name="multiple[]" multiple>
</div>
<button type="submit">Upload</button>
</form>
</body>
</html>
Best Practices🔗
Security Considerations🔗
use std::path::Path;
fn validate_upload(field: &Field) -> Result<()> {
// 1. Check file extension
if let Some(filename) = field.file_name() {
let allowed_extensions = ["jpg", "jpeg", "png", "gif", "pdf", "txt"];
let extension = Path::new(filename)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_lowercase();
if !allowed_extensions.contains(&extension.as_str()) {
return Err(Error::BadRequest("File type not allowed".into()));
}
}
// 2. Check content type
if let Some(content_type) = field.content_type() {
let allowed_types = [
"image/jpeg", "image/png", "image/gif",
"application/pdf", "text/plain"
];
if !allowed_types.contains(&content_type) {
return Err(Error::BadRequest("Content type not allowed".into()));
}
}
// 3. Validate file name
if let Some(filename) = field.file_name() {
if filename.contains("..") || filename.contains("/") || filename.contains("\\") {
return Err(Error::BadRequest("Invalid filename".into()));
}
}
Ok(())
}
Performance Tips🔗
- Use streaming for large files to avoid memory issues
- Configure appropriate thresholds for memory vs disk usage
- Validate early to reject invalid uploads quickly
- Use async file operations to avoid blocking
- Implement cleanup for failed uploads
Directory Structure🔗
use tokio::fs;
async fn ensure_upload_dirs() -> Result<()> {
fs::create_dir_all("./uploads/images").await
.map_err(|e| Error::Internal(format!("Failed to create upload dirs: {}", e)))?;
fs::create_dir_all("./uploads/documents").await
.map_err(|e| Error::Internal(format!("Failed to create upload dirs: {}", e)))?;
Ok(())
}
This comprehensive guide covers file upload handling in Ignitia, from basic usage to advanced streaming scenarios with proper error handling and security considerations.