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}