diff --git a/README.md b/README.md index 54f1df5157a346ffa16a12fb701e2063d55358b8..8cfdc8857c3aa375975d2f24a2f3b117d90f1bfa 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,6 @@ ------------------------------------------------ _______ Krustacea is a package that contains a web server written in Rust (Actix based) and a web application to build a static website. - -The web server loads a website as a single JSON document, the html contents is built in memory by the server at start, and the website is served without the need of any static html file. It also handles the necessary static files such as images, or additional source code like css or javascript. - -Modifications to the website object will be done through the use of a REST API and the web application on client side. - _________________ ## **/!\\** WIP ## @@ -40,7 +35,7 @@ A website data is store as a JSON file with the following structure "metadata": { "title": "Hello Krustcea !", "description": "An example website", - "image": "https://example.com/static/images/ex_pic.png", + "image": "https://example.com/images/ex_pic.png", "css": [], "js": [], "url_slug": "", @@ -109,11 +104,11 @@ A website data is store as a JSON file with the following structure } ], "assets_index": [ - "/static/images/toto.jpg", - "/static/sounds/toto.mp3", - "/static/video/toto.mp4", - "/static/docs/toto.xcf", - "/static/source_code/toto.js" + "/images/toto.jpg", + "/sounds/toto.mp3", + "/video/toto.mp4", + "/docs/toto.xcf", + "/source_code/toto.js" ] } ``` \ No newline at end of file diff --git a/example.json b/example.json index 01cab40f5c5aa919418737c9d3bd8a142a125738..5411f335b92ed7b40193699e13a4f1593c7e74e2 100644 --- a/example.json +++ b/example.json @@ -4,7 +4,7 @@ "metadata": { "title": "Hello Krustcea !", "description": "An example website", - "image": "https://example.com/static/images/ex_pic.png", + "image": "https://example.com/images/ex_pic.png", "css": [], "js": [], "url_slug": "", @@ -33,7 +33,7 @@ }, "templates": [ { - "name": "Custom template", + "name": "A Custom template", "layout": { "display": "grid" }, @@ -73,10 +73,10 @@ } ], "assets_index": [ - "/static/images/toto.jpg", - "/static/sounds/toto.mp3", - "/static/video/toto.mp4", - "/static/docs/toto.xcf", - "/static/source_code/toto.js" + "/images/toto.jpg", + "/sounds/toto.mp3", + "/video/toto.mp4", + "/docs/toto.xcf", + "/source_code/toto.js" ] } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index c302b94a9fb24167c65ea90649c5908626f86b7a..00a89e61ce34d44ee101afb9bdecb9a0602a9fb0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,7 @@ async fn main() -> std::io::Result<()> { let port_tls = app_state.config.port_tls; let srv_conf = tls_config(&app_state.config); - let static_dir = website.static_files_manager.dir.clone(); + let dir = website.static_files_manager.dir.clone(); let mut_app_state = std::sync::Mutex::new(app_state); let app_state = web::Data::new(mut_app_state); let mut_website = web::Data::new(std::sync::Mutex::new(website)); @@ -44,7 +44,6 @@ async fn main() -> std::io::Result<()> { .wrap(RedirectHttps::default().to_port(port_tls)) .app_data(web::Data::clone(&app_state)) .app_data(web::Data::clone(&mut_website)) - .service(Files::new("/static/", &static_dir)) .service( web::scope("/admin") .service(service::admin_login) @@ -56,7 +55,7 @@ async fn main() -> std::io::Result<()> { ), ) .service(service::files::favicon) - .service(service::page) + .service(Files::new("/", &dir).index_file("index.html")) }) .bind(format!("{}:{}", host, port))? .bind_rustls(format!("{}:{}", host, port_tls), srv_conf)? diff --git a/src/service/admin.rs b/src/service/admin.rs index 2f7898d190d8de54396db4a6e842511889de4d0c..0366a0c6a5224ed1c5cbaae8fe87d58fbcff21dc 100644 --- a/src/service/admin.rs +++ b/src/service/admin.rs @@ -76,7 +76,7 @@ pub async fn admin_login() -> impl Responder { <meta http-equiv='X-UA-Compatible' content='IE=edge'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>Krutacea - Admin Login</title> - <link rel='stylesheet' href='/static/default/admin.css'> + <link rel='stylesheet' href='/default/admin.css'> </head> <body> @@ -92,7 +92,7 @@ pub async fn admin_login() -> impl Responder { <input type='submit' /> </form> </body> -<script src='/static/default/admin.js'></script> +<script src='/default/admin.js'></script> </html> ", ) diff --git a/src/service/mod.rs b/src/service/mod.rs index 61f75aee4dccd0132a927a8ab004bde5b73b488d..f461f48edf00e87b0d99250d2c16288a977ae750 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,5 +1,3 @@ mod admin; pub mod files; -mod page; pub use admin::*; -pub use page::*; diff --git a/src/service/page.rs b/src/service/page.rs deleted file mode 100644 index c0bfe7f2bbd8b9af7f4285555e292d3ec5986ef9..0000000000000000000000000000000000000000 --- a/src/service/page.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::website::WebSite; -use actix_web::{get, web, HttpResponse, Responder}; -use std::path::PathBuf; - -#[get("/{pth:.*}")] -pub async fn page( - website: web::Data<std::sync::Mutex<WebSite>>, - pth: web::Path<PathBuf>, -) -> impl Responder { - let website = website.lock().unwrap(); - let pth = pth.into_inner(); - - match website.get_page_by_url(&pth) { - Some(page) => HttpResponse::Ok().body(page.html.to_string()), - None => HttpResponse::NotFound().body(format!("Not found {}", pth.display())), - } -} diff --git a/src/static_files/static_files.rs b/src/static_files/static_files.rs index 4e4f6010fac04606f66bd7897926910930e66af6..74cd130abc00659fff6d526d862e0cd9eabedb33 100644 --- a/src/static_files/static_files.rs +++ b/src/static_files/static_files.rs @@ -1,4 +1,6 @@ use crate::app::AppState; +use crate::website::Page; +use std::io::prelude::*; use std::path::{Path, PathBuf}; #[derive(Clone, Debug)] @@ -18,7 +20,7 @@ const STATIC_ASSETS_DIRECTORIES: [&'static str; 6] = [ impl StaticFilesManager { pub fn new(app_state: &AppState) -> Result<Self, String> { - match Self::create_dir_if_missing(&app_state.config.storage_dir) { + match Self::create_dir_tree(&app_state.config.storage_dir) { Ok(dir) => Ok(StaticFilesManager { index: Vec::new(), dir, @@ -29,7 +31,7 @@ impl StaticFilesManager { #[cfg(test)] pub fn testing_new(test_dir: &PathBuf) -> Result<Self, String> { - match Self::create_dir_if_missing(test_dir) { + match Self::create_dir_tree(test_dir) { Ok(dir) => Ok(StaticFilesManager { index: Vec::new(), dir, @@ -38,30 +40,30 @@ impl StaticFilesManager { } } - fn create_dir_if_missing(app_dir: &PathBuf) -> Result<PathBuf, String> { - let static_dir = app_dir.join("static"); + fn create_dir_tree(dir: &PathBuf) -> Result<PathBuf, String> { + if !dir.exists() { + if let Err(err) = std::fs::create_dir_all(&dir) { + return Err(format!("{}", err)); + }; + } - if !static_dir.exists() { - match std::fs::create_dir_all(&static_dir) { - Ok(_) => { - if let Err(err) = Self::create_assets_directories_structure(&static_dir) { - return Err(format!("{}", err)); - }; + if let Err(err) = Self::create_assets_directories_structure(&dir) { + return Err(format!("{}", err)); + }; - if let Err(err) = Self::copy_default_files(&static_dir) { - return Err(format!("{}", err)); - } - } - Err(err) => return Err(format!("{}", err)), - } + if let Err(err) = Self::copy_default_files(&dir) { + return Err(format!("{}", err)); } - Ok(static_dir) + Ok(dir.clone()) } fn create_assets_directories_structure(root: &PathBuf) -> Result<(), std::io::Error> { for d in STATIC_ASSETS_DIRECTORIES { - std::fs::create_dir(root.join(d))?; + let p = root.join(d); + if !p.exists() { + std::fs::create_dir(p)?; + } } Ok(()) @@ -72,6 +74,8 @@ impl StaticFilesManager { let default_static = static_dir.join("default"); let mut cpy_options = fs_extra::dir::CopyOptions::new(); cpy_options.content_only = true; + cpy_options.overwrite = true; + match fs_extra::dir::copy(local_default_static, default_static, &cpy_options) { Err(err) => Err(format!("{}", err)), Ok(_) => Ok(()), @@ -142,6 +146,25 @@ impl StaticFilesManager { pub fn get_index(&self) -> Vec<String> { self.index.clone() } + + pub fn write_html_page(&self, url: &PathBuf, page: &Page) -> std::io::Result<()> { + let dir = self.dir.join(self.clean_relative_path(url)); + + if !dir.exists() { + std::fs::create_dir_all(&dir)?; + } + + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&dir.join("index.html"))?; + + file.write_all(page.html.to_string().as_bytes())?; + file.flush()?; + + Ok(()) + } } #[cfg(test)] @@ -164,7 +187,7 @@ mod test_static_files_manager { let _manager = StaticFilesManager::testing_new(&test_dir).unwrap(); for d in STATIC_ASSETS_DIRECTORIES { - let p = test_dir.join("static").join(d); + let p = test_dir.join(d); let exists = p.exists(); assert!( exists, @@ -181,7 +204,7 @@ mod test_static_files_manager { fn test_indexation() { let test_dir = create_test_dir(); let mut manager = StaticFilesManager::testing_new(&test_dir).unwrap(); - let file_pth = test_dir.join("static").join("docs").join("testing.txt"); + let file_pth = test_dir.join("docs").join("testing.txt"); std::fs::File::create(&file_pth).unwrap(); manager = manager.build(); @@ -195,10 +218,10 @@ mod test_static_files_manager { } #[test] - fn test_pushd_andremove_path() { + fn test_pushd_and_remove_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"); + let file_pth = test_dir.join("docs").join("testing.txt"); std::fs::File::create(&file_pth).unwrap(); let indexed_path = Path::new("docs/testing.txt"); let added = manager.push_path(&indexed_path); diff --git a/src/testing.rs b/src/testing.rs index d4bd942e96d40dc1af3fba4132fcd12f7149481a..0ab5f7b1320168b4ccff77bf02ae3397a5751508 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,12 +1,12 @@ #[cfg(test)] -pub const TEST_JSON_WEBSITE: &'static str = " +pub const _TEST_JSON_WEBSITE: &'static str = " { \"root_page\": { \"template_name\": \"TEST TEMPLATE\", \"metadata\": { \"title\": \"TEST\", \"description\": \"TEST DESCRIPTION\", - \"image\": [\"https://test/static/images/test.png\"], + \"image\": [\"https://test/images/test.png\"], \"css\": [], \"lang\":\"en\", \"js\": [], @@ -24,7 +24,7 @@ pub const TEST_JSON_WEBSITE: &'static str = " \"metadata\": { \"title\": \"TEST SUBPAGE\", \"description\": \"TEST DESCRIPTION SUBPAGE\", - \"image\": [\"https://test/static/images/test.png\"], + \"image\": [\"https://test/images/test.png\"], \"css\": [], \"lang\":\"en\", \"js\": [], @@ -42,7 +42,7 @@ pub const TEST_JSON_WEBSITE: &'static str = " \"metadata\": { \"title\": \"TEST NESTED\", \"description\": \"TEST DESCRIPTION NESTED\", - \"image\": [\"https://test/static/images/test.png\"], + \"image\": [\"https://test/images/test.png\"], \"css\": [], \"js\": [], \"url_slug\": \"nested\", diff --git a/src/website/mod.rs b/src/website/mod.rs index df329bf20e460e4767e629c900517b8b50597f50..c1c6ad901df80cf18aef76011857b658ab59c3cf 100644 --- a/src/website/mod.rs +++ b/src/website/mod.rs @@ -3,4 +3,5 @@ mod html; mod page; mod website; +pub use page::Page; pub use website::*; diff --git a/src/website/page.rs b/src/website/page.rs index c8a2029017d30c17b70efbfccce0b7828f5d7d44..92df3d3a8d168ee438d6e2e38ba430c3a020f24a 100644 --- a/src/website/page.rs +++ b/src/website/page.rs @@ -3,8 +3,10 @@ use super::html::{ replace_placeholders, HtmlDoc, HtmlElement, CSS_LINK_FRAGMENT, FAVICON_LINK_FRAGMENT, IMAGE_LINKS_FRAGMENT, SCRIPT_FRAGMENT, }; +use crate::StaticFilesManager; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::path::PathBuf; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Page { @@ -84,7 +86,7 @@ pub struct FaviconLink(String); impl FaviconLink { pub fn new() -> Self { - FaviconLink(String::from("/static/default/favicon.ico")) + FaviconLink(String::from("/default/favicon.ico")) } } @@ -107,7 +109,7 @@ pub struct CSSLinks(Vec<String>); impl CSSLinks { pub fn new() -> Self { - CSSLinks(vec!["/static/default/style.css".to_owned()]) + CSSLinks(vec!["/default/style.css".to_owned()]) } } @@ -135,7 +137,7 @@ pub struct JSLinks(Vec<String>); impl JSLinks { pub fn new() -> Self { - JSLinks(vec!["/static/default/script.js".to_owned()]) + JSLinks(vec!["/default/script.js".to_owned()]) } } @@ -187,6 +189,31 @@ impl std::fmt::Display for ImageLinks { } impl Page { + pub fn build( + &mut self, + templates: &Vec<PageTemplate>, + from_url: PathBuf, + static_files_manager: &StaticFilesManager, + ) { + self.build_with_template( + templates + .iter() + .find(|t| t.name == self.template_name) + .expect("Page template not found") + .clone(), + ); + + self.build_html(); + + let url = from_url.join(&self.metadata.url_slug); + + static_files_manager.write_html_page(&url, self).unwrap(); + + for p in self.sub_pages.iter_mut() { + p.build(templates, url.clone(), static_files_manager); + } + } + pub fn build_html(&mut self) { self.html = HtmlDoc::from_page(self); } @@ -233,18 +260,18 @@ mod test_pages { description: String::from("test descr"), url_slug: String::from("test-page"), css: CSSLinks(vec![ - "/static/source_code/mystyle.css".to_string(), - "/static/source_code/mystyle2.css".to_string(), + "/source_code/mystyle.css".to_string(), + "/source_code/mystyle2.css".to_string(), ]), js: JSLinks(vec![ - "/static/source_code/myscript.js".to_string(), - "/static/source_code/myscript2.js".to_string(), + "/source_code/myscript.js".to_string(), + "/source_code/myscript2.js".to_string(), ]), - favicon: FaviconLink(String::from("/static/images/testicon.ico")), + favicon: FaviconLink(String::from("/images/testicon.ico")), author: String::from("test author"), image: ImageLinks(vec![ - "/static/images/testimage.png".to_string(), - "/static/images/testimage2.png".to_string(), + "/images/testimage.png".to_string(), + "/images/testimage2.png".to_string(), ]), } } @@ -334,7 +361,7 @@ mod test_pages { let pmd = test_page_metadata(); assert_eq!( pmd.favicon.to_string(), - "<link rel='icon' type='image/*' href='/static/images/testicon.ico'/>" + "<link rel='icon' type='image/*' href='/images/testicon.ico'/>" ) } @@ -343,8 +370,8 @@ mod test_pages { let pmd = test_page_metadata(); assert_eq!( pmd.css.to_string(), - "<link rel='stylesheet' href='/static/source_code/mystyle.css'> -<link rel='stylesheet' href='/static/source_code/mystyle2.css'>" + "<link rel='stylesheet' href='/source_code/mystyle.css'> +<link rel='stylesheet' href='/source_code/mystyle2.css'>" ) } @@ -353,8 +380,8 @@ mod test_pages { let pmd = test_page_metadata(); assert_eq!( pmd.js.to_string(), - "<script src='/static/source_code/myscript.js'></script> -<script src='/static/source_code/myscript2.js'></script>" + "<script src='/source_code/myscript.js'></script> +<script src='/source_code/myscript2.js'></script>" ) } @@ -363,12 +390,12 @@ mod test_pages { let pmd = test_page_metadata(); assert_eq!( pmd.image.to_string(), - "<meta name='image' content='/static/images/testimage.png'/> -<meta property='og:image' content='/static/images/testimage.png'/> -<meta property='twitter:image' content='/static/images/testimage.png'/> -<meta name='image' content='/static/images/testimage2.png'/> -<meta property='og:image' content='/static/images/testimage2.png'/> -<meta property='twitter:image' content='/static/images/testimage2.png'/>" + "<meta name='image' content='/images/testimage.png'/> +<meta property='og:image' content='/images/testimage.png'/> +<meta property='twitter:image' content='/images/testimage.png'/> +<meta name='image' content='/images/testimage2.png'/> +<meta property='og:image' content='/images/testimage2.png'/> +<meta property='twitter:image' content='/images/testimage2.png'/>" ) } diff --git a/src/website/website.rs b/src/website/website.rs index ff976581c936064dcd45c96dc0b6852a9d2c89ca..d3a17dda8ce6555e80df52f738ff774f64dc2a28 100644 --- a/src/website/website.rs +++ b/src/website/website.rs @@ -2,7 +2,6 @@ use super::page::{Page, PageTemplate}; use crate::app::AppConfig; use crate::static_files::StaticFilesManager; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::path::PathBuf; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -18,7 +17,6 @@ pub struct WebSite { root_page: Page, pub static_files_manager: StaticFilesManager, templates: Vec<PageTemplate>, - pages_index: HashMap<PathBuf, Page>, } impl WebSiteBuilder { @@ -38,7 +36,6 @@ impl WebSiteBuilder { static_files_manager }, templates: self.templates.clone(), - pages_index: HashMap::new(), } } @@ -57,70 +54,11 @@ impl WebSiteBuilder { impl WebSite { pub fn build(&mut self) -> Self { - self.root_page.build_with_template( - self.templates - .iter() - .find(|t| t.name == self.root_page.template_name) - .expect("Page template not found") - .clone(), + self.root_page.build( + &self.templates, + PathBuf::from("/"), + &self.static_files_manager, ); - - self.root_page.build_html(); - - for p in self.root_page.sub_pages.iter_mut() { - p.build_with_template( - self.templates - .iter() - .find(|t| t.name == p.template_name) - .expect("Page template not found") - .clone(), - ); - p.build_html(); - } - - self.build_pages_index(self.root_page.clone(), PathBuf::from("/")); self.clone() } - - fn build_pages_index(&mut self, root_page: Page, from_url: PathBuf) { - let url = from_url.join(&root_page.metadata.url_slug); - - self.pages_index.insert(url.clone(), root_page.clone()); - - for p in root_page.sub_pages { - self.build_pages_index(p, url.clone()); - } - } - - pub fn get_page_by_url(&self, url: &PathBuf) -> Option<&Page> { - self.pages_index.get(&PathBuf::from("/").join(url)) - } -} - -#[cfg(test)] -mod test_website { - use super::*; - use crate::testing::TEST_JSON_WEBSITE; - - #[test] - fn test_index_pages_by_slug() { - let website = WebSiteBuilder::from_json(TEST_JSON_WEBSITE) - .with_static_files_manager(StaticFilesManager { - dir: PathBuf::from("."), - index: Vec::new(), - }) - .build(); - - let root_page = website.get_page_by_url(&PathBuf::from("/")); - assert!(root_page.is_some()); - assert_eq!(root_page.unwrap().metadata.title, "TEST"); - - let sub_page = website.get_page_by_url(&PathBuf::from("subpage")); - assert!(sub_page.is_some()); - assert_eq!(sub_page.unwrap().metadata.title, "TEST SUBPAGE"); - - let nested_page = website.get_page_by_url(&PathBuf::from("subpage/nested")); - assert!(nested_page.is_some()); - assert_eq!(nested_page.unwrap().metadata.title, "TEST NESTED"); - } } diff --git a/templates/new_website.json b/templates/new_website.json index 392249b0714e3bf8057f4fd3b2e1b68d9cb07b90..5ae0438a3a7b2a1426fc7207b481711d4b6becbc 100644 --- a/templates/new_website.json +++ b/templates/new_website.json @@ -12,6 +12,55 @@ "tag": "h1", "text": "New website" } + ], + "sub_pages": [ + { + "template_name": "Nav Content Footer", + "metadata": { + "title": "A sub page", + "description": "A new subpage", + "url_slug": "subpage", + "lang": "en" + }, + "body": [ + { + "tag": "h1", + "text": "subpage" + } + ], + "sub_pages": [ + { + "template_name": "Nav Content Footer", + "metadata": { + "title": "A nested page", + "description": "A new nested page", + "url_slug": "nested", + "lang": "en" + }, + "body": [ + { + "tag": "h1", + "text": "nested" + } + ] + } + ] + }, + { + "template_name": "Nav Content Footer", + "metadata": { + "title": "Another sub page", + "description": "A new subpage", + "url_slug": "other", + "lang": "en" + }, + "body": [ + { + "tag": "h1", + "text": "other" + } + ] + } ] }, "templates": [