use crate::static_files::upload::*; use crate::website::WebSite; use actix_files::NamedFile; use actix_multipart::Multipart; use actix_web::{ delete, get, post, web, web::{Data, Path}, HttpResponse, Responder, }; use futures::StreamExt; use std::{ fs::{remove_file, File}, io::Write, sync::RwLock, }; #[get("/favicon.ico")] pub async fn favicon(website: web::Data<RwLock<WebSite>>) -> impl Responder { NamedFile::open( &website .read() .unwrap() .static_files_manager .dir .join("default") .join("favicon.ico"), ) } fn upload_data_from_multipart_field( field: &actix_multipart::Field, ) -> Result<UploadFileData, String> { match field.content_disposition().get_filename() { Some(fname) => match file_ext(&fname.to_string()) { Ok(ext) => Ok(UploadFileData { up_type: upload_type_from_file_ext(&ext), filename: fname.to_owned(), }), Err(msg) => return Err(msg), }, None => Err("Couldn't retrieve file extension".to_string()), } } async fn write_uploaded_file( website: &WebSite, field: &mut actix_multipart::Field, filename: &String, upload_type: UploadFileType, ) -> Result<String, String> { let root = &website.static_files_manager.dir; let sub_dir = dirname_from_type(&upload_type); let filepath = root.join(sub_dir).join(&filename); match File::create(&filepath) { Err(e) => Err(format!("Error creating file {:?} : {:?}", filepath, e)), Ok(mut f) => { // Field in turn is stream of *Bytes* object while let Some(chunk) = field.next().await { match chunk { Ok(chunk) => { if f.write_all(&chunk).is_err() { remove_file(&filepath).unwrap(); return Err("Error writing chunk".to_string()); } } Err(e) => { return Err(format!("Error writing file {} : {:?}", filename, e)); } } } Ok(filepath.into_os_string().into_string().unwrap()) } } } #[post("/post-files")] pub async fn post_files(website: Data<RwLock<WebSite>>, mut payload: Multipart) -> impl Responder { let mut uploaded_filepathes = Vec::new(); let mut website = website .write() .expect("Couldn't acquire write lock for RwLock<WebSite"); while let Some(item) = payload.next().await { match item { Ok(mut field) => { let up_data = upload_data_from_multipart_field(&field); if let Err(msg) = up_data { return HttpResponse::InternalServerError().body(msg); } let up_data = up_data.unwrap(); match write_uploaded_file(&website, &mut field, &up_data.filename, up_data.up_type) .await { Err(msg) => return HttpResponse::InternalServerError().body(msg), Ok(filepath) => uploaded_filepathes.extend( website .static_files_manager .push_path(std::path::Path::new(&filepath)), ), } } Err(e) => { return HttpResponse::InternalServerError().body(format!("FIELD ERR {:?}", e)) } } } HttpResponse::Ok().json(uploaded_filepathes) } #[get("/static-files-index")] async fn get_static_files_index(website: Data<RwLock<WebSite>>) -> impl Responder { HttpResponse::Ok().json(website.read().unwrap().static_files_manager.get_index()) } #[delete("/delete-file/{category}/{filename}")] async fn delete_static_file( website: Data<RwLock<WebSite>>, fileinfo: Path<(String, String)>, ) -> impl Responder { let mut website = website .write() .expect("Couldn't acquire write lock for RwLock<WebSite"); let (cat, fname) = fileinfo.into_inner(); let fpath = std::path::PathBuf::from(cat).join(fname); match remove_file(website.static_files_manager.dir.join(&fpath)) { Ok(_) => { website .static_files_manager .remove_path(fpath.to_string_lossy().into()); HttpResponse::Accepted().body("File was deleted") } Err(e) => HttpResponse::InternalServerError().body(format!("Error deleting file {:?}", e)), } } #[cfg(test)] mod test_static_files_services { use super::*; use crate::{cookie::*, website::*, AppState}; use actix_web::{ http::{Method, StatusCode}, test, web::Bytes, App, }; use std::sync::RwLock; fn create_simple_request() -> Bytes { Bytes::from( "--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ test\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ Content-Disposition: form-data; name=\"file\"; filename=\"data.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ data\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", ) } // fn clear_testing_static(dir: &std::path::PathBuf) { // std::fs::remove_dir_all(dir).unwrap(); // } #[actix_web::test] async fn post_files_unauthenticated_should_be_unauthorized() { let app_state = AppState::testing(); let static_dir = std::path::PathBuf::from("./test"); let ws = WebSiteBuilder::testing(&static_dir); let mut app = test::init_service( App::new() .app_data(web::Data::new(RwLock::new(app_state.clone()))) .app_data(web::Data::new(RwLock::new(ws.clone()))) .wrap(crate::middleware::AuthService {}) .service(post_files), ) .await; let req = test::TestRequest::with_uri("/post-files") .method(Method::POST) .append_header(( "Content-Type", "multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", )) .cookie(SecureCookie::new( &"wrong-cookie".to_string(), &SecureCookie::generate_token(36), )) .set_payload(create_simple_request()) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } // #[actix_web::test] // async fn test_post_files() { // let app_state = AppState::for_test().await; // let admin_user = Administrator::authenticated( // &app_state, // AdminAuthCredentials { // username: app_state.env.default_admin_username.to_owned(), // password: app_state.env.default_admin_password.to_owned(), // }, // ) // .await // .unwrap(); // let static_files_index = create_files_index(&app_state); // let mut app = test::init_service( // App::new() // .app_data(Data::new(app_state.clone())) // .app_data(Data::clone(&static_files_index)) // .app_data(Data::new(AuthenticatedAdminMiddleware::new( // "kuadrado-admin-auth", // ))) // .service(post_files), // ) // .await; // let req = test::TestRequest::with_uri("/post-files") // .method(Method::POST) // .header( // "Content-Type", // "multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", // ) // .cookie(get_auth_cookie( // "kuadrado-admin-auth", // app_state // .encryption // .decrypt(&admin_user.auth_token.unwrap()) // .to_owned(), // )) // .set_payload(create_simple_request()) // .to_request(); // let resp = test::call_service(&mut app, req).await; // let status = resp.status(); // assert_eq!(status, StatusCode::OK); // let pathes: Vec<String> = test::read_body_json(resp).await; // let public_dir = StaticFilesIndex::get_public_dir(&app_state.env); // let pathes_from_public = pathes // .iter() // .map(|p| { // format!( // "/{}", // std::path::Path::new(p) // .strip_prefix(&public_dir) // .unwrap() // .to_str() // .unwrap() // ) // }) // .collect::<Vec<String>>(); // let index = static_files_index.lock().unwrap(); // assert_eq!(pathes_from_public, index.0); // let mut iter_pathes = pathes.iter(); // let f = std::fs::read_to_string(iter_pathes.next().unwrap()).unwrap(); // assert_eq!(f, "test"); // let f = std::fs::read_to_string(iter_pathes.next().unwrap()).unwrap(); // assert_eq!(f, "data"); // clear_testing_static(); // } // #[actix_web::test] // async fn test_delete_file() { // let app_state = AppState::for_test().await; // let admin_user = Administrator::authenticated( // &app_state, // AdminAuthCredentials { // username: app_state.env.default_admin_username.to_owned(), // password: app_state.env.default_admin_password.to_owned(), // }, // ) // .await // .unwrap(); // let static_files_index = create_files_index(&app_state); // let mut app = test::init_service( // App::new() // .app_data(Data::new(app_state.clone())) // .app_data(Data::clone(&static_files_index)) // .app_data(Data::new(AuthenticatedAdminMiddleware::new( // "kuadrado-admin-auth", // ))) // .service(post_files) // .service(delete_static_file), // ) // .await; // let auth_token = admin_user.auth_token.unwrap(); // let req = test::TestRequest::with_uri("/post-files") // .method(Method::POST) // .header( // "Content-Type", // "multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", // ) // .cookie(get_auth_cookie( // "kuadrado-admin-auth", // app_state.encryption.decrypt(&auth_token).to_owned(), // )) // .set_payload(create_simple_request()) // .to_request(); // let resp = test::call_service(&mut app, req).await; // let status = resp.status(); // assert_eq!(status, StatusCode::OK); // let req = test::TestRequest::with_uri("/delete-file/uploads/docs/test.txt") // .method(Method::DELETE) // .cookie(get_auth_cookie( // "kuadrado-admin-auth", // app_state.encryption.decrypt(&auth_token).to_owned(), // )) // .to_request(); // let resp = test::call_service(&mut app, req).await; // let status = resp.status(); // assert_eq!(status, StatusCode::ACCEPTED); // clear_testing_static(); // } }