rustream/routes/upload.rs
1use std::fs::File;
2use std::io::Write;
3use std::sync::Arc;
4
5use actix_multipart::Multipart;
6use actix_web::{http, HttpRequest, HttpResponse, web};
7use fernet::Fernet;
8use futures_util::StreamExt as _;
9
10use crate::{constant, routes, squire};
11
12/// Saves files locally by breaking them into chunks.
13///
14/// # Arguments
15///
16/// * `request` - A reference to the Actix web `HttpRequest` object.
17/// * `payload` - Mutable multipart struct that is sent from the UI as `FormData`.
18/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
19/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
20/// * `config` - Configuration data for the application.
21///
22/// ## See Also
23///
24/// - The JavaScript in the frontend appends a reference/pointer to the file.
25/// - Once the reference is loaded, it makes an asynchronous call to the server.
26/// - The server then breaks the file into chunks and downloads it iteratively.
27/// - The number of files that can be uploaded simultaneously depends on the number of workers configured.
28///
29/// ## References
30/// - [Server Side](https://docs.rs/actix-multipart/latest/actix_multipart/struct.Multipart.html)
31/// - [Client Side (not implemented)](https://accreditly.io/articles/uploading-large-files-with-chunking-in-javascript)
32///
33/// # Returns
34///
35/// * `200` - Plain HTTPResponse indicating that the file was uploaded.
36/// * `422` - HTTPResponse with JSON object indicating that the payload was incomplete.
37/// * `400` - HTTPResponse with JSON object indicating that the payload was invalid.
38#[post("/upload")]
39pub async fn save_files(request: HttpRequest,
40 mut payload: Multipart,
41 fernet: web::Data<Arc<Fernet>>,
42 session: web::Data<Arc<constant::Session>>,
43 config: web::Data<Arc<squire::settings::Config>>) -> HttpResponse {
44 let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
45 if !auth_response.ok {
46 return routes::auth::failed_auth(auth_response, &config);
47 }
48 let mut upload_path = config.media_source.clone(); // cannot be borrowed as mutable
49 let mut secure_str = "";
50 if let Some(secure_flag) = request.headers().get("secure-flag") {
51 if secure_flag.to_str().unwrap_or("false") == "true" {
52 secure_str = "to secure index ";
53 upload_path.extend([format!("{}_{}", &auth_response.username, constant::SECURE_INDEX)])
54 }
55 }
56 while let Some(item) = payload.next().await {
57 match item {
58 Ok(mut field) => {
59 let filename = match field.content_disposition() {
60 Some(content_disposition) => match content_disposition.get_filename() {
61 Some(filename) => filename,
62 None => {
63 let error = "Filename not found in content disposition";
64 log::error!("{}", &error);
65 return HttpResponse::BadRequest().json(error);
66 }
67 },
68 None => {
69 let error = "Content disposition not found";
70 log::error!("{}", &error);
71 return HttpResponse::BadRequest().json(error);
72 }
73 };
74 let mut destination = File::create(&upload_path.join(filename)).unwrap();
75 log::info!("Downloading '{}' {}- uploaded by '{}'", &filename, secure_str, &auth_response.username);
76 while let Some(fragment) = field.next().await {
77 match fragment {
78 Ok(chunk) => {
79 destination.write_all(&chunk).unwrap();
80 }
81 Err(err) => {
82 // User might have aborted file upload
83 let error = format!("Error processing chunk: {}", err);
84 log::warn!("{}", &error);
85 return HttpResponse::UnprocessableEntity().json(error);
86 }
87 }
88 }
89 }
90 Err(err) => {
91 let error = format!("Error processing field: {}", err);
92 log::error!("{}", &error);
93 return HttpResponse::BadRequest().json(error);
94 }
95 }
96 }
97 HttpResponse::Ok().finish()
98}
99
100/// Handles requests for the `/upload` endpoint, serving the file upload template.
101///
102/// # Arguments
103///
104/// * `request` - A reference to the Actix web `HttpRequest` object.
105/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
106/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
107/// * `metadata` - Struct containing metadata of the application.
108/// * `config` - Configuration data for the application.
109/// * `template` - Configuration container for the loaded templates.
110///
111/// # Returns
112///
113/// Returns an `HttpResponse` with the upload page as its body.
114#[get("/upload")]
115pub async fn upload_files(request: HttpRequest,
116 fernet: web::Data<Arc<Fernet>>,
117 session: web::Data<Arc<constant::Session>>,
118 metadata: web::Data<Arc<constant::MetaData>>,
119 config: web::Data<Arc<squire::settings::Config>>,
120 template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
121 let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
122 if !auth_response.ok {
123 return routes::auth::failed_auth(auth_response, &config);
124 }
125 let landing = template.get_template("upload").unwrap();
126 HttpResponse::build(http::StatusCode::OK)
127 .content_type("text/html; charset=utf-8")
128 .body(landing.render(minijinja::context!(
129 version => metadata.pkg_version,
130 user => auth_response.username,
131 secure_index => constant::SECURE_INDEX
132 )).unwrap())
133}