Skip to content

Commit 64eabad

Browse files
committed
cache file support range request
1 parent b822284 commit 64eabad

File tree

3 files changed

+109
-34
lines changed

3 files changed

+109
-34
lines changed

doc/Roadmap.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@
2424
### version 1.2.x
2525
- [x] more log for debug and trace
2626
- [x] basic CORS
27-
- [x] compress regression support(if client don't send accept-encoding header(including gzip), will send back data from file instead of cache)
27+
- [x] compress regression support(~~if client don't send accept-encoding header(including gzip), will send back data from file instead of cache~~ improved by v1.2.3)
2828
- [x] hot reload web static server(use SO_REUSEPORT *nix api, so it may be wrong with Windows).
2929
- [ ] ~~different config(cors/cache strategy/https and so on) for different domain.~~ (if this is needed?)
30-
31-
### version 1.3.x
32-
- [ ] cache File `Range` Header support
33-
- [ ] drop self maintained `Warp`(copy out needed code from Warp)
34-
- [ ] `HEAD` request support or drop
30+
--- version 1.2.2
31+
- [x] cache File `Range` Header support
32+
- [ ] ~~drop self maintained `Warp`(copy out needed code from Warp)~~ (so much code from warp/fs.)
33+
- [x] `HEAD` request support or drop(support, don't need to do anything)

server/src/file_cache.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,14 @@ use anyhow::anyhow;
33
use dashmap::DashMap;
44
use flate2::read::GzEncoder;
55
use flate2::Compression;
6-
use futures_util::sink::Buffer;
7-
use headers::{ETag, HeaderValue};
86
use hyper::body::Bytes;
97
use lazy_static::lazy_static;
108
use mime::Mime;
119
use std::collections::HashMap;
1210
use std::collections::HashSet;
1311
use std::fs::{File, Metadata};
14-
use std::io::{BufReader, Error, Read};
12+
use std::io::{BufReader, Read};
1513
use std::path::PathBuf;
16-
use std::str::FromStr;
1714
use std::sync::Arc;
1815
use std::time::Duration;
1916
use walkdir::WalkDir;
@@ -92,7 +89,7 @@ impl FileCache {
9289
.map_or("".to_string(), |x| x.to_string());
9390

9491
let data_block = if self.conf.max_size.unwrap_or(DEFAULT_MAX_SIZE)
95-
> metadata.len()
92+
< metadata.len()
9693
{
9794
tracing::debug!("file block:{}", entry_path.display());
9895
DataBlock::FileBlock(ArcPath(Arc::new(entry_path)))

server/src/static_file_filter.rs

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ use headers::{
55
AcceptRanges, CacheControl, ContentEncoding, ContentLength, ContentRange, ContentType,
66
HeaderMapExt, LastModified, Range,
77
};
8+
use hyper::body::Bytes;
89
use hyper::Body;
910
use percent_encoding::percent_decode_str;
11+
use std::ops::Bound;
1012
use std::path::Path;
1113
use std::sync::Arc;
1214
use tokio::fs::File;
1315
use tokio::io;
14-
use warp::fs::{bytes_range, conditionals, file_stream, optimal_buf_size, Cond, Conditionals};
16+
use warp::fs::{conditionals, file_stream, optimal_buf_size, Cond, Conditionals};
1517
use warp::host::Authority;
16-
use warp::http::{Response, StatusCode};
18+
use warp::http::{Method, Response, StatusCode};
1719
use warp::{reject, Filter, Rejection};
1820

1921
//from warp::fs
@@ -31,6 +33,50 @@ fn sanitize_path(tail: &str) -> Result<String, Rejection> {
3133
Err(reject::not_found())
3234
}
3335
}
36+
#[derive(Debug)]
37+
pub struct BadRange;
38+
39+
//from warp/fs
40+
fn bytes_range(range: Option<Range>, max_len: u64) -> Result<(u64, u64), BadRange> {
41+
let range = if let Some(range) = range {
42+
range
43+
} else {
44+
return Ok((0, max_len));
45+
};
46+
47+
let ret = range
48+
.iter()
49+
.map(|(start, end)| {
50+
let start = match start {
51+
Bound::Unbounded => 0,
52+
Bound::Included(s) => s,
53+
Bound::Excluded(s) => s + 1,
54+
};
55+
56+
let end = match end {
57+
Bound::Unbounded => max_len,
58+
Bound::Included(s) => {
59+
// For the special case where s == the file size
60+
if s == max_len {
61+
s
62+
} else {
63+
s + 1
64+
}
65+
}
66+
Bound::Excluded(s) => s,
67+
};
68+
69+
if start < end && end <= max_len {
70+
Ok((start, end))
71+
} else {
72+
tracing::trace!("unsatisfiable byte range: {}-{}/{}", start, end, max_len);
73+
Err(BadRange)
74+
}
75+
})
76+
.next()
77+
.unwrap_or(Ok((0, max_len)));
78+
ret
79+
}
3480

3581
// copy from warp::fs file_reply
3682
async fn file_reply(
@@ -89,50 +135,83 @@ async fn file_reply(
89135
}
90136
}
91137

138+
fn cache_reply(
139+
item: &CacheItem,
140+
bytes: &Bytes,
141+
range: Option<Range>,
142+
modified: Option<LastModified>,
143+
) -> Response<Body> {
144+
let mut len = bytes.len() as u64;
145+
// don't support multiple range
146+
bytes_range(range, len)
147+
.map(|(start, end)| {
148+
let sub_len = end - start;
149+
// range or all
150+
let body = Body::from(bytes.slice((
151+
Bound::Included(start as usize),
152+
Bound::Excluded(end as usize),
153+
)));
154+
let mut resp = Response::new(body);
155+
156+
if sub_len != len {
157+
*resp.status_mut() = StatusCode::PARTIAL_CONTENT;
158+
resp.headers_mut().typed_insert(
159+
ContentRange::bytes(start..end, len).expect("valid ContentRange"),
160+
);
161+
len = sub_len;
162+
resp.headers_mut().typed_insert(ContentLength(sub_len));
163+
} else {
164+
resp.headers_mut().typed_insert(ContentLength(len));
165+
}
166+
cache_item_to_response_header(&mut resp, &item, modified);
167+
resp
168+
})
169+
.unwrap_or_else(|_| {
170+
let mut resp = Response::new(Body::empty());
171+
*resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
172+
resp.headers_mut()
173+
.typed_insert(ContentRange::unsatisfied_bytes(len));
174+
resp
175+
})
176+
}
177+
92178
async fn cache_or_file_reply(
93179
item: Arc<CacheItem>,
94180
conditionals: Conditionals,
95181
accept_encoding: Option<String>,
96182
) -> Result<Response<Body>, Rejection> {
97183
let modified = item.meta.modified().map(LastModified::from).ok();
98-
99-
let resp = match conditionals.check(modified) {
184+
match conditionals.check(modified) {
100185
Cond::NoBody(resp) => Ok(resp),
101186
Cond::WithBody(range) => match &item.data {
102187
DataBlock::CacheBlock {
103188
bytes,
104-
compressed,
105189
path,
190+
compressed,
106191
} => {
107-
if (accept_encoding
192+
let compressed = *compressed;
193+
let client_accept_gzip = accept_encoding
108194
.as_ref()
109195
.filter(|x| x.contains("gzip"))
110-
.is_none())
111-
&& *compressed
112-
{
113-
tracing::debug!(
114-
"{} don't use cache for accept_encoding:{:?}",
115-
path.as_ref().display(),
116-
&accept_encoding
117-
);
196+
.is_some();
197+
//gzip header, compressed_data
198+
//true,true => cache
199+
//true, false => cache without content-encoding
200+
//false,false => cache without content-encoding
201+
//false, true => file
202+
if !client_accept_gzip && compressed {
118203
file_reply(&item, path.as_ref(), range, modified).await
119204
} else {
120-
let mut resp = Response::new(Body::from(bytes.clone()));
121-
cache_item_to_response_header(&mut resp, &item, modified);
122-
resp.headers_mut()
123-
.typed_insert(ContentLength(bytes.len() as u64));
124-
if *compressed {
125-
resp.headers_mut()
126-
.typed_insert(ContentLength(bytes.len() as u64));
205+
let mut resp = cache_reply(item.as_ref(), bytes, range, modified);
206+
if client_accept_gzip && compressed {
127207
resp.headers_mut().typed_insert(ContentEncoding::gzip());
128208
}
129209
Ok(resp)
130210
}
131211
}
132212
DataBlock::FileBlock(path) => file_reply(&item, path.as_ref(), range, modified).await,
133213
},
134-
};
135-
resp
214+
}
136215
}
137216

138217
fn cache_item_to_response_header(

0 commit comments

Comments
 (0)