aboutsummaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/Cargo.toml20
-rw-r--r--server/src/database.rs13
-rw-r--r--server/src/frontend/mod.rs2
-rw-r--r--server/src/frontend/pages/home.rs14
-rw-r--r--server/src/frontend/pages/layout.rs20
-rw-r--r--server/src/frontend/pages/mod.rs69
-rw-r--r--server/src/frontend/pages/node.rs60
-rw-r--r--server/src/frontend/style/layout.css42
-rw-r--r--server/src/frontend/style/mod.rs2
-rw-r--r--server/src/library.rs141
-rw-r--r--server/src/main.rs45
-rw-r--r--server/src/metadata.rs11
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,
+}