/* 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 */ use account::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 style; #[get("/")] pub async fn r_index(sess: Option) -> MyResult>> { 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() })) } } 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.as_bytes().len(), Cursor::new(out)) .ok() } } pub struct Defer(Pin + 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> { 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 { 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() } } }