itsi-scheduler 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 683d36dfdd824a4d0640252867beb3875fbe0406f29a421bdec7fb401f62a554
4
- data.tar.gz: 822b0a8bf6b0f03308be76d35b738f2a39ea977c9e7de43e7c432a029540598b
3
+ metadata.gz: 0c574c681706b2c5e414d9a976c82cc56bc979e5cec2e24f76bd4f4696e84632
4
+ data.tar.gz: 14857bd2404e1b8cc5d0de66781d553b3c88d74d010b1d4300b7e5ff84e0bd53
5
5
  SHA512:
6
- metadata.gz: 70b32ef89c42624b44c5ad0dce35af94c3d685f13717e36b7897f422588a8254d20199d99502bcedf0bf332d4ebe8c1f567ec6bb0fc199b1adf9018aff2db8e3
7
- data.tar.gz: 7c1b34f2bd362e310571e758fba867248dee214ad28d01a38872186b5f471258cf471bce4d07715431d8a164b14bb1580e162af5737e396d1d0509af9b007748
6
+ metadata.gz: 9678cef317ffd880ea2d6b7acf46819dca15003fc025b4b971812f14fee2afdd3dff073ae3ada55ebd70a7a11bb05a62e6e7e9d1ff70485fa4f20bf06093a6d0
7
+ data.tar.gz: 3a433e4c5177d54a00c05d05456ce74b7f4aa2c1bb6261bf9873d63d8b97180a7edc2a121d38642c04642ca3550a863927f9e2ba08c3c4c00aba1dd1837feaf9
data/Cargo.lock CHANGED
@@ -213,7 +213,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
213
213
 
214
214
  [[package]]
215
215
  name = "itsi-scheduler"
216
- version = "0.2.3"
216
+ version = "0.2.4"
217
217
  dependencies = [
218
218
  "bytes",
219
219
  "derive_more",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "itsi-scheduler"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  edition = "2021"
5
5
  authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "itsi-server"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  edition = "2021"
5
5
  authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
6
  license = "MIT"
@@ -1,5 +1,6 @@
1
1
  use crate::{
2
- server::http_message_types::HttpResponse, services::itsi_http_service::HttpRequestContext,
2
+ server::http_message_types::{HttpRequest, HttpResponse},
3
+ services::itsi_http_service::HttpRequestContext,
3
4
  };
4
5
 
5
6
  use super::{
@@ -13,6 +14,7 @@ use async_compression::{
13
14
  };
14
15
  use async_trait::async_trait;
15
16
  use bytes::{Bytes, BytesMut};
17
+ use either::Either;
16
18
  use futures::TryStreamExt;
17
19
  use http::{
18
20
  header::{GetAll, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE},
@@ -20,6 +22,7 @@ use http::{
20
22
  };
21
23
  use http_body_util::{combinators::BoxBody, BodyExt, Full, StreamBody};
22
24
  use hyper::body::{Body, Frame};
25
+ use magnus::error::Result;
23
26
  use serde::{Deserialize, Serialize};
24
27
  use std::convert::Infallible;
25
28
  use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
@@ -151,6 +154,15 @@ fn update_content_encoding(parts: &mut http::response::Parts, new_encoding: Head
151
154
 
152
155
  #[async_trait]
153
156
  impl MiddlewareLayer for Compression {
157
+ async fn before(
158
+ &self,
159
+ req: HttpRequest,
160
+ context: &mut HttpRequestContext,
161
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
162
+ context.set_supported_encoding_set(&req);
163
+ Ok(Either::Left(req))
164
+ }
165
+
154
166
  /// We'll apply compression on the response, where appropriate.
155
167
  /// This is if:
156
168
  /// * The response body is larger than the minimum size.
@@ -207,16 +219,21 @@ impl MiddlewareLayer for Compression {
207
219
  }
208
220
  }
209
221
 
210
- let compression_method = match find_first_supported(
211
- &context.supported_encoding_set,
212
- self.algorithms.iter().map(|algo| algo.as_str()),
213
- ) {
214
- Some("gzip") => CompressionAlgorithm::Gzip,
215
- Some("br") => CompressionAlgorithm::Brotli,
216
- Some("deflate") => CompressionAlgorithm::Deflate,
217
- Some("zstd") => CompressionAlgorithm::Zstd,
218
- _ => CompressionAlgorithm::Identity,
219
- };
222
+ let compression_method =
223
+ if let Some(supported_encoding_set) = context.supported_encoding_set() {
224
+ match find_first_supported(
225
+ supported_encoding_set,
226
+ self.algorithms.iter().map(|algo| algo.as_str()),
227
+ ) {
228
+ Some("gzip") => CompressionAlgorithm::Gzip,
229
+ Some("br") => CompressionAlgorithm::Brotli,
230
+ Some("deflate") => CompressionAlgorithm::Deflate,
231
+ Some("zstd") => CompressionAlgorithm::Zstd,
232
+ _ => CompressionAlgorithm::Identity,
233
+ }
234
+ } else {
235
+ CompressionAlgorithm::Identity
236
+ };
220
237
 
221
238
  debug!(
222
239
  target: "middleware::compress",
@@ -62,7 +62,7 @@ impl MiddlewareLayer for LogRequests {
62
62
  req: HttpRequest,
63
63
  context: &mut HttpRequestContext,
64
64
  ) -> Result<Either<HttpRequest, HttpResponse>> {
65
- context.track_start_time();
65
+ context.init_logging_params();
66
66
  if let Some(LogConfig { level, format }) = self.before.as_ref() {
67
67
  level.log(format.rewrite_request(&req, context));
68
68
  }
@@ -33,7 +33,7 @@ use reqwest::{
33
33
  Body, Client, Url,
34
34
  };
35
35
  use serde::Deserialize;
36
- use tracing::{debug, info};
36
+ use tracing::debug;
37
37
 
38
38
  #[derive(Debug, Clone, Deserialize)]
39
39
  pub struct Proxy {
@@ -324,7 +324,6 @@ impl MiddlewareLayer for Proxy {
324
324
  .unwrap_or("")
325
325
  });
326
326
 
327
- info!("Extracted host str is {}", host_str);
328
327
  let req_info = RequestInfo {
329
328
  method: req.method().clone(),
330
329
  headers: req_headers.clone(),
@@ -12,10 +12,11 @@ use async_trait::async_trait;
12
12
  use either::Either;
13
13
  use http::{
14
14
  header::{IF_MODIFIED_SINCE, RANGE},
15
- HeaderMap, Method,
15
+ HeaderMap, HeaderValue, Method,
16
16
  };
17
17
  use itsi_error::ItsiError;
18
18
  use magnus::error::Result;
19
+ use moka::sync::Cache;
19
20
  use regex::Regex;
20
21
  use serde::Deserialize;
21
22
  use std::{collections::HashMap, path::PathBuf, sync::OnceLock, time::Duration};
@@ -75,6 +76,10 @@ impl MiddlewareLayer for StaticAssets {
75
76
  recheck_interval: Duration::from_secs(self.file_check_interval),
76
77
  serve_hidden_files: self.serve_hidden_files,
77
78
  allowed_extensions: self.allowed_extensions.clone(),
79
+ miss_cache: Cache::builder()
80
+ .max_capacity(self.max_files_in_memory)
81
+ .time_to_live(Duration::from_secs(self.file_check_interval))
82
+ .build(),
78
83
  })?)
79
84
  .map_err(ItsiError::new)?;
80
85
  Ok(())
@@ -90,6 +95,8 @@ impl MiddlewareLayer for StaticAssets {
90
95
  debug!(target: "middleware::static_assets", "Refusing to handle non-GET/HEAD request");
91
96
  return Ok(Either::Left(req));
92
97
  }
98
+
99
+ context.set_supported_encoding_set(&req);
93
100
  let abs_path = req.uri().path();
94
101
  let rel_path = if !self.relative_path {
95
102
  abs_path.trim_start_matches("/")
@@ -123,6 +130,10 @@ impl MiddlewareLayer for StaticAssets {
123
130
 
124
131
  // Let the file server handle everything
125
132
  let file_server = self.file_server.get().unwrap();
133
+ let encodings: &[HeaderValue] = context
134
+ .supported_encoding_set()
135
+ .map(Vec::as_slice)
136
+ .unwrap_or(&[] as &[HeaderValue]);
126
137
  let response = file_server
127
138
  .serve(
128
139
  &req,
@@ -131,9 +142,10 @@ impl MiddlewareLayer for StaticAssets {
131
142
  serve_range,
132
143
  if_modified_since,
133
144
  is_head_request,
134
- &context.supported_encoding_set,
145
+ encodings,
135
146
  )
136
147
  .await;
148
+
137
149
  if response.is_none() {
138
150
  Ok(Either::Left(req))
139
151
  } else {
@@ -50,6 +50,35 @@ pub fn parse_template(template: &str) -> Vec<Segment> {
50
50
  }
51
51
 
52
52
  impl StringRewrite {
53
+ /// Apply a single modifier of the form `op:arg` (or for replace `op:from,to`)
54
+ #[inline]
55
+ fn apply_modifier(s: &mut String, mod_str: &str) {
56
+ if let Some((op, arg)) = mod_str.split_once(':') {
57
+ match op {
58
+ "strip_prefix" => {
59
+ if s.starts_with(arg) {
60
+ let _ = s.drain(..arg.len());
61
+ }
62
+ }
63
+ "strip_suffix" => {
64
+ if s.ends_with(arg) {
65
+ let len = s.len();
66
+ let start = len.saturating_sub(arg.len());
67
+ let _ = s.drain(start..);
68
+ }
69
+ }
70
+ "replace" => {
71
+ if let Some((from, to)) = arg.split_once(',') {
72
+ if s.contains(from) {
73
+ *s = s.replace(from, to);
74
+ }
75
+ }
76
+ }
77
+ _ => {}
78
+ }
79
+ }
80
+ }
81
+
53
82
  pub fn rewrite_request(&self, req: &HttpRequest, context: &HttpRequestContext) -> String {
54
83
  let segments = self
55
84
  .segments
@@ -63,9 +92,17 @@ impl StringRewrite {
63
92
 
64
93
  for segment in segments {
65
94
  match segment {
66
- Segment::Literal(text) => result.push_str(text),
67
- Segment::Placeholder(placeholder) => {
68
- let replacement = match placeholder.as_str() {
95
+ Segment::Literal(text) => {
96
+ result.push_str(text);
97
+ }
98
+ Segment::Placeholder(raw) => {
99
+ // split into key and optional modifier
100
+ let mut parts = raw.split('|');
101
+ let key = parts.next().unwrap();
102
+ let modifiers = parts; // zero o
103
+
104
+ // 1) lookup the raw replacement
105
+ let mut replacement = match key {
69
106
  "request_id" => context.short_request_id(),
70
107
  "request_id_full" => context.request_id(),
71
108
  "method" => req.method().as_str().to_string(),
@@ -76,13 +113,13 @@ impl StringRewrite {
76
113
  .uri()
77
114
  .path_and_query()
78
115
  .map(|pq| pq.to_string())
79
- .unwrap_or("".to_string()),
116
+ .unwrap_or_default(),
80
117
  "query" => {
81
- let query = req.uri().query().unwrap_or("").to_string();
82
- if query.is_empty() {
83
- query
118
+ let q = req.uri().query().unwrap_or("");
119
+ if q.is_empty() {
120
+ "".to_string()
84
121
  } else {
85
- format!("?{}", query)
122
+ format!("?{}", q)
86
123
  }
87
124
  }
88
125
  "port" => req
@@ -91,31 +128,34 @@ impl StringRewrite {
91
128
  .map(|p| p.to_string())
92
129
  .unwrap_or_else(|| "80".to_string()),
93
130
  "start_time" => {
94
- if let Some(start_time) = context.start_time() {
95
- start_time.format("%Y-%m-%d:%H:%M:%S:%3f").to_string()
131
+ if let Some(ts) = context.start_time() {
132
+ ts.format("%Y-%m-%d:%H:%M:%S:%3f").to_string()
96
133
  } else {
97
134
  "N/A".to_string()
98
135
  }
99
136
  }
100
137
  other => {
101
- if let Some(header_val) = req.headers().get(other) {
102
- if let Ok(s) = header_val.to_str() {
103
- s.to_string()
104
- } else {
105
- "".to_string()
106
- }
107
- } else if let Some(caps) = &captures {
108
- if let Some(m) = caps.name(other) {
109
- m.as_str().to_string()
110
- } else {
111
- // Fallback: leave the placeholder as is.
112
- format!("{{{}}}", other)
113
- }
114
- } else {
138
+ // headers first
139
+ if let Some(hv) = req.headers().get(other) {
140
+ hv.to_str().unwrap_or("").to_string()
141
+ }
142
+ // then any regex‐capture
143
+ else if let Some(caps) = &captures {
144
+ caps.name(other)
145
+ .map(|m| m.as_str().to_string())
146
+ .unwrap_or_else(|| format!("{{{}}}", other))
147
+ }
148
+ // fallback: leave placeholder intact
149
+ else {
115
150
  format!("{{{}}}", other)
116
151
  }
117
152
  }
118
153
  };
154
+
155
+ for m in modifiers {
156
+ Self::apply_modifier(&mut replacement, m);
157
+ }
158
+
119
159
  result.push_str(&replacement);
120
160
  }
121
161
  }
@@ -132,37 +172,42 @@ impl StringRewrite {
132
172
  let mut result = String::with_capacity(self.template_string.len());
133
173
  for segment in segments {
134
174
  match segment {
135
- Segment::Literal(text) => result.push_str(text),
136
- Segment::Placeholder(placeholder) => {
137
- let replacement = match placeholder.as_str() {
175
+ Segment::Literal(text) => {
176
+ result.push_str(text);
177
+ }
178
+ Segment::Placeholder(raw) => {
179
+ let mut parts = raw.split('|');
180
+ let key = parts.next().unwrap();
181
+ let modifiers = parts; // zero o
182
+
183
+ let mut replacement = match key {
138
184
  "request_id" => context.short_request_id(),
139
185
  "request_id_full" => context.request_id(),
140
186
  "status" => resp.status().as_str().to_string(),
141
187
  "addr" => context.addr.to_owned(),
142
188
  "response_time" => {
143
- if let Some(response_time) = context.get_response_time() {
144
- if let Some(microseconds) = response_time.num_microseconds() {
145
- format!("{:.3}ms", microseconds as f64 / 1000.0)
146
- } else {
147
- format!("{}ms", response_time.num_milliseconds())
148
- }
189
+ let dur = context.get_response_time();
190
+ let micros = dur.as_micros();
191
+ if micros < 1_000 {
192
+ format!("{}µs", micros)
149
193
  } else {
150
- "-".to_string()
194
+ let ms = dur.as_secs_f64() * 1_000.0;
195
+ format!("{:.3}ms", ms)
151
196
  }
152
197
  }
153
198
  other => {
154
- // Try pulling from response headers first
155
- if let Some(val) = resp.headers().get(other) {
156
- if let Ok(s) = val.to_str() {
157
- s.to_string()
158
- } else {
159
- "".to_string()
160
- }
199
+ if let Some(hv) = resp.headers().get(other) {
200
+ hv.to_str().unwrap_or("").to_string()
161
201
  } else {
162
202
  format!("{{{}}}", other)
163
203
  }
164
204
  }
165
205
  };
206
+
207
+ for m in modifiers {
208
+ Self::apply_modifier(&mut replacement, m);
209
+ }
210
+
166
211
  result.push_str(&replacement);
167
212
  }
168
213
  }
@@ -1,14 +1,15 @@
1
1
  use crate::default_responses::{NOT_FOUND_RESPONSE, TIMEOUT_RESPONSE};
2
2
  use crate::ruby_types::itsi_server::itsi_server_config::{ItsiServerTokenPreference, ServerParams};
3
3
  use crate::server::binds::listener::ListenerInfo;
4
- use crate::server::http_message_types::{ConversionExt, HttpResponse, RequestExt, ResponseFormat};
4
+ use crate::server::http_message_types::{
5
+ ConversionExt, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
6
+ };
5
7
  use crate::server::lifecycle_event::LifecycleEvent;
6
8
  use crate::server::middleware_stack::MiddlewareLayer;
7
9
  use crate::server::request_job::RequestJob;
8
10
  use crate::server::serve_strategy::single_mode::RunningPhase;
9
11
  use crate::server::signal::send_lifecycle_event;
10
- use chrono;
11
- use chrono::Local;
12
+ use chrono::{self, DateTime, Local};
12
13
  use either::Either;
13
14
  use http::header::ACCEPT_ENCODING;
14
15
  use http::{HeaderValue, Request};
@@ -18,6 +19,7 @@ use itsi_error::ItsiError;
18
19
  use regex::Regex;
19
20
  use std::sync::atomic::{AtomicBool, Ordering};
20
21
  use std::sync::OnceLock;
22
+ use std::time::{Duration, Instant};
21
23
  use tracing::error;
22
24
 
23
25
  use std::{future::Future, ops::Deref, pin::Pin, sync::Arc};
@@ -68,17 +70,16 @@ impl Deref for RequestContextInner {
68
70
  }
69
71
 
70
72
  pub struct RequestContextInner {
71
- pub request_id: u128,
73
+ pub request_id: u64,
72
74
  pub service: ItsiHttpService,
73
75
  pub accept: ResponseFormat,
74
76
  pub matching_pattern: Option<Arc<Regex>>,
75
77
  pub origin: OnceLock<Option<String>>,
76
78
  pub response_format: OnceLock<ResponseFormat>,
77
- pub start_time: chrono::DateTime<chrono::Utc>,
78
- pub request: Option<Arc<Request<Incoming>>>,
79
- pub request_start_time: OnceLock<chrono::DateTime<Local>>,
79
+ pub request_start_time: OnceLock<DateTime<Local>>,
80
+ pub start_instant: Instant,
80
81
  pub if_none_match: OnceLock<Option<String>>,
81
- pub supported_encoding_set: Vec<HeaderValue>,
82
+ pub supported_encoding_set: OnceLock<Vec<HeaderValue>>,
82
83
  pub is_ruby_request: Arc<AtomicBool>,
83
84
  }
84
85
 
@@ -87,27 +88,38 @@ impl HttpRequestContext {
87
88
  service: ItsiHttpService,
88
89
  matching_pattern: Option<Arc<Regex>>,
89
90
  accept: ResponseFormat,
90
- supported_encoding_set: Vec<HeaderValue>,
91
91
  is_ruby_request: Arc<AtomicBool>,
92
92
  ) -> Self {
93
93
  HttpRequestContext {
94
94
  inner: Arc::new(RequestContextInner {
95
- request_id: rand::random::<u128>(),
95
+ request_id: rand::random::<u64>(),
96
96
  service,
97
97
  matching_pattern,
98
98
  accept,
99
99
  origin: OnceLock::new(),
100
100
  response_format: OnceLock::new(),
101
- start_time: chrono::Utc::now(),
102
- request: None,
103
101
  request_start_time: OnceLock::new(),
102
+ start_instant: Instant::now(),
104
103
  if_none_match: OnceLock::new(),
105
- supported_encoding_set,
104
+ supported_encoding_set: OnceLock::new(),
106
105
  is_ruby_request,
107
106
  }),
108
107
  }
109
108
  }
110
109
 
110
+ pub fn set_supported_encoding_set(&self, req: &HttpRequest) {
111
+ let supported_encoding_set = req
112
+ .headers()
113
+ .get_all(ACCEPT_ENCODING)
114
+ .into_iter()
115
+ .cloned()
116
+ .collect::<Vec<_>>();
117
+ self.inner
118
+ .supported_encoding_set
119
+ .set(supported_encoding_set)
120
+ .unwrap();
121
+ }
122
+
111
123
  pub fn set_origin(&self, origin: Option<String>) {
112
124
  self.inner.origin.set(origin).unwrap();
113
125
  }
@@ -121,28 +133,29 @@ impl HttpRequestContext {
121
133
  }
122
134
 
123
135
  pub fn short_request_id(&self) -> String {
124
- format!("{:016x}", self.inner.request_id & 0xffff_ffff_ffff_ffff)
136
+ format!("{:08x}", self.inner.request_id & 0xffff_ffff)
125
137
  }
126
138
 
127
139
  pub fn request_id(&self) -> String {
128
- format!("{:016x}", self.inner.request_id)
140
+ format!("{:08x}", self.inner.request_id)
129
141
  }
130
142
 
131
- pub fn track_start_time(&self) {
143
+ pub fn init_logging_params(&self) {
132
144
  self.inner
133
145
  .request_start_time
134
146
  .get_or_init(chrono::Local::now);
135
147
  }
136
148
 
137
- pub fn start_time(&self) -> Option<chrono::DateTime<Local>> {
149
+ pub fn start_instant(&self) -> Instant {
150
+ self.inner.start_instant
151
+ }
152
+
153
+ pub fn start_time(&self) -> Option<DateTime<Local>> {
138
154
  self.inner.request_start_time.get().cloned()
139
155
  }
140
156
 
141
- pub fn get_response_time(&self) -> Option<chrono::TimeDelta> {
142
- self.inner
143
- .request_start_time
144
- .get()
145
- .map(|instant| Local::now() - instant)
157
+ pub fn get_response_time(&self) -> Duration {
158
+ self.inner.start_instant.elapsed()
146
159
  }
147
160
 
148
161
  pub fn set_response_format(&self, format: ResponseFormat) {
@@ -152,6 +165,10 @@ impl HttpRequestContext {
152
165
  pub fn response_format(&self) -> &ResponseFormat {
153
166
  self.inner.response_format.get().unwrap()
154
167
  }
168
+
169
+ pub fn supported_encoding_set(&self) -> Option<&Vec<HeaderValue>> {
170
+ self.inner.supported_encoding_set.get()
171
+ }
155
172
  }
156
173
 
157
174
  const SERVER_TOKEN_VERSION: HeaderValue =
@@ -170,12 +187,7 @@ impl Service<Request<Incoming>> for ItsiHttpService {
170
187
  let accept: ResponseFormat = req.accept().into();
171
188
  let accept_clone = accept.clone();
172
189
  let is_single_mode = self.server_params.workers == 1;
173
- let supported_encoding_set = req
174
- .headers()
175
- .get_all(ACCEPT_ENCODING)
176
- .into_iter()
177
- .cloned()
178
- .collect::<Vec<_>>();
190
+
179
191
  let request_timeout = self.server_params.request_timeout;
180
192
  let is_ruby_request = Arc::new(AtomicBool::new(false));
181
193
  let irr_clone = is_ruby_request.clone();
@@ -187,7 +199,6 @@ impl Service<Request<Incoming>> for ItsiHttpService {
187
199
  self_clone,
188
200
  matching_pattern,
189
201
  accept_clone.clone(),
190
- supported_encoding_set,
191
202
  irr_clone,
192
203
  );
193
204
  let mut depth = 0;
@@ -229,8 +240,8 @@ impl Service<Request<Incoming>> for ItsiHttpService {
229
240
  Ok(resp)
230
241
  };
231
242
 
232
- Box::pin(async move {
233
- if let Some(timeout_duration) = request_timeout {
243
+ if let Some(timeout_duration) = request_timeout {
244
+ Box::pin(async move {
234
245
  match timeout(timeout_duration, service_future).await {
235
246
  Ok(result) => result,
236
247
  Err(_) => {
@@ -249,9 +260,9 @@ impl Service<Request<Incoming>> for ItsiHttpService {
249
260
  Ok(TIMEOUT_RESPONSE.to_http_response(accept).await)
250
261
  }
251
262
  }
252
- } else {
253
- service_future.await
254
- }
255
- })
263
+ })
264
+ } else {
265
+ Box::pin(service_future)
266
+ }
256
267
  }
257
268
  }
@@ -51,6 +51,10 @@ pub static ROOT_STATIC_FILE_SERVER: LazyLock<StaticFileServer> = LazyLock::new(|
51
51
  not_found_behavior: NotFoundBehavior::Error(ErrorResponse::not_found()),
52
52
  serve_hidden_files: false,
53
53
  allowed_extensions: vec!["html".to_string(), "css".to_string(), "js".to_string()],
54
+ miss_cache: Cache::builder()
55
+ .max_capacity(1000)
56
+ .time_to_live(Duration::from_secs(1))
57
+ .build(),
54
58
  })
55
59
  .unwrap()
56
60
  });
@@ -85,6 +89,7 @@ pub struct StaticFileServerConfig {
85
89
  pub headers: Option<HashMap<String, String>>,
86
90
  pub serve_hidden_files: bool,
87
91
  pub allowed_extensions: Vec<String>,
92
+ pub miss_cache: Cache<String, NotFoundBehavior>,
88
93
  }
89
94
 
90
95
  #[derive(Debug, Clone)]
@@ -389,6 +394,29 @@ impl StaticFileServer {
389
394
  abs_path: &str,
390
395
  accept: ResponseFormat,
391
396
  ) -> std::result::Result<ResolvedAsset, NotFoundBehavior> {
397
+ let ext_opt = Path::new(key)
398
+ .extension()
399
+ .and_then(|e| e.to_str())
400
+ .map(|s| s.to_lowercase());
401
+
402
+ // If the allowed list is non-empty, enforce membership
403
+ if !self.allowed_extensions.is_empty() {
404
+ match ext_opt {
405
+ Some(ref ext)
406
+ if self
407
+ .allowed_extensions
408
+ .iter()
409
+ .any(|ae| ae.eq_ignore_ascii_case(ext)) => {}
410
+ None if self.config.try_html_extension => {}
411
+ _ => {
412
+ return Err(self.config.not_found_behavior.clone());
413
+ }
414
+ }
415
+ }
416
+
417
+ if let Some(cached_nf) = self.miss_cache.get(key) {
418
+ return Err(cached_nf.clone());
419
+ }
392
420
  // First check if we have a cached mapping for this key
393
421
  if let Some(path) = self.key_to_path.lock().await.get(key) {
394
422
  // Check if the cached entry is still valid
@@ -449,7 +477,6 @@ impl StaticFileServer {
449
477
 
450
478
  let mut full_path = self.config.root_dir.clone();
451
479
  full_path.push(normalized_path);
452
- debug!("Resolving path {:?}", full_path);
453
480
  // Check if path exists and is a file
454
481
  match tokio::fs::metadata(&full_path).await {
455
482
  Ok(metadata) => {
@@ -561,7 +588,6 @@ impl StaticFileServer {
561
588
  }
562
589
  Err(_) => {
563
590
  // Path doesn't exist, try with .html extension if configured
564
- debug!("Path doesn't exist");
565
591
  if self.config.try_html_extension {
566
592
  let mut html_path = full_path.clone();
567
593
  html_path.set_extension("html");
@@ -592,7 +618,9 @@ impl StaticFileServer {
592
618
  }
593
619
 
594
620
  // If we get here, we couldn't resolve the key to a file
595
- Err(self.config.not_found_behavior.clone())
621
+ let nf = self.config.not_found_behavior.clone();
622
+ self.miss_cache.insert(key.to_string(), nf.clone());
623
+ Err(nf)
596
624
  }
597
625
 
598
626
  async fn stream_file_range(
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Scheduler
5
- VERSION = "0.2.3"
5
+ VERSION = "0.2.4"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi-scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters