1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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()
}
}
}
|