diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/Cargo.toml | 20 | ||||
-rw-r--r-- | server/src/database.rs | 13 | ||||
-rw-r--r-- | server/src/frontend/mod.rs | 2 | ||||
-rw-r--r-- | server/src/frontend/pages/home.rs | 14 | ||||
-rw-r--r-- | server/src/frontend/pages/layout.rs | 20 | ||||
-rw-r--r-- | server/src/frontend/pages/mod.rs | 69 | ||||
-rw-r--r-- | server/src/frontend/pages/node.rs | 60 | ||||
-rw-r--r-- | server/src/frontend/style/layout.css | 42 | ||||
-rw-r--r-- | server/src/frontend/style/mod.rs | 2 | ||||
-rw-r--r-- | server/src/library.rs | 141 | ||||
-rw-r--r-- | server/src/main.rs | 45 | ||||
-rw-r--r-- | server/src/metadata.rs | 11 |
12 files changed, 439 insertions, 0 deletions
diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 0000000..70b8752 --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "jellything" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.91" + +log = "0.4.17" +env_logger = "0.10.0" +anyhow = "1.0.68" + +rocket = "0.5.0-rc.2" +tokio = "1.24.1" +markup = "0.13.1" +chashmap = "2.2.2" + +sled = "0.34.7" +typed-sled = "0.2.3" diff --git a/server/src/database.rs b/server/src/database.rs new file mode 100644 index 0000000..3ba8c52 --- /dev/null +++ b/server/src/database.rs @@ -0,0 +1,13 @@ +use anyhow::Context; + +#[derive(Debug)] +pub struct Database { + pub db: sled::Db, +} + +impl Database { + pub fn open(path: &str) -> Result<Self, anyhow::Error> { + let db = sled::open(path).context("opening database")?; + Ok(Self { db }) + } +} diff --git a/server/src/frontend/mod.rs b/server/src/frontend/mod.rs new file mode 100644 index 0000000..99c22f8 --- /dev/null +++ b/server/src/frontend/mod.rs @@ -0,0 +1,2 @@ +pub mod style; +pub mod pages; diff --git a/server/src/frontend/pages/home.rs b/server/src/frontend/pages/home.rs new file mode 100644 index 0000000..5076177 --- /dev/null +++ b/server/src/frontend/pages/home.rs @@ -0,0 +1,14 @@ +use crate::frontend::pages::node::NodePage; +use crate::{frontend::pages::HtmlTemplate, AppState}; +use rocket::{get, State}; + +#[get("/")] +pub async fn page_home(state: &State<AppState>) -> HtmlTemplate<markup::DynRender> { + HtmlTemplate( + "Home".to_string(), + markup::new! { + p { "Welcome to Jellything" } + @NodePage { node: state.library.root.clone() } + }, + ) +} diff --git a/server/src/frontend/pages/layout.rs b/server/src/frontend/pages/layout.rs new file mode 100644 index 0000000..5654d3b --- /dev/null +++ b/server/src/frontend/pages/layout.rs @@ -0,0 +1,20 @@ +use markup::Render; + +markup::define! { + Layout<Main: Render>(title: String, main: Main) { + @markup::doctype() + html { + head { + title { @title " - Jellything" } + link[rel="stylesheet", href="/assets/style.css"]; + } + body { + nav { + h1 { "Jellything" } + + } + #main { @main } + } + } + } +} diff --git a/server/src/frontend/pages/mod.rs b/server/src/frontend/pages/mod.rs new file mode 100644 index 0000000..a20fa0e --- /dev/null +++ b/server/src/frontend/pages/mod.rs @@ -0,0 +1,69 @@ +use markup::Render; +use rocket::{ + http::ContentType, + response::{self, Responder}, + Request, Response, +}; +use std::{fmt::Display, io::Cursor}; + +use self::layout::Layout; + +pub mod home; +pub mod layout; +pub mod node; + +pub struct HtmlTemplate<T>(pub String, pub T); + +impl<'r, T: Render> Responder<'r, 'static> for HtmlTemplate<T> { + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { + let mut out = String::new(); + Layout { + title: self.0, + main: self.1, + } + .render(&mut out) + .unwrap(); + Response::build() + .header(ContentType::HTML) + .streamed_body(Cursor::new(out)) + .ok() + } +} + +#[derive(Debug)] +pub struct MyError(anyhow::Error); + +impl<'r> Responder<'r, 'static> for MyError { + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { + let mut out = String::new(); + Layout { + title: "Error".to_string(), + main: markup::new! { + h2 { "An error occured. Nobody is sorry"} + pre.error { @format!("{:?}", self.0) } + }, + } + .render(&mut out) + .unwrap(); + Response::build() + .header(ContentType::HTML) + .streamed_body(Cursor::new(out)) + .ok() + } +} + +impl Display for MyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} +impl From<anyhow::Error> for MyError { + fn from(err: anyhow::Error) -> MyError { + MyError(err) + } +} +impl From<std::fmt::Error> for MyError { + fn from(err: std::fmt::Error) -> MyError { + MyError(anyhow::anyhow!("{err}")) + } +} diff --git a/server/src/frontend/pages/node.rs b/server/src/frontend/pages/node.rs new file mode 100644 index 0000000..7ac4332 --- /dev/null +++ b/server/src/frontend/pages/node.rs @@ -0,0 +1,60 @@ +use crate::{ + frontend::pages::HtmlTemplate, + library::{Directory, Item, Node}, + AppState, +}; +use anyhow::Context; +use rocket::{get, uri, State}; +use std::{ops::Deref, path::PathBuf, sync::Arc}; + +use super::MyError; + +#[get("/library/<path..>")] +pub async fn page_library_node( + path: PathBuf, + state: &State<AppState>, +) -> Result<HtmlTemplate<markup::DynRender>, MyError> { + let path = path.to_str().unwrap().to_string(); + let node = state + .library + .nested(&path) + .context("retrieving library node")? + .clone(); + Ok(HtmlTemplate( + format!("{}", node.title()), + markup::new! { + @NodePage { node: node.clone() } + }, + )) +} + +markup::define! { + NodePage(node: Arc<Node>) { + @match node.deref() { + Node::Directory(dir) => { @DirectoryPage { dir: dir.clone() } } + Node::Item(item) => { @ItemPage { item: item.clone() } } + } + } + DirectoryCard(dir: Arc<Directory>) { + span { a[href=&uri!(page_library_node(&dir.lib_path)).to_string()] { @dir.data.name } } + } + DirectoryPage(dir: Arc<Directory>) { + h1 { @dir.data.name } + ul.directorylisting { + @for el in &dir.children { + li { + span.title { @match el.deref().to_owned() { + Node::Directory(dir) => { @DirectoryCard { dir } } + Node::Item(item) => { @ItemCard { item } } + }} + } + } + } + } + ItemCard(item: Arc<Item>) { + span { a[href=&uri!(page_library_node(&item.lib_path)).to_string()] { @item.data.title } } + } + ItemPage(item: Arc<Item>) { + h1 { @item.data.title } + } +} diff --git a/server/src/frontend/style/layout.css b/server/src/frontend/style/layout.css new file mode 100644 index 0000000..4418903 --- /dev/null +++ b/server/src/frontend/style/layout.css @@ -0,0 +1,42 @@ +@import url("https://s.metamuffin.org/static/font-ubuntu/include.css"); + +* { + color: white; + font-family: "Ubuntu", sans-serif; + font-weight: 300; + margin: 0px; + padding: 0px; +} + +body { + background-color: #1a1a1a; + width: 100vw; +} + +nav { + position: absolute; + top: 0px; + left: 0px; + padding: 1em; + width: calc(100vw - 2em); + height: 2em; + background-color: #41414144; +} + +nav h1 { + margin: 0px; + font-size: 1.5em; +} + +#main { + margin-top: 5em; + padding: 1em; + padding-left: 3em; + padding-right: 3em; +} + +.error { + padding: 1em; + color: rgb(255, 117, 117); + font-family: monospace; +} diff --git a/server/src/frontend/style/mod.rs b/server/src/frontend/style/mod.rs new file mode 100644 index 0000000..9d0729e --- /dev/null +++ b/server/src/frontend/style/mod.rs @@ -0,0 +1,2 @@ + +pub const CSS_BUNDLE: &'static str = include_str!("layout.css"); diff --git a/server/src/library.rs b/server/src/library.rs new file mode 100644 index 0000000..0c42a73 --- /dev/null +++ b/server/src/library.rs @@ -0,0 +1,141 @@ +use crate::metadata::{DirectoryInfo, ItemInfo}; +use anyhow::{anyhow, bail, Context, Ok}; +use std::{ffi::OsStr, fs::File, path::PathBuf, sync::Arc}; + +pub struct Library { + pub root: Arc<Node>, +} + +#[derive(Debug, Clone)] +pub enum Node { + Directory(Arc<Directory>), + Item(Arc<Item>), +} + +#[derive(Debug, Clone)] +pub struct Directory { + pub lib_path: PathBuf, + pub identifier: String, + pub data: DirectoryInfo, + pub children: Vec<Arc<Node>>, +} + +#[derive(Debug, Clone)] +pub struct Item { + pub lib_path: PathBuf, + pub identifier: String, + pub data: ItemInfo, +} + +impl Library { + pub fn open(path: &str) -> anyhow::Result<Self> { + Ok(Self { + root: Node::from_path(path.into(), PathBuf::new(), true).context("indexing root")?, + }) + } + pub fn nested(&self, path: &str) -> anyhow::Result<Arc<Node>> { + let mut n = self.root.clone(); + if path == "" { + return Ok(n); + } + for seg in path.split("/") { + n = n + .get_directory()? + .child_by_ident(seg) + .ok_or(anyhow!("does not exist"))?; + } + Ok(n) + } +} + +impl Node { + pub fn get_directory(&self) -> anyhow::Result<&Directory> { + match self { + Node::Directory(d) => Ok(d), + Node::Item(_) => bail!("not a directory"), + } + } + pub fn title(&self) -> &str { + match self { + Node::Directory(d) => &d.data.name, + Node::Item(i) => &i.data.title, + } + } + pub fn identifier(&self) -> &str { + match self { + Node::Directory(d) => &d.identifier, + Node::Item(i) => &i.identifier, + } + } + pub fn from_path( + path: PathBuf, + mut lib_path: PathBuf, + root: bool, + ) -> anyhow::Result<Arc<Node>> { + if path.is_dir() { + let mpath = path.join("directory.json"); + let data: DirectoryInfo = + serde_json::from_reader(File::open(mpath).context("metadata missing")?)?; + + let identifier = path.file_name().unwrap().to_str().unwrap().to_string(); + if !root { + lib_path = lib_path.join(identifier.clone()); + } + + let children = path + .read_dir()? + .map(|e| e.unwrap().path()) + .filter(|e| e.extension() != Some(OsStr::new("json"))) + .map(|e| { + Node::from_path(e.clone(), lib_path.clone(), false) + .context(format!("loading {e:?}")) + }) + .into_iter() + .collect::<anyhow::Result<Vec<_>>>()?; + + Ok(Node::Directory(Arc::new(Directory { + lib_path, + children, + data, + identifier, + })) + .into()) + } else if path.is_file() { + let mpath = path.clone().with_extension("metadata.json"); + let datafile = File::open(mpath.clone()) + .context(format!("metadata missing, tried path {mpath:?}"))?; + let data: ItemInfo = serde_json::from_reader(datafile).context("invalid metadata")?; + let identifier = path + .with_extension("") + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(); + Ok(Node::Item(Arc::new(Item { + lib_path: lib_path.join(identifier.clone()), + data, + identifier, + })) + .into()) + } else { + bail!("did somebody really put a fifo or socket in the library?!") + } + } +} +impl Item { + pub fn path(&self) -> String { + self.lib_path.to_str().unwrap().to_string() + } +} +impl Directory { + pub fn path(&self) -> String { + self.lib_path.to_str().unwrap().to_string() + } + pub fn child_by_ident(&self, i: &str) -> Option<Arc<Node>> { + self.children + .iter() + .find(|e| e.identifier() == i) + .map(|e| e.to_owned()) + } +} diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 0000000..b7a3249 --- /dev/null +++ b/server/src/main.rs @@ -0,0 +1,45 @@ +#![feature(box_syntax)] + +use crate::frontend::style::CSS_BUNDLE; +use database::Database; +use frontend::pages::{home::page_home, node::page_library_node}; +use library::Library; +use rocket::{get, http::ContentType, launch, routes}; +use std::fs::read_to_string; + +pub mod database; +pub mod frontend; +pub mod library; +pub mod metadata; + +#[get("/assets/style.css")] +async fn assets_style() -> (ContentType, String) { + ( + ContentType::CSS, + if cfg!(debug_assertions) { + read_to_string("src/frontend/style/layout.css").unwrap() + } else { + CSS_BUNDLE.to_string() + }, + ) +} + +pub struct AppState { + pub database: Database, + pub library: Library, +} + +#[launch] +fn rocket() -> _ { + env_logger::init_from_env("LOG"); + let db_path = std::env::var("DB_PATH").unwrap_or("data/db".to_string()); + let lib_path = std::env::var("LIB_PATH").unwrap_or("data/library".to_string()); + let state = AppState { + library: Library::open(&lib_path).unwrap(), + database: Database::open(&db_path).unwrap(), + }; + + rocket::build() + .manage(state) + .mount("/", routes![page_home, page_library_node, assets_style]) +} diff --git a/server/src/metadata.rs b/server/src/metadata.rs new file mode 100644 index 0000000..ff45af2 --- /dev/null +++ b/server/src/metadata.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct DirectoryInfo { + pub name: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ItemInfo { + pub title: String, +} |