Skip to content

Commit cbacc6c

Browse files
committed
refactor: turn asset cache into tower service
1 parent 9a20441 commit cbacc6c

File tree

8 files changed

+191
-119
lines changed

8 files changed

+191
-119
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,11 @@ snafu = { version = "0.8.5", features = ["rust_1_81"] }
8989
time = "0.3.36"
9090
tokio = { version = "1.42.0", features = ["macros"] }
9191
tokio-stream = "*"
92+
tower = "*"
9293
tower-cookies = "*"
9394
tower-http = "0.5"
9495
tower-sessions = "0.14.0"
96+
tower-service = "0.3.3"
9597
tracing = "0.1.41"
9698
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
9799
z32 = "1.1.1"

crates/rostra-web-ui/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ rostra-util = { workspace = true }
3131
rostra-util-error = { workspace = true }
3232
tokio = { workspace = true }
3333
tokio-stream = { workspace = true, features = ["fs"] }
34+
tower = { workspace = true }
3435
tower-cookies = { workspace = true }
36+
tower-service = { workspace = true }
3537
tower-http = { workspace = true, features = ["cors", "compression-br", "fs"] }
3638
tower-sessions = { workspace = true }
3739
tracing = { workspace = true }

crates/rostra-web-ui/src/asset_cache.rs

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use std::path::{self, PathBuf};
77
use std::string::String;
88
use std::sync::LazyLock;
99

10-
use axum::extract::Path;
1110
use bytes::Bytes;
1211
use futures::stream::{BoxStream, StreamExt};
1312
use rostra_util_error::WhateverResult;
@@ -17,39 +16,14 @@ use tracing::{debug, info};
1716

1817
use crate::LOG_TARGET;
1918

20-
const HASH_SPLIT_CHAR: char = '.';
21-
22-
/// Maps static asset filenames to their compressed bytes and content type. This
19+
/// Pre-loaded and pre-compressed static assets. This
2320
/// is used to serve static assets from the build directory without reading from
2421
/// disk, as the cache stays in RAM for the life of the server.
25-
///
26-
/// This type should be accessed via the `cache` property in `AppState`.
2722
#[derive(Debug)]
28-
pub struct AssetCache(HashMap<String, StaticAsset>);
29-
30-
impl AssetCache {
31-
/// Attempts to return a static asset from the cache from a cache key. If
32-
/// the asset is not found, `None` is returned.
33-
pub fn get(&self, key: &str) -> Option<&StaticAsset> {
34-
self.0.get(key)
35-
}
36-
37-
/// Helper method to get a static asset from an extracted request path.
38-
pub fn get_from_path(&self, path: &Path<String>) -> Option<&StaticAsset> {
39-
let key = Self::get_cache_key(path);
40-
self.get(&key)
41-
}
42-
43-
fn get_cache_key(path: &str) -> String {
44-
let mut parts = path.split(['.', HASH_SPLIT_CHAR]);
23+
pub struct StaticAssets(HashMap<String, StaticAsset>);
4524

46-
let basename = parts.next().unwrap_or_default();
47-
let ext = parts.next_back().unwrap_or_default();
48-
49-
format!("{}.{}", basename, ext)
50-
}
51-
52-
pub async fn load_files(root_dir: &path::Path) -> WhateverResult<Self> {
25+
impl StaticAssets {
26+
pub async fn load(root_dir: &path::Path) -> WhateverResult<Self> {
5327
info!(target: LOG_TARGET, dir=%root_dir.display(), "Loading assets");
5428
let mut cache = HashMap::default();
5529

@@ -111,7 +85,7 @@ impl AssetCache {
11185
},
11286
)))
11387
})
114-
.buffered(16)
88+
.buffered(32)
11589
.filter_map(
11690
|res_opt: WhateverResult<std::option::Option<(String, StaticAsset)>>| {
11791
ready(res_opt.transpose())
@@ -122,7 +96,7 @@ impl AssetCache {
12296

12397
for asset_res in assets {
12498
let (filename, asset) = asset_res?;
125-
cache.insert(Self::get_cache_key(&filename), asset);
99+
cache.insert(filename, asset);
126100
}
127101

128102
for (key, asset) in &cache {
@@ -132,6 +106,12 @@ impl AssetCache {
132106

133107
Ok(Self(cache))
134108
}
109+
110+
/// Attempts to return a static asset from the cache from a cache key. If
111+
/// the asset is not found, `None` is returned.
112+
pub fn get(&self, key: &str) -> Option<&StaticAsset> {
113+
self.0.get(key)
114+
}
135115
}
136116

137117
/// Represents a single static asset from the build directory. Assets are
@@ -197,8 +177,6 @@ fn calculate_etag(data: &[u8]) -> String {
197177
format!("\"{}\"", hasher.finish())
198178
}
199179

200-
// async fn read_dir_stream(dir: impl AsRef<path::Path>) -> impl Stream<Item =
201-
// io::Result<PathBuf>> {
202180
fn read_dir_stream(dir: PathBuf) -> BoxStream<'static, io::Result<PathBuf>> {
203181
async_stream::try_stream! {
204182
let entries = ReadDirStream::new(
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use std::sync::Arc;
2+
use std::task::{Context, Poll};
3+
4+
use axum::body::Body;
5+
use axum::http::header::{ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_TYPE};
6+
use axum::http::{HeaderMap, HeaderValue, Request, Response, StatusCode};
7+
use axum::response::IntoResponse;
8+
use futures::future::BoxFuture;
9+
use tower_service::Service;
10+
11+
use crate::asset_cache::StaticAssets;
12+
13+
#[derive(Clone)]
14+
pub struct StaticAssetService {
15+
assets: Arc<StaticAssets>,
16+
}
17+
18+
impl StaticAssetService {
19+
pub fn new(assets: Arc<StaticAssets>) -> Self {
20+
Self { assets }
21+
}
22+
23+
fn handle_request(&self, req: Request<Body>) -> Response<Body> {
24+
let path = req.uri().path().trim_start_matches('/');
25+
let Some(asset) = self.assets.get(path) else {
26+
dbg!("NOT FOUND", path);
27+
return (StatusCode::NOT_FOUND, Body::empty()).into_response();
28+
};
29+
30+
let req_headers = req.headers();
31+
let mut resp_headers = HeaderMap::new();
32+
33+
// Set content type
34+
resp_headers.insert(
35+
CONTENT_TYPE,
36+
HeaderValue::from_static(asset.content_type().unwrap_or("application/octet-stream")),
37+
);
38+
39+
// Handle ETag and conditional request
40+
let etag = asset.etag.clone();
41+
if let Some(response) = crate::handle_etag(req_headers, &etag, &mut resp_headers) {
42+
return response;
43+
}
44+
45+
let accepts_brotli = req_headers
46+
.get_all(ACCEPT_ENCODING)
47+
.into_iter()
48+
.any(|encodings| {
49+
let Ok(str) = encodings.to_str() else {
50+
return false;
51+
};
52+
53+
str.split(',').any(|s| s.trim() == "br")
54+
});
55+
56+
let content = match (accepts_brotli, asset.compressed.as_ref()) {
57+
(true, Some(compressed)) => {
58+
resp_headers.insert(CONTENT_ENCODING, HeaderValue::from_static("br"));
59+
compressed.clone()
60+
}
61+
_ => asset.raw.clone(),
62+
};
63+
64+
(resp_headers, content).into_response()
65+
}
66+
}
67+
68+
impl<B> Service<Request<B>> for StaticAssetService
69+
where
70+
B: Send + 'static,
71+
{
72+
type Response = Response<Body>;
73+
type Error = std::convert::Infallible;
74+
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
75+
76+
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), std::convert::Infallible>> {
77+
Poll::Ready(Ok(()))
78+
}
79+
80+
fn call(
81+
&mut self,
82+
req: Request<B>,
83+
) -> BoxFuture<'static, Result<Response<Body>, std::convert::Infallible>> {
84+
let service = self.clone();
85+
let uri = req.uri().clone();
86+
87+
Box::pin(async move {
88+
// Convert to a Request<Body> by extracting just the URI
89+
let new_req = Request::builder().uri(uri).body(Body::empty()).unwrap();
90+
91+
Ok(service.handle_request(new_req))
92+
})
93+
}
94+
}

crates/rostra-web-ui/src/lib.rs

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub(crate) mod asset_cache;
2+
mod asset_service;
23
mod error;
34
mod fragment;
45
pub mod html_utils;
@@ -14,10 +15,9 @@ use std::sync::Arc;
1415
use std::time::Duration;
1516
use std::{io, result};
1617

17-
use asset_cache::AssetCache;
18+
use asset_cache::StaticAssets;
1819
use axum::http::header::{ACCEPT, CONTENT_TYPE};
1920
use axum::http::{HeaderName, HeaderValue, Method};
20-
use axum::routing::get;
2121
use axum::{Router, middleware};
2222
use error::{IdMismatchSnafu, UnlockError, UnlockResult};
2323
use listenfd::ListenFd;
@@ -27,7 +27,7 @@ use rostra_client::{ClientHandle, ClientRefError};
2727
use rostra_core::id::{RostraId, RostraIdSecretKey};
2828
use rostra_util::is_rostra_dev_mode_set;
2929
use rostra_util_error::WhateverResult;
30-
use routes::{cache_control, get_static_asset};
30+
use routes::cache_control;
3131
use snafu::{ResultExt as _, Snafu, Whatever, ensure};
3232
use tokio::net::{TcpListener, TcpSocket};
3333
use tokio::signal;
@@ -136,7 +136,7 @@ pub type UiStateClientResult<T> = result::Result<T, UiStateClientError>;
136136

137137
pub struct UiState {
138138
clients: MultiClient,
139-
assets: Option<Arc<AssetCache>>,
139+
assets: Option<Arc<StaticAssets>>,
140140
default_profile: Option<RostraId>,
141141
}
142142

@@ -172,7 +172,7 @@ pub struct Server {
172172
listener: TcpListener,
173173

174174
state: SharedState,
175-
assets: Option<Arc<AssetCache>>,
175+
assets: Option<Arc<StaticAssets>>,
176176
opts: Opts,
177177
}
178178

@@ -219,7 +219,7 @@ impl Server {
219219
None
220220
} else {
221221
Some(Arc::new(
222-
AssetCache::load_files(&opts.assets_dir)
222+
StaticAssets::load(&opts.assets_dir)
223223
.await
224224
.context(AssetsLoadSnafu)?,
225225
))
@@ -275,22 +275,15 @@ impl Server {
275275
);
276276
let mut router = Router::new().merge(routes::route_handler(self.state.clone()));
277277

278-
match self.assets {
279-
Some(_assets) => {
280-
router = router.nest("/assets", {
281-
let state = self.state.clone();
282-
Router::new()
283-
.route("/{*file}", get(get_static_asset))
284-
.with_state(state)
285-
});
278+
router = match self.assets.clone() {
279+
Some(assets) => {
280+
router.nest_service("/assets", asset_service::StaticAssetService::new(assets))
286281
}
287-
_ => {
288-
router = router.nest_service(
289-
"/assets",
290-
ServeDir::new(format!("{}/assets", env!("CARGO_MANIFEST_DIR"))),
291-
);
292-
}
293-
}
282+
_ => router.nest_service(
283+
"/assets",
284+
ServeDir::new(format!("{}/assets", env!("CARGO_MANIFEST_DIR"))),
285+
),
286+
};
294287

295288
let session_store = MemoryStore::default();
296289
let session_layer = SessionManagerLayer::new(session_store)

0 commit comments

Comments
 (0)