From 3a8686ef40c9287983405269b1fdd0661b1f491a Mon Sep 17 00:00:00 2001
From: peterrabbit <pierre.jarriges@tutanota.com>
Date: Wed, 14 Sep 2022 23:17:55 +0200
Subject: [PATCH] pages served as html static real files

---
 README.md                        | 17 +++-----
 example.json                     | 14 +++----
 src/main.rs                      |  5 +--
 src/service/admin.rs             |  4 +-
 src/service/mod.rs               |  2 -
 src/service/page.rs              | 17 --------
 src/static_files/static_files.rs | 67 ++++++++++++++++++++----------
 src/testing.rs                   |  8 ++--
 src/website/mod.rs               |  1 +
 src/website/page.rs              | 69 +++++++++++++++++++++----------
 src/website/website.rs           | 70 ++------------------------------
 templates/new_website.json       | 49 ++++++++++++++++++++++
 12 files changed, 168 insertions(+), 155 deletions(-)
 delete mode 100644 src/service/page.rs

diff --git a/README.md b/README.md
index 54f1df5..8cfdc88 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 01cab40..5411f33 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 c302b94..00a89e6 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 2f7898d..0366a0c 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 61f75ae..f461f48 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 c0bfe7f..0000000
--- 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 4e4f601..74cd130 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 d4bd942..0ab5f7b 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 df329bf..c1c6ad9 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 c8a2029..92df3d3 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 ff97658..d3a17dd 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 392249b..5ae0438 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": [
-- 
GitLab