aboutsummaryrefslogtreecommitdiff
path: root/server/src/ui/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/ui/mod.rs')
-rw-r--r--server/src/ui/mod.rs136
1 files changed, 136 insertions, 0 deletions
diff --git a/server/src/ui/mod.rs b/server/src/ui/mod.rs
new file mode 100644
index 0000000..b98fbec
--- /dev/null
+++ b/server/src/ui/mod.rs
@@ -0,0 +1,136 @@
+/*
+ This file is part of jellything (https://codeberg.org/metamuffin/jellything)
+ which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
+ Copyright (C) 2025 metamuffin <metamuffin.org>
+*/
+use crate::logic::session::Session;
+use error::MyResult;
+use home::rocket_uri_macro_r_home;
+use jellybase::CONF;
+use layout::{DynLayoutPage, LayoutPage};
+use log::debug;
+use markup::Render;
+use rocket::{
+ futures::FutureExt,
+ get,
+ http::{ContentType, Header, Status},
+ response::{self, Redirect, Responder},
+ Either, Request, Response,
+};
+use std::{
+ collections::hash_map::DefaultHasher,
+ future::Future,
+ hash::{Hash, Hasher},
+ io::Cursor,
+ os::unix::prelude::MetadataExt,
+ path::Path,
+ pin::Pin,
+};
+use tokio::{
+ fs::{read_to_string, File},
+ io::AsyncRead,
+};
+
+pub mod account;
+pub mod admin;
+pub mod assets;
+pub mod browser;
+pub mod error;
+pub mod home;
+pub mod layout;
+pub mod node;
+pub mod player;
+pub mod search;
+pub mod sort;
+pub mod stats;
+pub mod style;
+
+#[get("/")]
+pub async fn r_index(sess: Option<Session>) -> MyResult<Either<Redirect, DynLayoutPage<'static>>> {
+ if sess.is_some() {
+ Ok(Either::Left(Redirect::temporary(rocket::uri!(r_home()))))
+ } else {
+ let front = read_to_string(CONF.asset_path.join("front.htm")).await?;
+ Ok(Either::Right(LayoutPage {
+ title: "Home".to_string(),
+ content: markup::new! {
+ @markup::raw(&front)
+ },
+ ..Default::default()
+ }))
+ }
+}
+
+#[get("/favicon.ico")]
+pub async fn r_favicon() -> MyResult<File> {
+ Ok(File::open(CONF.asset_path.join("favicon.ico")).await?)
+}
+
+pub struct HtmlTemplate<'a>(pub markup::DynRender<'a>);
+
+impl<'r> Responder<'r, 'static> for HtmlTemplate<'_> {
+ fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'static> {
+ let mut out = String::new();
+ self.0.render(&mut out).unwrap();
+ Response::build()
+ .header(ContentType::HTML)
+ .sized_body(out.len(), Cursor::new(out))
+ .ok()
+ }
+}
+
+pub struct Defer(Pin<Box<dyn Future<Output = String> + Send>>);
+
+impl AsyncRead for Defer {
+ fn poll_read(
+ mut self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ buf: &mut tokio::io::ReadBuf<'_>,
+ ) -> std::task::Poll<std::io::Result<()>> {
+ match self.0.poll_unpin(cx) {
+ std::task::Poll::Ready(r) => {
+ buf.put_slice(r.as_bytes());
+ std::task::Poll::Ready(Ok(()))
+ }
+ std::task::Poll::Pending => std::task::Poll::Pending,
+ }
+ }
+}
+
+pub struct CacheControlFile(File, String);
+impl CacheControlFile {
+ pub async fn new_cachekey(p: &Path) -> anyhow::Result<Self> {
+ let tag = p.file_name().unwrap().to_str().unwrap().to_owned();
+ let f = File::open(p).await?;
+ Ok(Self(f, tag))
+ }
+ pub async fn new_mtime(f: File) -> Self {
+ let meta = f.metadata().await.unwrap();
+ let modified = meta.mtime();
+ let mut h = DefaultHasher::new();
+ modified.hash(&mut h);
+ let tag = format!("{:0>16x}", h.finish());
+ Self(f, tag)
+ }
+}
+impl<'r> Responder<'r, 'static> for CacheControlFile {
+ fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
+ let Self(file, tag) = self;
+ if req.headers().get_one("if-none-match") == Some(&tag) {
+ debug!("file cache: not modified");
+ Response::build()
+ .status(Status::NotModified)
+ .header(Header::new("cache-control", "private"))
+ .header(Header::new("etag", tag))
+ .ok()
+ } else {
+ debug!("file cache: transfer");
+ Response::build()
+ .status(Status::Ok)
+ .header(Header::new("cache-control", "private"))
+ .header(Header::new("etag", tag))
+ .streamed_body(file)
+ .ok()
+ }
+ }
+}