1use std::collections::HashSet;
9
10use crate::range::RangeHandling;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[allow(clippy::manual_non_exhaustive)]
15pub enum StreamingDecision {
16 Buffer,
18
19 SkipCache,
21
22 #[doc(hidden)]
24 StreamThrough,
25
26 StreamIfPossible,
28}
29
30#[derive(Debug, Clone)]
55pub struct StreamingPolicy {
56 pub enabled: bool,
58
59 pub max_cacheable_size: Option<usize>,
62
63 pub excluded_content_types: HashSet<String>,
66
67 pub force_cache_content_types: HashSet<String>,
70
71 pub stream_threshold: usize,
74
75 pub range_handling: RangeHandling,
77
78 pub enable_chunk_cache: bool,
83
84 pub chunk_size: usize,
90
91 pub min_chunk_file_size: u64,
96}
97
98impl Default for StreamingPolicy {
99 fn default() -> Self {
100 Self {
101 enabled: true,
102 max_cacheable_size: Some(1024 * 1024), excluded_content_types: HashSet::from([
104 "application/pdf".to_string(),
105 "video/*".to_string(),
106 "audio/*".to_string(),
107 "application/zip".to_string(),
108 "application/x-rar".to_string(),
109 "application/x-tar".to_string(),
110 "application/gzip".to_string(),
111 "application/x-7z-compressed".to_string(),
112 "application/octet-stream".to_string(),
113 ]),
114 force_cache_content_types: HashSet::from([
115 "application/json".to_string(),
116 "application/xml".to_string(),
117 "text/*".to_string(),
118 ]),
119 stream_threshold: 512 * 1024, range_handling: RangeHandling::default(),
121 enable_chunk_cache: false, chunk_size: 1024 * 1024, min_chunk_file_size: 10 * 1024 * 1024, }
125 }
126}
127
128impl StreamingPolicy {
129 pub fn disabled() -> Self {
132 Self {
133 enabled: false,
134 max_cacheable_size: None,
135 excluded_content_types: HashSet::new(),
136 force_cache_content_types: HashSet::new(),
137 stream_threshold: usize::MAX,
138 range_handling: RangeHandling::PassThrough,
139 enable_chunk_cache: false,
140 chunk_size: 1024 * 1024,
141 min_chunk_file_size: 0,
142 }
143 }
144
145 pub fn size_only(max_size: usize) -> Self {
147 Self {
148 enabled: true,
149 max_cacheable_size: Some(max_size),
150 excluded_content_types: HashSet::new(),
151 force_cache_content_types: HashSet::new(),
152 stream_threshold: max_size,
153 range_handling: RangeHandling::PassThrough,
154 enable_chunk_cache: false,
155 chunk_size: 1024 * 1024,
156 min_chunk_file_size: 0,
157 }
158 }
159
160 pub fn content_type_only(excluded: HashSet<String>) -> Self {
162 Self {
163 enabled: true,
164 max_cacheable_size: None,
165 excluded_content_types: excluded,
166 force_cache_content_types: HashSet::new(),
167 stream_threshold: usize::MAX,
168 range_handling: RangeHandling::PassThrough,
169 enable_chunk_cache: false,
170 chunk_size: 1024 * 1024,
171 min_chunk_file_size: 0,
172 }
173 }
174}
175
176pub fn should_stream(
195 policy: &StreamingPolicy,
196 size_hint: &http_body::SizeHint,
197 content_type: Option<&str>,
198 content_length: Option<u64>,
199) -> StreamingDecision {
200 if !policy.enabled {
202 return StreamingDecision::Buffer;
203 }
204
205 let is_forced = if let Some(ct) = content_type {
207 if is_excluded_content_type(ct, &policy.excluded_content_types) {
209 return StreamingDecision::SkipCache;
210 }
211
212 is_forced_content_type(ct, &policy.force_cache_content_types)
214 } else {
215 false
216 };
217
218 if let Some(exact_size) = size_hint.exact() {
220 return decide_by_size(exact_size as usize, policy, is_forced);
221 }
222
223 if let Some(upper_bound) = size_hint.upper() {
224 return decide_by_size(upper_bound as usize, policy, is_forced);
225 }
226
227 if let Some(content_len) = content_length {
229 return decide_by_size(content_len as usize, policy, is_forced);
230 }
231
232 StreamingDecision::StreamIfPossible
235}
236
237fn decide_by_size(size: usize, policy: &StreamingPolicy, _is_forced: bool) -> StreamingDecision {
242 if let Some(max_size) = policy.max_cacheable_size {
243 if size > max_size {
244 return StreamingDecision::SkipCache;
245 }
246 }
247
248 StreamingDecision::Buffer
251}
252
253fn is_excluded_content_type(content_type: &str, excluded: &HashSet<String>) -> bool {
257 let normalized = content_type.to_lowercase();
258
259 for pattern in excluded {
260 if matches_pattern(&normalized, pattern) {
261 return true;
262 }
263 }
264
265 false
266}
267
268fn is_forced_content_type(content_type: &str, forced: &HashSet<String>) -> bool {
270 let normalized = content_type.to_lowercase();
271
272 for pattern in forced {
273 if matches_pattern(&normalized, pattern) {
274 return true;
275 }
276 }
277
278 false
279}
280
281fn matches_pattern(content_type: &str, pattern: &str) -> bool {
286 let pattern_lower = pattern.to_lowercase();
287
288 if pattern_lower.ends_with("/*") {
289 let prefix = &pattern_lower[..pattern_lower.len() - 2];
291 content_type.starts_with(prefix)
292 } else {
293 content_type == pattern_lower || content_type.starts_with(&format!("{};", pattern_lower))
295 }
296}
297
298pub fn extract_size_info(
300 size_hint: &http_body::SizeHint,
301 content_length: Option<u64>,
302) -> Option<u64> {
303 size_hint
304 .exact()
305 .or_else(|| size_hint.upper())
306 .or(content_length)
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use http_body::SizeHint;
313
314 #[test]
315 fn test_default_policy_excludes_pdf() {
316 let policy = StreamingPolicy::default();
317 let size_hint = SizeHint::with_exact(5 * 1024 * 1024); let decision = should_stream(
320 &policy,
321 &size_hint,
322 Some("application/pdf"),
323 Some(5 * 1024 * 1024),
324 );
325
326 assert_eq!(decision, StreamingDecision::SkipCache);
327 }
328
329 #[test]
330 fn test_default_policy_excludes_video() {
331 let policy = StreamingPolicy::default();
332 let size_hint = SizeHint::with_exact(10 * 1024 * 1024);
333
334 let decision = should_stream(&policy, &size_hint, Some("video/mp4"), None);
335
336 assert_eq!(decision, StreamingDecision::SkipCache);
337 }
338
339 #[test]
340 fn test_small_json_gets_buffered() {
341 let policy = StreamingPolicy::default();
342 let size_hint = SizeHint::with_exact(1024); let decision = should_stream(&policy, &size_hint, Some("application/json"), Some(1024));
345
346 assert_eq!(decision, StreamingDecision::Buffer);
347 }
348
349 #[test]
350 fn test_large_json_skipped_by_size() {
351 let policy = StreamingPolicy::default();
352 let size_hint = SizeHint::with_exact(2 * 1024 * 1024); let decision = should_stream(
355 &policy,
356 &size_hint,
357 Some("application/json"),
358 Some(2 * 1024 * 1024),
359 );
360
361 assert_eq!(decision, StreamingDecision::SkipCache);
362 }
363
364 #[test]
365 fn test_force_cache_respects_size_limits() {
366 let mut policy = StreamingPolicy::default();
368 policy
369 .force_cache_content_types
370 .insert("application/important".to_string());
371
372 let size_hint = SizeHint::with_exact(5 * 1024 * 1024); let decision = should_stream(
375 &policy,
376 &size_hint,
377 Some("application/important"),
378 Some(5 * 1024 * 1024),
379 );
380
381 assert_eq!(decision, StreamingDecision::SkipCache);
383
384 let small_hint = SizeHint::with_exact(500 * 1024); let decision_small = should_stream(
387 &policy,
388 &small_hint,
389 Some("application/important"),
390 Some(500 * 1024),
391 );
392 assert_eq!(decision_small, StreamingDecision::Buffer);
393 }
394
395 #[test]
396 fn test_disabled_policy_always_buffers() {
397 let policy = StreamingPolicy::disabled();
398 let size_hint = SizeHint::with_exact(10 * 1024 * 1024);
399
400 let decision = should_stream(
401 &policy,
402 &size_hint,
403 Some("application/pdf"),
404 Some(10 * 1024 * 1024),
405 );
406
407 assert_eq!(decision, StreamingDecision::Buffer);
408 }
409
410 #[test]
411 fn test_wildcard_pattern_matching() {
412 assert!(matches_pattern("video/mp4", "video/*"));
413 assert!(matches_pattern("video/mpeg", "video/*"));
414 assert!(matches_pattern("audio/mp3", "audio/*"));
415 assert!(!matches_pattern("application/json", "video/*"));
416 }
417
418 #[test]
419 fn test_exact_pattern_matching() {
420 assert!(matches_pattern("application/pdf", "application/pdf"));
421 assert!(!matches_pattern("application/pdf", "pdf")); assert!(!matches_pattern("text/plain", "application/pdf"));
423 assert!(matches_pattern(
425 "application/json; charset=utf-8",
426 "application/json"
427 ));
428 }
429
430 #[test]
431 fn test_size_hint_exact() {
432 let policy = StreamingPolicy::default();
433 let size_hint = SizeHint::with_exact(500 * 1024); let decision = should_stream(&policy, &size_hint, None, None);
436 assert_eq!(decision, StreamingDecision::Buffer);
437 }
438
439 #[test]
440 fn test_size_hint_upper_bound() {
441 let policy = StreamingPolicy::default();
442 let mut size_hint = SizeHint::default();
443 size_hint.set_upper(500 * 1024);
444
445 let decision = should_stream(&policy, &size_hint, None, None);
446 assert_eq!(decision, StreamingDecision::Buffer);
447 }
448
449 #[test]
450 fn test_content_length_fallback() {
451 let policy = StreamingPolicy::default();
452 let size_hint = SizeHint::default(); let decision = should_stream(&policy, &size_hint, None, Some(500 * 1024));
455 assert_eq!(decision, StreamingDecision::Buffer);
456 }
457
458 #[test]
459 fn test_unknown_size_conservative() {
460 let policy = StreamingPolicy::default();
461 let size_hint = SizeHint::default();
462
463 let decision = should_stream(&policy, &size_hint, None, None);
464 assert_eq!(decision, StreamingDecision::StreamIfPossible);
465 }
466
467 #[test]
468 fn test_size_only_policy() {
469 let policy = StreamingPolicy::size_only(512 * 1024);
470 let size_hint = SizeHint::with_exact(1024 * 1024);
471
472 let decision = should_stream(&policy, &size_hint, Some("application/pdf"), None);
474 assert_eq!(decision, StreamingDecision::SkipCache); let size_hint_small = SizeHint::with_exact(256 * 1024);
477 let decision_small =
478 should_stream(&policy, &size_hint_small, Some("application/pdf"), None);
479 assert_eq!(decision_small, StreamingDecision::Buffer); }
481
482 #[test]
483 fn test_content_type_only_policy() {
484 let mut excluded = HashSet::new();
485 excluded.insert("application/pdf".to_string());
486 let policy = StreamingPolicy::content_type_only(excluded);
487
488 let size_hint = SizeHint::with_exact(10 * 1024 * 1024); let decision = should_stream(&policy, &size_hint, Some("application/json"), None);
492 assert_eq!(decision, StreamingDecision::Buffer);
493
494 let decision_pdf = should_stream(&policy, &size_hint, Some("application/pdf"), None);
496 assert_eq!(decision_pdf, StreamingDecision::SkipCache);
497 }
498
499 #[test]
500 fn test_extract_size_info() {
501 let size_hint = SizeHint::with_exact(1024);
502 assert_eq!(extract_size_info(&size_hint, None), Some(1024));
503
504 let mut size_hint_upper = SizeHint::default();
505 size_hint_upper.set_upper(2048);
506 assert_eq!(extract_size_info(&size_hint_upper, None), Some(2048));
507
508 let size_hint_none = SizeHint::default();
509 assert_eq!(extract_size_info(&size_hint_none, Some(4096)), Some(4096));
510
511 assert_eq!(extract_size_info(&size_hint_none, None), None);
512 }
513
514 #[test]
515 fn test_case_insensitive_content_type() {
516 let policy = StreamingPolicy::default();
517 let size_hint = SizeHint::with_exact(1024);
518
519 assert_eq!(
521 should_stream(&policy, &size_hint, Some("Application/PDF"), None),
522 StreamingDecision::SkipCache
523 );
524 assert_eq!(
525 should_stream(&policy, &size_hint, Some("APPLICATION/PDF"), None),
526 StreamingDecision::SkipCache
527 );
528 assert_eq!(
529 should_stream(&policy, &size_hint, Some("Video/MP4"), None),
530 StreamingDecision::SkipCache
531 );
532 }
533
534 #[test]
535 fn test_content_type_with_charset() {
536 let policy = StreamingPolicy::default();
537 let size_hint = SizeHint::with_exact(1024);
538
539 let decision = should_stream(
541 &policy,
542 &size_hint,
543 Some("application/json; charset=utf-8"),
544 None,
545 );
546 assert_eq!(decision, StreamingDecision::Buffer);
547 }
548}