diff --git a/Cargo.toml b/Cargo.toml index ebf0e8d..e52820f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,5 +46,6 @@ regex = "1.5" walkdir = "2.3.2" openssl = "0.10" dotenv = "0.15.0" +futures-util = "0.3.30" [target.'cfg(target_os = "linux")'.dependencies] openssl = { version = "0.10", features = ["vendored"] } diff --git a/src/lib.rs b/src/lib.rs index 50c87f6..8b6c51f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,6 @@ #[macro_use] extern crate actix_web; -use core::time::Duration; use std::io; use actix_web::{App, HttpServer, middleware, web}; @@ -62,7 +61,6 @@ pub async fn start() -> io::Result<()> { The closure is defining the configuration for the Actix web server. The purpose of the closure is to configure the server before it starts listening for incoming requests. */ - let max_payload_size = 10 * 1024 * 1024 * 1024; // 10 GB let application = move || { App::new() // Creates a new Actix web application .app_data(web::Data::new(config_clone.clone())) @@ -82,14 +80,10 @@ pub async fn start() -> io::Result<()> { .service(routes::media::streaming_endpoint) .service(routes::upload::upload_files) .service(routes::upload::save_files) - .app_data(web::PayloadConfig::default().limit(max_payload_size)) }; - let duration = Duration::new(600, 0); let server = HttpServer::new(application) .workers(config.workers as usize) - .max_connections(config.max_connections as usize) - .client_request_timeout(duration) - .client_disconnect_timeout(duration); + .max_connections(config.max_connections as usize); // Reference: https://actix.rs/docs/http2/ if config.cert_file.exists() && config.key_file.exists() { log::info!("Binding SSL certificate to serve over HTTPS"); diff --git a/src/routes/upload.rs b/src/routes/upload.rs index 9d28051..8499c7b 100644 --- a/src/routes/upload.rs +++ b/src/routes/upload.rs @@ -1,42 +1,90 @@ +use std::fs::File; +use std::io::Write; use std::path::Path; use std::sync::Arc; +use std::time::Instant; -use actix_multipart::form::{ - MultipartForm, - tempfile::TempFile, -}; -use actix_web::{HttpRequest, HttpResponse, web, http}; +use actix_multipart::Multipart; +use actix_web::{http, HttpRequest, HttpResponse, web}; use fernet::Fernet; +use futures_util::StreamExt as _; use crate::{constant, routes, squire}; -#[derive(Debug, MultipartForm)] -struct UploadForm { - #[multipart(rename = "file")] - files: Vec, -} - +/// Saves files locally by breaking them into chunks. +/// +/// # Arguments +/// +/// * `request` - A reference to the Actix web `HttpRequest` object. +/// * `payload` - Mutable multipart struct that is sent from the UI as `FormData`. +/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie. +/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions. +/// * `config` - Configuration data for the application. +/// +/// ## References +/// - [Server Side](https://docs.rs/actix-multipart/latest/actix_multipart/struct.Multipart.html) +/// - [Client Side (not implemented)](https://accreditly.io/articles/uploading-large-files-with-chunking-in-javascript) +/// +/// # Returns +/// +/// * `200` - Plain HTTPResponse indicating that the file was uploaded. +/// * `401` - HTTPResponse with JSON payload indicating the problem uploading file. #[post("/upload")] pub async fn save_files(request: HttpRequest, + mut payload: Multipart, fernet: web::Data>, session: web::Data>, - config: web::Data>, - MultipartForm(form): MultipartForm) -> HttpResponse { + config: web::Data>) -> HttpResponse { let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session); if !auth_response.ok { return routes::auth::failed_auth(auth_response, &config); } - let mut source_path = config.media_source.clone(); - for file in form.files { - let filename = file.file_name.unwrap(); - source_path.push(&filename); - let path = source_path.to_string_lossy().to_string(); - log::info!("Saving '{filename}'"); - file.file.persist(path).unwrap(); + let source_path = Path::new("uploads"); + while let Some(item) = payload.next().await { + match item { + Ok(mut field) => { + let filename = field.content_disposition().get_filename().unwrap(); + let mut destination = File::create(source_path.join(filename)).unwrap(); + let start = Instant::now(); + log::info!("Downloading '{}'", &filename); + while let Some(fragment) = field.next().await { + match fragment { + Ok(chunk) => { + destination.write_all(&chunk).unwrap(); + } + Err(err) => { + let error = format!("Error processing field: {}", err); + log::error!("{}", &error); + return HttpResponse::InternalServerError().json(error); + } + } + } + // todo: Remove this or set to debug + log::info!("Download completed in {}s", start.elapsed().as_secs()) + } + Err(err) => { + let error = format!("Error processing field: {}", err); + log::error!("{}", &error); + return HttpResponse::InternalServerError().json(error); + } + } } - HttpResponse::Ok().body("Success") + HttpResponse::Ok().finish() } +/// Handles requests for the '/upload' endpoint, serving the file upload template. +/// +/// # Arguments +/// +/// * `request` - A reference to the Actix web `HttpRequest` object. +/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie. +/// * `config` - Configuration data for the application. +/// * `template` - Configuration container for the loaded templates. +/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions. +/// +/// # Returns +/// +/// Returns an `HttpResponse` with the upload page as its body. #[get("/upload")] pub async fn upload_files(request: HttpRequest, fernet: web::Data>, @@ -47,6 +95,7 @@ pub async fn upload_files(request: HttpRequest, if !auth_response.ok { return routes::auth::failed_auth(auth_response, &config); } + // todo: Create user specific directories and upload there instead let upload_path = Path::new("uploads"); if !upload_path.exists() { match std::fs::create_dir("uploads") { diff --git a/src/templates/landing.rs b/src/templates/landing.rs index 0522f0c..aff7f9a 100644 --- a/src/templates/landing.rs +++ b/src/templates/landing.rs @@ -253,6 +253,9 @@ pub fn get_content() -> String { function goHome() { window.location.href = window.location.origin + "/home"; } + function upload() { + window.location.href = window.location.origin + "/upload"; + } function goBack() { window.history.back(); } diff --git a/src/templates/upload.rs b/src/templates/upload.rs index 4faf424..2433c57 100644 --- a/src/templates/upload.rs +++ b/src/templates/upload.rs @@ -284,10 +284,11 @@ pub fn get_content() -> String { +


Upload Files

-

PDF, Images & Videos are allowed.

+

Only PDF, Images & Videos are allowed.