summary refs log tree commit diff
path: root/src/data
diff options
context:
space:
mode:
Diffstat (limited to 'src/data')
-rw-r--r--src/data/config.rs49
-rw-r--r--src/data/content.rs262
-rw-r--r--src/data/mod.rs73
-rw-r--r--src/data/namespace.rs96
-rw-r--r--src/data/page.rs151
5 files changed, 353 insertions, 278 deletions
diff --git a/src/data/config.rs b/src/data/config.rs
index 9a17077..11e10cc 100644
--- a/src/data/config.rs
+++ b/src/data/config.rs
@@ -13,34 +13,55 @@ struct ConfigFile {
 }
 
 pub struct Config {
-    site_title: String,
-    data_dir: PathBuf,
-    external_root: String,
-    listen_port: u16,
-    footer_copyright: Option<String>
+    pub site_title: String,
+    pub data_dir: PathBuf,
+    pub external_root: String,
+    pub listen_port: u16,
+    pub footer_copyright: Option<String>,
 }
 
 #[cfg(feature = "ssr")]
 impl Config {
-    pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
-        let config_contents = fs::read_to_string(&path)
+    pub fn read_from_file() -> Result<Self, String> {
+        let config_path = Self::get_location()?;
+        let config_contents = fs::read_to_string(&config_path)
             .map_err(|_| "Unable to open config file".to_string())?;
 
-        let file : ConfigFile = toml::from_str(&config_contents)
-            .map_err(|err| err.to_string())?;
+        let file: ConfigFile = toml::from_str(&config_contents).map_err(|err| err.to_string())?;
 
         let port = file.listen_port.unwrap_or(3000);
 
         Ok(Self {
-            site_title: file.site_title
+            site_title: file
+                .site_title
                 .unwrap_or("Untitled StormScribe Site".to_string()),
-            data_dir: file.data_dir.unwrap_or(path.as_ref()
-                .canonicalize().map_err(|_| "Cannot resolve config file location".to_string())?
-                .parent().ok_or("Cannot resolve data dir".to_string())?.to_path_buf()),
-            external_root: file.external_root
+            data_dir: file.data_dir.unwrap_or(
+                PathBuf::from(&config_path)
+                    .canonicalize()
+                    .map_err(|_| "Cannot resolve config file location".to_string())?
+                    .parent()
+                    .ok_or("Cannot resolve data dir".to_string())?
+                    .to_path_buf(),
+            ),
+            external_root: file
+                .external_root
                 .unwrap_or(format!("http://localhost:{port}/")),
             listen_port: port,
             footer_copyright: file.footer_copyright,
         })
     }
+
+    fn get_location() -> Result<String, String> {
+        Ok(
+            std::env::var("STORMSCRIBE_CONFIG_FILE").or_else(|_| -> Result<String, String> {
+                Ok(std::path::Path::join(
+                    &std::env::current_dir()
+                        .map_err(|_| "Could not read current directory".to_string())?,
+                    "config.toml",
+                )
+                .to_string_lossy()
+                .to_string())
+            })?,
+        )
+    }
 }
diff --git a/src/data/content.rs b/src/data/content.rs
deleted file mode 100644
index 4a39967..0000000
--- a/src/data/content.rs
+++ /dev/null
@@ -1,262 +0,0 @@
-#[cfg(feature="ssr")]
-use std::fs::File;
-use std::collections::HashMap;
-#[cfg(feature="ssr")]
-use std::io::{BufRead, BufReader};
-use std::path::{PathBuf, Path};
-use std::sync::Arc;
-#[cfg(feature="ssr")]
-use tokio::sync::RwLock;
-use chrono::{DateTime, Utc};
-use leptos::prelude::StorageAccess;
-use serde::Deserialize;
-use uuid::Uuid;
-#[cfg(feature="ssr")]
-use fs2::FileExt;
-#[cfg(feature="ssr")]
-use tokio::runtime;
-#[cfg(feature="ssr")]
-use tokio_stream::wrappers::ReadDirStream;
-#[cfg(feature="ssr")]
-use futures::stream::StreamExt;
-
-#[derive(Hash, PartialEq, Eq, Clone)]
-pub struct PageUuid(Uuid);
-#[derive(Hash, PartialEq, Eq, Clone)]
-pub struct NamespaceUuid(Uuid);
-#[derive(Hash, PartialEq, Eq, Clone)]
-pub struct MediaUuid(Uuid);
-
-pub struct ContentSnapshot {
-    pub pages: HashMap<PageUuid, Page>,
-    pub namespaces: HashMap<NamespaceUuid, Namespace>,
-    media: HashMap<MediaUuid, Media>,
-
-    pub namespace_paths: HashMap<String, NamespaceUuid>,
-    pub page_paths: HashMap<String, PageUuid>,
-    media_paths: HashMap<String, MediaUuid>,
-
-    pub render_cache: HashMap<PageUuid, String>,
-}
-
-pub struct Page {
-    pub uuid: PageUuid,
-    pub namespace: NamespaceUuid,
-    pub author: Uuid,
-    pub title: String,
-    pub slug: String,
-    pub current_version: DateTime<Utc>,
-    pub prev_versions: Vec<DateTime<Utc>>,
-    content_offset: usize,
-}
-
-pub struct Namespace {
-    pub uuid: NamespaceUuid,
-    pub path: String,
-    pub pages: Vec<PageUuid>,
-}
-
-struct Media {
-    uuid: MediaUuid,
-    filename: String,
-    mime_type: String,
-    uploaded_by: Uuid,
-    uploaded_on: Uuid,
-    used_on: Vec<PageUuid>,
-}
-
-#[cfg(feature="ssr")]
-#[derive(Clone)]
-pub struct ContentController {
-    snapshot: Arc<RwLock<Box<Arc<ContentSnapshot>>>>,
-    lock: Arc<File>,
-}
-
-#[cfg(feature = "ssr")]
-impl ContentController {
-    pub async fn init(data_dir: PathBuf) -> Result<Self, String> {
-        let lock_path = Path::join(&data_dir, ".lock");
-        let lockfile = std::fs::OpenOptions::new()
-            .read(true).write(true).create(true)
-            .open(&lock_path)
-            .map_err(|_| "Could not open data directory".to_string())?;
-
-        lockfile.try_lock_exclusive()
-            .map_err(|_| "Could not lock data directory".to_string())?;
-
-        // Read the things
-        let snapshot = Self::read_data(&data_dir).await?;
-
-        Ok(Self {
-            lock: Arc::new(lockfile),
-            snapshot: Arc::new(RwLock::new(Box::new(Arc::new(snapshot)))),
-        })
-    }
-
-    async fn read_data(data_dir: &PathBuf) -> Result<ContentSnapshot, String> {
-        use tokio::fs;
-
-        let pagedata_cache = Arc::new(tokio::sync::Mutex::new(HashMap::<PageUuid, (String, NamespaceUuid)>::new()));
-
-        let namespace_names_dir = Path::join(&data_dir, "namespaces/names");
-        let namespace_ids_dir = Path::join(&data_dir, "namespaces/id");
-        let namespaces = fs::read_dir(&namespace_names_dir).await
-            .map_err(|_| "Could not open namespace directory".to_string())
-            .map(|dir_entries| { ReadDirStream::new(dir_entries) })?
-            .filter_map(async |dir_entry| -> Option<Namespace> {
-                let link_path = dir_entry.as_ref().ok()?.path();
-                let target_path = dir_entry.as_ref().ok()?
-                    .metadata().await.ok()?
-                    .is_symlink()
-                    .then_some(
-                        fs::read_link(link_path).await.ok()
-                    )??;
-
-                let last_segment = target_path.file_name()?;
-                target_path.parent()?
-                    .eq(&namespace_ids_dir).then_some(())?;
-
-                let namespace_name = dir_entry.as_ref().ok()?.file_name().to_str()?.to_string();
-                let namespace_uuid = NamespaceUuid(Uuid::try_parse(last_segment.to_str()?).ok()?);
-
-                let namespace_pages = fs::read_dir(Path::join(&namespace_ids_dir, last_segment).join("pages")).await.ok()?;
-                let namespace_page_uuids = ReadDirStream::new(namespace_pages)
-                    .filter_map(async |dir_entry| -> Option<PageUuid> {
-                        let page_path = dir_entry.as_ref().ok()?.path();
-                        let page_uuid = dir_entry.as_ref().ok()?
-                            .metadata().await.ok()?
-                            .is_symlink()
-                            .then_some(
-                                fs::read_link(&page_path).await.ok()
-                            )??;
-
-                        let page_uuid = PageUuid(Uuid::try_parse(&page_uuid.to_str()?).ok()?);
-                        let page_slug = page_path.file_name()?.to_str()?.to_string();
-
-                        pagedata_cache.lock().await.insert(page_uuid.clone(), (page_slug, namespace_uuid.clone()));
-
-                        Some(page_uuid)
-                    }).collect::<Vec<PageUuid>>().await;
-
-                Some(Namespace {
-                    uuid: namespace_uuid,
-                    path: namespace_name,
-                    pages: namespace_page_uuids,
-                })
-            }).collect::<Vec<Namespace>>().await;
-
-        let (namespaces_by_id, namespace_paths): (HashMap<_,_>, HashMap<_,_>) =
-            namespaces.into_iter()
-            .map(|namespace| {
-                let namespace_uuid = namespace.uuid.clone();
-                let namespace_path = namespace.path.clone();
-                (
-                    (namespace_uuid.clone(), namespace),
-                    (namespace_path, namespace_uuid)
-                )
-            })
-            .unzip();
-
-        let pages_dir = Path::join(&data_dir, "pages");
-        let pages = fs::read_dir(&pages_dir).await
-            .map_err(|_| "Could not open pages data directory".to_string())
-            .map(|dir_entries| { ReadDirStream::new(dir_entries) })?
-            .filter_map(async |dir_entry| -> Option<Page> {
-                let page_dir_path = dir_entry.as_ref().ok()?.path();
-                let current_path = dir_entry.as_ref().ok()?
-                    .metadata().await.ok()?
-                    .is_dir()
-                    .then_some(
-                        fs::read_link(Path::join(&page_dir_path, "current")).await.ok()
-                    )??;
-                
-                Page::init_from_file(&current_path, pagedata_cache.lock().await.as_borrowed()).await
-            }).collect::<Vec<Page>>().await;
-
-        let (pages_by_id, page_paths): (HashMap<_,_>, HashMap<_,_>) =
-            pages.into_iter()
-            .filter_map(|page| {
-                let page_uuid = page.uuid.clone();
-                let namespace_path = &namespaces_by_id.get(&page.namespace)?.path;
-                let page_path = page.slug.clone();
-
-                Some((
-                    (page_uuid.clone(), page),
-                    (format!("{namespace_path}/{page_path}"), page_uuid)
-                ))
-            })
-            .unzip();
-
-        Ok(ContentSnapshot {
-            pages: pages_by_id,
-            namespaces: namespaces_by_id,
-            media: HashMap::new(),
-            namespace_paths,
-            page_paths,
-            media_paths: HashMap::new(),
-            render_cache: HashMap::new(),
-        })
-    }
-
-    pub async fn get_snapshot(&self) -> Arc<ContentSnapshot> {
-        self.snapshot.read().await.as_ref().clone()
-    }
-
-    pub async fn replace_state(&self, updated: ContentSnapshot) {
-        todo!()
-    }
-}
-
-const METADATA_DIVIDER : &'static str = "<!-- trans rights ~ath&+ -->";
-
-#[cfg(feature = "ssr")]
-impl Page {
-    async fn init_from_file(path: &PathBuf, pagedata_cache: &HashMap::<PageUuid, (String, NamespaceUuid)>) -> Option<Self> {
-        let mut reader = BufReader::new(File::open(path).ok()?);
-        let page_uuid = PageUuid(Uuid::try_parse(&path.parent()?.file_name()?.to_str()?).ok()?);
-        let (page_slug, namespace_uuid) = pagedata_cache.get(&page_uuid)?.as_borrowed();
-
-        let mut metadata_string = String::new();
-        let mut current_line = String::new();
-        let mut content_offset = 0;
-        while let Ok(size) = reader.read_line(&mut current_line) {
-            content_offset += size;
-            if size == 0 {
-                return None
-            }
-
-            if current_line == METADATA_DIVIDER {
-                break
-            }
-
-            metadata_string.push_str(&current_line);
-            current_line.truncate(0);
-        }
-
-        #[derive(Deserialize)]
-        struct PageMetadata {
-            title: String,
-            author: String,
-            prev_versions: Option<Vec<String>>,
-        }
-
-        let metadata : PageMetadata = toml::from_str(&metadata_string).ok()?;
-        let current_version = DateTime::parse_from_rfc3339(path.file_name()?.to_str()?.replace("_", ":").as_str()).ok()?.to_utc();
-        let prev_versions = metadata.prev_versions
-            .unwrap_or(Vec::new())
-            .iter()
-            .filter_map(|str| DateTime::parse_from_rfc3339(str.replace("_", ":").as_str()).ok().map(|timestamp| timestamp.to_utc()))
-            .collect::<Vec<_>>();
-
-        Some(Page {
-            uuid: page_uuid,
-            author: Uuid::try_parse(&metadata.author).ok()?,
-            title: metadata.title,
-            namespace: namespace_uuid.clone(),
-            slug: page_slug.clone(),
-            current_version,
-            prev_versions,
-            content_offset,
-        })
-    }
-}
diff --git a/src/data/mod.rs b/src/data/mod.rs
index 4767914..0c61bd7 100644
--- a/src/data/mod.rs
+++ b/src/data/mod.rs
@@ -1,3 +1,72 @@
-pub mod config;
-pub mod content;
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
 
+#[cfg(feature = "ssr")]
+use fs2::FileExt;
+#[cfg(feature = "ssr")]
+use std::fs::File;
+#[cfg(feature = "ssr")]
+use std::sync::LazyLock;
+
+use std::{collections::HashMap, path::Path};
+
+mod config;
+mod namespace;
+mod page;
+
+use config::Config;
+pub use namespace::{Namespace, Namespaces};
+pub use page::{Page, Pages};
+
+#[derive(Hash, PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
+pub struct PageUuid(Uuid);
+
+#[cfg(feature = "ssr")]
+pub static CONFIG: LazyLock<Config> =
+    LazyLock::new(|| Config::read_from_file().expect("Could not open config file"));
+
+#[cfg(feature = "ssr")]
+static DATA_LOCK: LazyLock<StormscribeData> = LazyLock::new(|| {
+    let config = &CONFIG;
+    let lock_path = Path::join(&config.data_dir, ".lock");
+    let lockfile = std::fs::OpenOptions::new()
+        .read(true)
+        .write(true)
+        .create(true)
+        .open(&lock_path)
+        .map_err(|_| "Could not open data directory".to_string())
+        .unwrap();
+
+    lockfile
+        .try_lock_exclusive()
+        .map_err(|_| "Could not lock data directory".to_string())
+        .unwrap();
+
+    StormscribeData {
+        file_lock: lockfile,
+        namespaces: Namespaces::init(&Path::join(&config.data_dir, "namespace/")).unwrap(),
+        pages: Pages::init(&Path::join(&config.data_dir, "pages/")).unwrap(),
+    }
+});
+
+#[cfg(feature = "ssr")]
+pub struct StormscribeData {
+    file_lock: File,
+    namespaces: Namespaces,
+    pages: Pages,
+}
+
+#[cfg(feature = "ssr")]
+impl StormscribeData {
+    fn get_data() -> &'static Self {
+        &DATA_LOCK
+    }
+
+    pub fn get_namespace() -> Namespace {
+        DATA_LOCK.namespaces.root.clone()
+    }
+
+    pub fn get_pages() -> HashMap<PageUuid, Page> {
+        DATA_LOCK.pages.pages.clone()
+    }
+}
diff --git a/src/data/namespace.rs b/src/data/namespace.rs
new file mode 100644
index 0000000..714ab37
--- /dev/null
+++ b/src/data/namespace.rs
@@ -0,0 +1,96 @@
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use uuid::Uuid;
+
+use crate::data::PageUuid;
+#[cfg(feature = "ssr")]
+use std::{
+    fs,
+    path::{Path, PathBuf},
+};
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Namespace {
+    pub page: Option<PageUuid>,
+    pub children: HashMap<String, Namespace>,
+}
+
+pub struct Namespaces {
+    pub root: Namespace,
+}
+
+impl Namespace {
+    pub fn new() -> Self {
+        Self {
+            page: None,
+            children: HashMap::new(),
+        }
+    }
+}
+
+#[cfg(feature = "ssr")]
+impl Namespaces {
+    pub fn init(namespaces_dir: &Path) -> Result<Self, String> {
+        // Read dir recursive
+
+        let mut paths = Vec::new();
+        Self::scan_dir(namespaces_dir, &mut paths);
+        let paths = paths
+            .into_iter()
+            .map(|path| PathBuf::from(path.strip_prefix(namespaces_dir).unwrap()))
+            .collect::<Vec<_>>();
+
+        // Build lookup
+        let mut root = Namespace::new();
+
+        for path in paths {
+            let mut current_node = &mut root;
+
+            for segment in path.iter() {
+                let segment = segment.to_string_lossy().to_string();
+                if segment == "_page" {
+                    let link_target = namespaces_dir.join(&path).read_link().unwrap();
+                    let uuid_string = link_target.file_name().unwrap().to_str().unwrap();
+                    let page_uuid = PageUuid(Uuid::try_parse(uuid_string).unwrap());
+                    current_node.page = Some(page_uuid);
+                } else {
+                    current_node
+                        .children
+                        .insert(segment.clone(), Namespace::new());
+
+                    current_node = current_node.children.get_mut(&segment).unwrap();
+                }
+            }
+        }
+
+        Ok(Self { root })
+    }
+
+    pub fn get_page_uuid(&self, path: String) -> Option<PageUuid> {
+        todo!()
+    }
+
+    pub fn remove_page(&self, path: String) -> Result<(), String> {
+        todo!()
+    }
+
+    pub fn add_page(&self, path: String, uuid: PageUuid) -> Result<(), String> {
+        todo!()
+    }
+
+    fn scan_dir(current_dir: &Path, out_vec: &mut Vec<PathBuf>) {
+        if !current_dir.is_dir() {
+            return;
+        }
+
+        for entry in fs::read_dir(current_dir).unwrap() {
+            let entry_path = entry.unwrap().path();
+
+            if entry_path.is_dir() && !entry_path.is_symlink() {
+                Self::scan_dir(&entry_path, out_vec);
+            } else {
+                out_vec.push(entry_path.into());
+            }
+        }
+    }
+}
diff --git a/src/data/page.rs b/src/data/page.rs
new file mode 100644
index 0000000..6d0b802
--- /dev/null
+++ b/src/data/page.rs
@@ -0,0 +1,151 @@
+use std::{
+    collections::HashMap,
+    fs::{self, File},
+    io::{BufRead, BufReader},
+    path::Path,
+};
+
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+use super::PageUuid;
+
+#[derive(Clone, Deserialize, Serialize, Debug)]
+pub struct Page {
+    pub uuid: PageUuid,
+    pub author: Uuid,
+    pub title: String,
+    pub current_version: DateTime<Utc>,
+    pub prev_versions: Vec<DateTime<Utc>>,
+    content_offset: usize,
+}
+
+pub struct Pages {
+    pub pages: HashMap<PageUuid, Page>,
+}
+
+const METADATA_DIVIDER: &'static str = "<!-- trans rights ~ath&+ -->";
+
+#[cfg(feature = "ssr")]
+impl Pages {
+    pub fn init(pages_dir: &Path) -> Result<Self, String> {
+        // Read dir
+        let page_dirs = fs::read_dir(&pages_dir)
+            .map_err(|_| "Could not open pages data directory".to_string())?;
+
+        // Parse each
+        let pages = page_dirs
+            .map(|dir_entry| -> Result<Page, String> {
+                let page_dir_path = dir_entry.as_ref().unwrap().path();
+
+                Pages::read_page(&page_dir_path)
+            })
+            .collect::<Result<Vec<Page>, String>>()?;
+
+        // Build lookup
+        Ok(Self {
+            pages: pages
+                .into_iter()
+                .map(|page| (page.uuid.clone(), page))
+                .collect::<HashMap<_, _>>(),
+        })
+    }
+
+    fn read_page(page_dir: &Path) -> Result<Page, String> {
+        let current_page = page_dir
+            .join("current")
+            .canonicalize()
+            .map_err(|_| "Could not canonicalize page location".to_string())?;
+
+        let mut reader = BufReader::new(
+            File::open(&current_page).map_err(|_| "Could not open page file".to_string())?,
+        );
+        let page_uuid = PageUuid(
+            Uuid::try_parse(
+                &page_dir
+                    .file_name()
+                    .ok_or("Could not read page directory".to_string())?
+                    .to_str()
+                    .unwrap(),
+            )
+            .map_err(|_| "Could not parse page UUID".to_string())?,
+        );
+
+        let mut metadata_string = String::new();
+        let mut current_line = String::new();
+        let mut content_offset = 0;
+        'readloop: while let Ok(size) = reader.read_line(&mut current_line) {
+            content_offset += size;
+            if size == 0 {
+                return Err("Page file is invalid".to_string());
+            }
+
+            if current_line.trim() == METADATA_DIVIDER {
+                break 'readloop;
+            }
+
+            metadata_string.push_str(&current_line);
+            current_line.truncate(0);
+        }
+
+        #[derive(Deserialize)]
+        struct PageMetadata {
+            title: String,
+            author: String,
+            prev_versions: Option<Vec<String>>,
+        }
+
+        let metadata: PageMetadata = toml::from_str(&metadata_string).map_err(|err| {
+            println!("{err:?}");
+            "Page metadata is invalid".to_string()
+        })?;
+        let current_version = DateTime::parse_from_rfc3339(
+            current_page
+                .file_name()
+                .unwrap()
+                .to_str()
+                .unwrap()
+                .replace("_", ":")
+                .as_str(),
+        )
+        .map_err(|_| "Invalid date format".to_string())?
+        .to_utc();
+        let prev_versions = metadata
+            .prev_versions
+            .unwrap_or(Vec::new())
+            .iter()
+            .filter_map(|str| {
+                DateTime::parse_from_rfc3339(str.replace("_", ":").as_str())
+                    .ok()
+                    .map(|timestamp| timestamp.to_utc())
+            })
+            .collect::<Vec<_>>();
+
+        Ok(Page {
+            uuid: page_uuid,
+            author: Uuid::try_parse(&metadata.author)
+                .map_err(|_| "Could not parse author UUID".to_string())?,
+            title: metadata.title,
+            current_version,
+            prev_versions,
+            content_offset,
+        })
+    }
+
+    pub fn get_page(&self, uuid: PageUuid) -> Option<Page> {
+        todo!()
+    }
+
+    pub fn create_page(&self, page: Page) {
+        todo!()
+    }
+
+    pub fn update_page(&self, page: Page) {
+        todo!()
+    }
+
+    pub fn delete_page(&self, uuid: PageUuid) -> Result<(), String> {
+        todo!()
+    }
+}