diff --git a/Cargo.lock b/Cargo.lock index 68b610fcb9b923d6743d5f92de1f3728653e748a..3ba626f247e6d231df838d46c1a70c5d182c431e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,24 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-multipart" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9edfb0e7663d7fe18c8d5b668c9c1bcf79176b1dcc9d4da9592503209a6bfb0" +dependencies = [ + "actix-utils", + "actix-web", + "bytes", + "derive_more", + "futures-core", + "httparse", + "local-waker", + "log", + "mime", + "twoway", +] + [[package]] name = "actix-router" version = "0.5.0" @@ -700,32 +718,90 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" +[[package]] +name = "futures" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" -version = "0.3.23" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" + +[[package]] +name = "futures-executor" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" + +[[package]] +name = "futures-macro" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "futures-sink" -version = "0.3.23" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" [[package]] name = "futures-task" -version = "0.3.23" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" [[package]] name = "futures-util" -version = "0.3.23" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -914,11 +990,13 @@ name = "krustacea" version = "0.1.0" dependencies = [ "actix-files", + "actix-multipart", "actix-web", "actix-web-lab", "dirs", "env_logger", "fs_extra", + "futures", "regex", "rustls", "rustls-pemfile", @@ -1641,12 +1719,28 @@ dependencies = [ "once_cell", ] +[[package]] +name = "twoway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47" +dependencies = [ + "memchr", + "unchecked-index", +] + [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" + [[package]] name = "unicase" version = "2.6.0" diff --git a/Cargo.toml b/Cargo.toml index c73e84beea9fe3a1bbdc9f05d64bdd071dcdc683..1d59dbcb2a344c6c74fb5af601a84a469a16bcbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,5 @@ structopt = "0.3" env_logger = "0.9" actix-web-lab = "0.17.0" actix-files = "0.6.2" +actix-multipart = "0.4" +futures = "0.3.24" diff --git a/src/service/files.rs b/src/service/files.rs index 4800bfdb6877c7765b7c6c20580946cffd7e56d8..b84f0247d13e8d4521af8f1c416eadb8073b0f54 100644 --- a/src/service/files.rs +++ b/src/service/files.rs @@ -1,11 +1,138 @@ -use crate::static_files::StaticFilesManager; +use crate::static_files::upload::*; +use crate::website::WebSite; use actix_files::NamedFile; -use actix_web::{get, web, Responder}; +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, +}; #[get("/favicon.ico")] -pub async fn favicon( - static_files_manager: web::Data<std::sync::Mutex<StaticFilesManager>>, -) -> impl Responder { - let static_files_manager = static_files_manager.lock().unwrap(); +pub async fn favicon(website: web::Data<std::sync::Mutex<WebSite>>) -> impl Responder { + let static_files_manager = &website.lock().unwrap().static_files_manager; NamedFile::open(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<std::sync::Mutex<WebSite>>, + mut payload: Multipart, +) -> impl Responder { + let mut uploaded_filepathes = Vec::new(); + let mut website = website.lock().unwrap(); + + 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<std::sync::Mutex<WebSite>>) -> impl Responder { + HttpResponse::Ok().json( + website + .lock() + .expect("Couldn't lock website") + .static_files_manager + .get_index(), + ) +} + +#[delete("/delete-file/{category}/{filename}")] +async fn delete_static_file( + website: Data<std::sync::Mutex<WebSite>>, + fileinfo: Path<(String, String)>, +) -> impl Responder { + let mut website = website.lock().unwrap(); + 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)), + } +} diff --git a/src/static_files/mod.rs b/src/static_files/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..b9b5f28e561a28ceb989cdb05ce7395f4535b09a --- /dev/null +++ b/src/static_files/mod.rs @@ -0,0 +1,3 @@ +mod static_files; +pub mod upload; +pub use static_files::*; diff --git a/src/static_files.rs b/src/static_files/static_files.rs similarity index 85% rename from src/static_files.rs rename to src/static_files/static_files.rs index 0f07dddf6e3eac5b3cfba032bde61c426109f5b2..c4c3bf94ce9dcb4e7199c852b39fadd35a67d129 100644 --- a/src/static_files.rs +++ b/src/static_files/static_files.rs @@ -101,10 +101,10 @@ impl StaticFilesManager { } } - fn push_path(&mut self, path: &Path) -> Vec<PathBuf> { + pub fn push_path(&mut self, path: &Path) -> Vec<PathBuf> { if self.validate_path(path) { self.index - .push(format!("/{}", path.to_str().unwrap().to_owned())); + .push(self.clean_relative_path(path).to_str().unwrap().to_owned()); vec![path.to_path_buf()] } else { println!( @@ -129,6 +129,22 @@ impl StaticFilesManager { self.rec_read_dir(&self.dir.clone(), &self.dir.clone()); self.clone() } + + pub fn remove_path(&mut self, strpath: String) { + println!("REMOVE {}", strpath); + println!("current Index {:#?}", self.index); + self.index = self + .index + .iter() + .filter(|url| !strpath.eq(*url)) + .map(|s| s.to_owned()) + .collect(); + println!("Updated Index {:#?}", self.index); + } + + pub fn get_index(&self) -> Vec<String> { + self.index.clone() + } } #[cfg(test)] @@ -173,7 +189,7 @@ mod test_static_files_manager { manager = manager.build(); assert!( - manager.index.contains(&"/docs/testing.txt".to_string()), + manager.index.contains(&"docs/testing.txt".to_string()), "Index doesn't contain path /docs/testing.txt\n{:?}", remove_test_dir(&test_dir) ); @@ -182,7 +198,7 @@ mod test_static_files_manager { } #[test] - fn test_push_path() { + fn test_pushd_andremove_path() { let test_dir = create_test_dir(); let mut manager = StaticFilesManager::testing_new(&test_dir).unwrap().build(); let file_pth = test_dir.join("static").join("docs").join("testing.txt"); @@ -198,11 +214,19 @@ mod test_static_files_manager { ); assert!( - manager.index.contains(&"/docs/testing.txt".to_string()), + manager.index.contains(&"docs/testing.txt".to_string()), "Index doesn't contain path /docs/testing.txt\n{:?}", remove_test_dir(&test_dir) ); + manager.remove_path("docs/testing.txt".to_string()); + + assert!( + !manager.index.contains(&"docs/testing.txt".to_string()), + "Path docs/testing.txt should have been removed\n{:?}", + remove_test_dir(&test_dir) + ); + remove_test_dir(&test_dir); } @@ -221,9 +245,7 @@ mod test_static_files_manager { ); assert!( - !manager - .index - .contains(&"/images/unexisting.png".to_string()), + !manager.index.contains(&"images/unexisting.png".to_string()), "Index shouldn't container unexisting path\n{:?}", remove_test_dir(&test_dir) ); diff --git a/src/static_files/upload.rs b/src/static_files/upload.rs new file mode 100644 index 0000000000000000000000000000000000000000..b532b3b438b85c9e04b266604139971520da6aa0 --- /dev/null +++ b/src/static_files/upload.rs @@ -0,0 +1,48 @@ +#[derive(Debug, PartialEq)] +pub enum UploadFileType { + Image, + Sound, + Video, + Doc, + Code, +} + +pub struct UploadFileData { + pub up_type: UploadFileType, + pub filename: String, +} + +pub fn dirname_from_type(upload_type: &UploadFileType) -> String { + match upload_type { + UploadFileType::Image => String::from("images"), + UploadFileType::Sound => String::from("sounds"), + UploadFileType::Video => String::from("videos"), + UploadFileType::Doc => String::from("docs"), + UploadFileType::Code => String::from("source_code"), + } +} + +pub fn upload_type_from_file_ext(ext: &String) -> UploadFileType { + match &ext[..] { + "webp" | "jpg" | "png" | "jpeg" | "bmp" | "gif" => UploadFileType::Image, + "mp3" | "ogg" | "wav" | "opus" => UploadFileType::Sound, + "mp4" | "webm" | "ogv" => UploadFileType::Video, + "js" | "css" | "html" => UploadFileType::Code, + _ => UploadFileType::Doc, + } +} + +pub fn file_ext(file_name: &String) -> Result<String, String> { + let parts = file_name.split(".").collect::<Vec<&str>>(); + + let err_msg = format!("Couldn't get extension from filename : {}", file_name); + + if parts.len() < 2 { + return Err(err_msg); + } + + match parts.last() { + Some(ext) => Ok(ext.to_string()), + None => Err(err_msg), + } +}