use crate::config::Config; use crate::file; use crate::header::ContentDisposition; use actix_files::NamedFile; use actix_multipart::Multipart; use actix_web::http::header::AUTHORIZATION; use actix_web::{error, get, post, web, Error, HttpRequest, HttpResponse, Responder}; use byte_unit::Byte; use futures_util::stream::StreamExt; use std::convert::TryFrom; use std::env; /// Shows the landing page. #[get("/")] async fn index() -> impl Responder { HttpResponse::Ok().body("oops!") } /// Serves a file from the upload directory. #[get("/{file}")] async fn serve( request: HttpRequest, path: web::Path, config: web::Data, ) -> Result { let path = config.server.upload_path.join(&*path); let file = NamedFile::open(&path)? .disable_content_disposition() .prefer_utf8(true); let response = file.into_response(&request)?; Ok(response) } /// Handles file upload by processing `multipart/form-data`. #[post("/")] async fn upload( request: HttpRequest, mut payload: Multipart, config: web::Data, ) -> Result { let connection = request.connection_info(); let host = connection.remote_addr().unwrap_or("unknown host"); if let Ok(token) = env::var("AUTH_TOKEN") { let auth_header = request .headers() .get(AUTHORIZATION) .map(|v| v.to_str().unwrap_or_default()) .map(|v| v.split_whitespace().last().unwrap_or_default()); if auth_header.unwrap_or_default() != token { log::warn!( "authorization failure for {} (header: {})", host, auth_header.unwrap_or("none"), ); return Err(error::ErrorUnauthorized("unauthorized")); } } let mut urls: Vec = Vec::new(); while let Some(item) = payload.next().await { let mut field = item?; let content = ContentDisposition::try_from(field.content_disposition())?; if content.has_form_field("file") { let mut bytes = Vec::::new(); while let Some(chunk) = field.next().await { bytes.append(&mut chunk?.to_vec()); } let bytes_unit = Byte::from_bytes(bytes.len() as u128).get_appropriate_unit(false); if bytes.len() as u128 > config.server.max_content_length.get_bytes() { log::warn!("upload rejected for {} ({})", host, bytes_unit); return Err(error::ErrorPayloadTooLarge("upload limit exceeded")); } let file_name = &file::save(content.get_file_name()?, &bytes, &config)?; log::info!("{} ({}) is uploaded from {}", file_name, bytes_unit, host); urls.push(format!( "{}://{}/{}\n", connection.scheme(), connection.host(), file_name )); } else { log::warn!("{} sent an invalid form field", host); return Err(error::ErrorBadRequest("invalid form field")); } } Ok(HttpResponse::Ok().body(urls.join(""))) } /// Configures the server routes. pub fn configure_routes(cfg: &mut web::ServiceConfig) { cfg.service(index) .service(serve) .service(upload) .route("", web::head().to(HttpResponse::MethodNotAllowed)); } #[cfg(test)] mod tests { use super::*; use actix_web::{http, test, App}; #[actix_rt::test] async fn test_index() { let mut app = test::init_service(App::new().service(index)).await; let req = test::TestRequest::with_header("content-type", "text/plain").to_request(); let resp = test::call_service(&mut app, req).await; assert!(resp.status().is_success()); } #[actix_rt::test] async fn test_serve() { let mut app = test::init_service(App::new().service(serve)).await; let req = test::TestRequest::default().to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(http::StatusCode::NOT_FOUND, resp.status()); } // TODO: add test for upload }