Skip to content

Commit 0405ee7

Browse files
committed
Fix #110: drop globals
This is a relatively chunky change replacing all globals with data in `AppState` along with some cleanups that occured naturally. Especially, all CSS can now be served with their SHA256 content hashsum to avoid browser cache issues. On the other hand, we dropped the possibility to nest the routes under the WASTEBIN_BASE_URL. This is better done by some dedicated proxy. The base URL is now only used to construct the QR link.
1 parent 713ef9d commit 0405ee7

File tree

20 files changed

+796
-710
lines changed

20 files changed

+796
-710
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99

1010
### Changed
1111

12+
- **Breaking**: From now on, `WASTEBIN_BASE_URL` is only used for the QR code
13+
link but not for internal routing. Use a dedicated proxy server to do that if
14+
necessary.
1215
- Use the [two-face](https://docs.rs/two-face) crate for an extended syntax
1316
list.
17+
- Serve all CSS assets under hashed URL to avoid caching issues.
1418

1519
### Fixes
1620

src/assets.rs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
use askama::Template;
2+
use axum::response::{IntoResponse, Response};
3+
use axum_extra::{headers, TypedHeader};
4+
use sha2::{Digest, Sha256};
5+
use std::io::Cursor;
6+
use std::time::Duration;
7+
use syntect::highlighting::{Color, ThemeSet};
8+
use syntect::html::{css_for_theme_with_class_style, ClassStyle};
9+
use two_face::theme::EmbeddedThemeName;
10+
11+
use crate::highlight::Theme;
12+
13+
/// An asset associated with a MIME type.
14+
#[derive(Clone)]
15+
pub struct Asset {
16+
/// Route that this will be served under.
17+
pub route: String,
18+
/// MIME type of this asset determined for the `ContentType` response header.
19+
mime: mime::Mime,
20+
/// Actual asset content.
21+
content: Vec<u8>,
22+
}
23+
24+
/// Asset kind.
25+
#[derive(Copy, Clone)]
26+
pub enum Kind {
27+
Css,
28+
Js,
29+
}
30+
31+
impl IntoResponse for Asset {
32+
fn into_response(self) -> Response {
33+
let content_type_header = headers::ContentType::from(self.mime);
34+
35+
let headers = (
36+
TypedHeader(content_type_header),
37+
TypedHeader(headers::CacheControl::new().with_max_age(Duration::from_secs(100))),
38+
);
39+
40+
(headers, self.content).into_response()
41+
}
42+
}
43+
44+
impl Asset {
45+
/// Construct new asset under the given `name`, `mime` type and `content`.
46+
pub fn new(name: &str, mime: mime::Mime, content: Vec<u8>) -> Self {
47+
Self {
48+
route: format!("/{name}"),
49+
mime,
50+
content,
51+
}
52+
}
53+
54+
/// Construct new hashed asset under the given `name`, `kind` and `content`.
55+
pub fn new_hashed(name: &str, kind: Kind, content: Vec<u8>) -> Self {
56+
let (mime, ext) = match kind {
57+
Kind::Css => (mime::TEXT_CSS, "css"),
58+
Kind::Js => (mime::TEXT_JAVASCRIPT, "js"),
59+
};
60+
61+
let route = format!(
62+
"/{name}.{}.{ext}",
63+
hex::encode(Sha256::digest(&content))
64+
.get(0..16)
65+
.expect("at least 16 characters")
66+
);
67+
68+
Self {
69+
route,
70+
mime,
71+
content,
72+
}
73+
}
74+
75+
pub fn route(&self) -> &str {
76+
&self.route
77+
}
78+
}
79+
80+
/// Collection of light and dark CSS and main UI style CSS derived from them.
81+
pub struct CssAssets {
82+
/// Main UI CSS stylesheet.
83+
pub style: Asset,
84+
/// Light theme colors.
85+
pub light: Asset,
86+
/// Dark theme colors.
87+
pub dark: Asset,
88+
}
89+
90+
trait ColorExt {
91+
/// Construct some color from the given RGBA components.
92+
fn new(r: u8, g: u8, b: u8, a: u8) -> Self;
93+
}
94+
95+
impl ColorExt for Color {
96+
fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
97+
Self { r, g, b, a }
98+
}
99+
}
100+
101+
impl CssAssets {
102+
/// Create CSS assets for `theme`.
103+
pub fn new(theme: Theme) -> Self {
104+
#[derive(Template)]
105+
#[template(path = "style.css", escape = "none")]
106+
struct StyleCss {
107+
light_background: Color,
108+
light_foreground: Color,
109+
dark_background: Color,
110+
dark_foreground: Color,
111+
light_asset: Asset,
112+
dark_asset: Asset,
113+
}
114+
115+
let light_theme = light_theme(theme);
116+
let dark_theme = dark_theme(theme);
117+
118+
let light_foreground = light_theme
119+
.settings
120+
.foreground
121+
.unwrap_or(Color::new(3, 3, 3, 100));
122+
123+
let light_background = light_theme
124+
.settings
125+
.background
126+
.unwrap_or(Color::new(250, 250, 250, 100));
127+
128+
let dark_foreground = dark_theme
129+
.settings
130+
.foreground
131+
.unwrap_or(Color::new(230, 225, 207, 100));
132+
133+
let dark_background = dark_theme
134+
.settings
135+
.background
136+
.unwrap_or(Color::new(15, 20, 25, 100));
137+
138+
let light = Asset::new_hashed(
139+
"light",
140+
Kind::Css,
141+
css_for_theme_with_class_style(&light_theme, ClassStyle::Spaced)
142+
.expect("generating CSS")
143+
.into_bytes(),
144+
);
145+
146+
let dark = Asset::new_hashed(
147+
"dark",
148+
Kind::Css,
149+
css_for_theme_with_class_style(&dark_theme, ClassStyle::Spaced)
150+
.expect("generating CSS")
151+
.into_bytes(),
152+
);
153+
154+
let style = StyleCss {
155+
light_background,
156+
light_foreground,
157+
dark_background,
158+
dark_foreground,
159+
light_asset: light.clone(),
160+
dark_asset: dark.clone(),
161+
};
162+
163+
let style = Asset::new_hashed(
164+
"style",
165+
Kind::Css,
166+
style.render().expect("rendering style css").into_bytes(),
167+
);
168+
169+
Self { style, light, dark }
170+
}
171+
}
172+
173+
fn light_theme(theme: Theme) -> syntect::highlighting::Theme {
174+
let theme_set = two_face::theme::extra();
175+
176+
match theme {
177+
Theme::Ayu => {
178+
let theme = include_str!("themes/ayu-light.tmTheme");
179+
ThemeSet::load_from_reader(&mut Cursor::new(theme)).expect("loading theme")
180+
}
181+
Theme::Base16Ocean => theme_set.get(EmbeddedThemeName::Base16OceanLight).clone(),
182+
Theme::Coldark => theme_set.get(EmbeddedThemeName::ColdarkCold).clone(),
183+
Theme::Gruvbox => theme_set.get(EmbeddedThemeName::GruvboxLight).clone(),
184+
Theme::Monokai => theme_set
185+
.get(EmbeddedThemeName::MonokaiExtendedLight)
186+
.clone(),
187+
Theme::Onehalf => theme_set.get(EmbeddedThemeName::OneHalfLight).clone(),
188+
Theme::Solarized => theme_set.get(EmbeddedThemeName::SolarizedLight).clone(),
189+
}
190+
}
191+
192+
fn dark_theme(theme: Theme) -> syntect::highlighting::Theme {
193+
let theme_set = two_face::theme::extra();
194+
195+
match theme {
196+
Theme::Ayu => {
197+
let theme = include_str!("themes/ayu-dark.tmTheme");
198+
ThemeSet::load_from_reader(&mut Cursor::new(theme)).expect("loading theme")
199+
}
200+
Theme::Base16Ocean => theme_set.get(EmbeddedThemeName::Base16OceanDark).clone(),
201+
Theme::Coldark => theme_set.get(EmbeddedThemeName::ColdarkDark).clone(),
202+
Theme::Gruvbox => theme_set.get(EmbeddedThemeName::GruvboxDark).clone(),
203+
Theme::Monokai => theme_set.get(EmbeddedThemeName::MonokaiExtended).clone(),
204+
Theme::Onehalf => theme_set.get(EmbeddedThemeName::OneHalfDark).clone(),
205+
Theme::Solarized => theme_set.get(EmbeddedThemeName::SolarizedDark).clone(),
206+
}
207+
}
208+
209+
#[cfg(test)]
210+
mod tests {
211+
use super::*;
212+
213+
#[test]
214+
fn hashed_asset() {
215+
let asset = Asset::new_hashed("style", Kind::Css, String::from("body {}").into_bytes());
216+
assert_eq!(asset.route, "/style.62368a1a29259b30.css");
217+
218+
let asset = Asset::new_hashed("main", Kind::Js, String::from("1 + 1").into_bytes());
219+
assert_eq!(asset.route, "/main.72fce59447a01f48.js");
220+
}
221+
}

src/env.rs

Lines changed: 15 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,8 @@ use std::env::VarError;
44
use std::net::SocketAddr;
55
use std::num::{NonZero, NonZeroU32, NonZeroUsize, ParseIntError};
66
use std::path::PathBuf;
7-
use std::sync::LazyLock;
87
use std::time::Duration;
98

10-
pub struct Metadata<'a> {
11-
pub title: String,
12-
pub version: &'a str,
13-
pub highlight: &'a highlight::Data<'a>,
14-
}
15-
169
pub const DEFAULT_HTTP_TIMEOUT: Duration = Duration::from_secs(5);
1710

1811
const VAR_ADDRESS_PORT: &str = "WASTEBIN_ADDRESS_PORT";
@@ -44,87 +37,29 @@ pub enum Error {
4437
HttpTimeout(ParseIntError),
4538
#[error("failed to parse {VAR_MAX_PASTE_EXPIRATION}: {0}")]
4639
MaxPasteExpiration(ParseIntError),
40+
#[error("unknown theme {0}")]
41+
UnknownTheme(String),
4742
}
4843

49-
pub struct BasePath(String);
50-
51-
impl BasePath {
52-
pub fn path(&self) -> &str {
53-
&self.0
54-
}
55-
56-
pub fn join(&self, s: &str) -> String {
57-
let b = &self.0;
58-
format!("{b}{s}")
59-
}
60-
}
61-
62-
impl Default for BasePath {
63-
fn default() -> Self {
64-
BasePath("/".to_string())
65-
}
44+
pub fn title() -> String {
45+
std::env::var("WASTEBIN_TITLE").unwrap_or_else(|_| "wastebin".to_string())
6646
}
6747

68-
pub static METADATA: LazyLock<Metadata> = LazyLock::new(|| {
69-
let title = std::env::var("WASTEBIN_TITLE").unwrap_or_else(|_| "wastebin".to_string());
70-
let version = env!("CARGO_PKG_VERSION");
71-
let highlight = &highlight::DATA;
72-
73-
Metadata {
74-
title,
75-
version,
76-
highlight,
77-
}
78-
});
79-
80-
// NOTE: This relies on `VAR_BASE_URL` but repeats parsing to handle errors.
81-
pub static BASE_PATH: LazyLock<BasePath> = LazyLock::new(|| {
82-
std::env::var(VAR_BASE_URL).map_or_else(
83-
|err| {
84-
match err {
85-
VarError::NotPresent => (),
86-
VarError::NotUnicode(_) => {
87-
tracing::warn!("`VAR_BASE_URL` not Unicode, defaulting to '/'");
88-
}
89-
};
90-
BasePath::default()
91-
},
92-
|var| match url::Url::parse(&var) {
93-
Ok(url) => {
94-
let path = url.path();
95-
96-
if path.ends_with('/') {
97-
BasePath(path.to_string())
98-
} else {
99-
BasePath(format!("{path}/"))
100-
}
101-
}
102-
Err(err) => {
103-
tracing::error!("error parsing `VAR_BASE_URL`, defaulting to '/': {err}");
104-
BasePath::default()
105-
}
106-
},
107-
)
108-
});
109-
110-
pub static THEME: LazyLock<highlight::Theme> = LazyLock::new(|| {
48+
pub fn theme() -> Result<highlight::Theme, Error> {
11149
std::env::var(VAR_THEME).map_or_else(
112-
|_| highlight::Theme::Ayu,
50+
|_| Ok(highlight::Theme::Ayu),
11351
|var| match var.as_str() {
114-
"ayu" => highlight::Theme::Ayu,
115-
"base16ocean" => highlight::Theme::Base16Ocean,
116-
"coldark" => highlight::Theme::Coldark,
117-
"gruvbox" => highlight::Theme::Gruvbox,
118-
"monokai" => highlight::Theme::Monokai,
119-
"onehalf" => highlight::Theme::Onehalf,
120-
"solarized" => highlight::Theme::Solarized,
121-
_ => {
122-
tracing::error!("unrecognized theme {var}");
123-
highlight::Theme::Ayu
124-
}
52+
"ayu" => Ok(highlight::Theme::Ayu),
53+
"base16ocean" => Ok(highlight::Theme::Base16Ocean),
54+
"coldark" => Ok(highlight::Theme::Coldark),
55+
"gruvbox" => Ok(highlight::Theme::Gruvbox),
56+
"monokai" => Ok(highlight::Theme::Monokai),
57+
"onehalf" => Ok(highlight::Theme::Onehalf),
58+
"solarized" => Ok(highlight::Theme::Solarized),
59+
_ => Err(Error::UnknownTheme(var)),
12560
},
12661
)
127-
});
62+
}
12863

12964
pub fn cache_size() -> Result<NonZeroUsize, Error> {
13065
std::env::var(VAR_CACHE_SIZE)

0 commit comments

Comments
 (0)