Skip to content

Commit

Permalink
Change upload logic to break files into chunks
Browse files Browse the repository at this point in the history
Remove max payload size and client timeouts
Update docstrings
  • Loading branch information
dormant-user committed Mar 10, 2024
1 parent 355296d commit 13c1a66
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 29 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
8 changes: 1 addition & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#[macro_use]
extern crate actix_web;

use core::time::Duration;
use std::io;

use actix_web::{App, HttpServer, middleware, web};
Expand Down Expand Up @@ -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()))
Expand All @@ -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");
Expand Down
91 changes: 70 additions & 21 deletions src/routes/upload.rs
Original file line number Diff line number Diff line change
@@ -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<TempFile>,
}

/// 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<Arc<Fernet>>,
session: web::Data<Arc<constant::Session>>,
config: web::Data<Arc<squire::settings::Config>>,
MultipartForm(form): MultipartForm<UploadForm>) -> HttpResponse {
config: web::Data<Arc<squire::settings::Config>>) -> 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<Arc<Fernet>>,
Expand All @@ -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") {
Expand Down
3 changes: 3 additions & 0 deletions src/templates/landing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
3 changes: 2 additions & 1 deletion src/templates/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,11 @@ pub fn get_content() -> String {
<button class="home" onclick="goHome()"><i class="fa fa-home"></i> Home</button>
<button class="back" onclick="goBack()"><i class="fa fa-backward"></i> Back</button>
<button class="logout" onclick="logOut()"><i class="fa fa-sign-out"></i> Logout</button>
<br><br><br>
<div class="container">
<div class="header-section">
<h1>Upload Files</h1>
<p>PDF, Images & Videos are allowed.</p>
<p>Only PDF, Images & Videos are allowed.</p>
</div>
<div class="drop-section">
<div class="col">
Expand Down

0 comments on commit 13c1a66

Please sign in to comment.