-
-
Notifications
You must be signed in to change notification settings - Fork 2
docs: Enterprise Learning Path & Testing Recipe #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| { | ||
| "last_processed_ref": "v0.1.335", | ||
| "date": "2025-02-24", | ||
| "notes": "Added background jobs recipe and expanded learning path with Module 10." | ||
| "notes": "Added Phase 4 (Enterprise Scale) to Learning Path, created Testing recipe, and updated File Uploads recipe." | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,80 +1,133 @@ | ||||||||||||||||||||||||||||
| # File Uploads | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Handling file uploads efficiently is crucial. RustAPI allows you to stream `Multipart` data, meaning you can handle 1GB uploads without using 1GB of RAM. | ||||||||||||||||||||||||||||
| Handling file uploads efficiently is crucial for modern applications. RustAPI provides a `Multipart` extractor that allows you to stream uploads, enabling you to handle large files (e.g., 1GB+) without consuming proportional RAM. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ## Dependencies | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Add `uuid` and `tokio` with `fs` features to your `Cargo.toml`. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ```toml | ||||||||||||||||||||||||||||
| [dependencies] | ||||||||||||||||||||||||||||
| rustapi-rs = "0.1.335" | ||||||||||||||||||||||||||||
| tokio = { version = "1", features = ["fs", "io-util"] } | ||||||||||||||||||||||||||||
| uuid = { version = "1", features = ["v4"] } | ||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ## Streaming Upload Handler | ||||||||||||||||||||||||||||
| ## Streaming Upload Example | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| This handler reads the incoming stream part-by-part and writes it directly to disk (or S3). | ||||||||||||||||||||||||||||
| Here is a complete, runnable example of a file upload server that streams files to a `./uploads` directory. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ```rust | ||||||||||||||||||||||||||||
| use rustapi_rs::prelude::*; | ||||||||||||||||||||||||||||
| use rustapi_rs::extract::Multipart; | ||||||||||||||||||||||||||||
| use rustapi_rs::extract::{Multipart, DefaultBodyLimit}; | ||||||||||||||||||||||||||||
| use tokio::fs::File; | ||||||||||||||||||||||||||||
|
Comment on lines
21
to
23
|
||||||||||||||||||||||||||||
| use tokio::io::AsyncWriteExt; | ||||||||||||||||||||||||||||
| use std::path::Path; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #[tokio::main] | ||||||||||||||||||||||||||||
| async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { | ||||||||||||||||||||||||||||
| // Ensure uploads directory exists | ||||||||||||||||||||||||||||
| tokio::fs::create_dir_all("./uploads").await?; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| println!("Starting Upload Server at http://127.0.0.1:8080"); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| RustApi::new() | ||||||||||||||||||||||||||||
| .route("/upload", post(upload_handler)) | ||||||||||||||||||||||||||||
| // Increase body limit to 1GB (default is usually 2MB) | ||||||||||||||||||||||||||||
| .layer(DefaultBodyLimit::max(1024 * 1024 * 1024)) | ||||||||||||||||||||||||||||
| .run("127.0.0.1:8080") | ||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+38
|
||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async fn upload_file(mut multipart: Multipart) -> Result<StatusCode, ApiError> { | ||||||||||||||||||||||||||||
| // Iterate over the fields | ||||||||||||||||||||||||||||
| while let Some(field) = multipart.next_field().await.map_err(|_| ApiError::BadRequest)? { | ||||||||||||||||||||||||||||
| async fn upload_handler(mut multipart: Multipart) -> Result<Json<UploadResponse>> { | ||||||||||||||||||||||||||||
| let mut uploaded_files = Vec::new(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Iterate over the fields in the multipart form | ||||||||||||||||||||||||||||
| while let Some(mut field) = multipart.next_field().await.map_err(|_| ApiError::bad_request("Invalid multipart"))? { | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| let name = field.name().unwrap_or("file").to_string(); | ||||||||||||||||||||||||||||
| let file_name = field.file_name().unwrap_or("unknown.bin").to_string(); | ||||||||||||||||||||||||||||
| let content_type = field.content_type().unwrap_or("application/octet-stream").to_string(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| println!("Uploading: {} ({})", file_name, content_type); | ||||||||||||||||||||||||||||
| // ⚠️ Security: Never trust the user-provided filename directly! | ||||||||||||||||||||||||||||
| // It could contain paths like "../../../etc/passwd". | ||||||||||||||||||||||||||||
| // Always generate a safe filename or sanitize inputs. | ||||||||||||||||||||||||||||
| let safe_filename = format!("{}-{}", uuid::Uuid::new_v4(), file_name); | ||||||||||||||||||||||||||||
| let path = Path::new("./uploads").join(&safe_filename); | ||||||||||||||||||||||||||||
|
Comment on lines
+51
to
+55
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Security: Create a safe random filename to prevent overwrites or path traversal | ||||||||||||||||||||||||||||
| let new_filename = format!("{}-{}", uuid::Uuid::new_v4(), file_name); | ||||||||||||||||||||||||||||
| let path = std::path::Path::new("./uploads").join(new_filename); | ||||||||||||||||||||||||||||
| println!("Streaming file: {} -> {:?}", file_name, path); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Open destination file | ||||||||||||||||||||||||||||
| let mut file = File::create(&path).await.map_err(|e| ApiError::InternalServerError(e.to_string()))?; | ||||||||||||||||||||||||||||
| let mut file = File::create(&path).await.map_err(|e| ApiError::internal(e.to_string()))?; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Write stream to file chunk by chunk | ||||||||||||||||||||||||||||
| // In RustAPI/Axum multipart, `field.bytes()` loads the whole field into memory. | ||||||||||||||||||||||||||||
| // To stream efficiently, we use `field.chunk()`: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| while let Some(chunk) = field.chunk().await.map_err(|_| ApiError::BadRequest)? { | ||||||||||||||||||||||||||||
| file.write_all(&chunk).await.map_err(|e| ApiError::InternalServerError(e.to_string()))?; | ||||||||||||||||||||||||||||
| // Stream the field content chunk-by-chunk | ||||||||||||||||||||||||||||
| // This is memory efficient even for large files. | ||||||||||||||||||||||||||||
| while let Some(chunk) = field.chunk().await.map_err(|_| ApiError::bad_request("Stream error"))? { | ||||||||||||||||||||||||||||
| file.write_all(&chunk).await.map_err(|e| ApiError::internal(e.to_string()))?; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+62
to
66
|
||||||||||||||||||||||||||||
| // Stream the field content chunk-by-chunk | |
| // This is memory efficient even for large files. | |
| while let Some(chunk) = field.chunk().await.map_err(|_| ApiError::bad_request("Stream error"))? { | |
| file.write_all(&chunk).await.map_err(|e| ApiError::internal(e.to_string()))?; | |
| } | |
| // Read the entire field content as bytes and write it to disk | |
| let data = field | |
| .bytes() | |
| .await | |
| .map_err(|_| ApiError::bad_request("Read error"))?; | |
| file.write_all(&data) | |
| .await | |
| .map_err(|e| ApiError::internal(e.to_string()))?; |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section refers to field.chunk() streaming and DefaultBodyLimit::max(...), but neither exists in the current RustAPI API. Consider rewriting this to reflect the actual behavior (buffered multipart parsing) and the supported configuration knobs (RustApi::body_limit(...), BodyLimitLayer, and/or documenting MultipartField::save_to filename sanitization).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This introduction claims
Multipartstreams uploads and can handle 1GB+ without proportional RAM use. The currentrustapi_core::multipart::Multipartimplementation parses the full request body into memory (and even converts it to a string during parsing), so this claim is inaccurate and could mislead users about memory/DoS characteristics.