feat: support multipart ranges (#535)
parent
d255f1376a
commit
eda9769b2a
|
@ -843,7 +843,7 @@ impl Server {
|
|||
}
|
||||
}
|
||||
|
||||
let range = if use_range {
|
||||
let ranges = if use_range {
|
||||
headers.get(RANGE).map(|range| {
|
||||
range
|
||||
.to_str()
|
||||
|
@ -864,27 +864,59 @@ impl Server {
|
|||
|
||||
res.headers_mut().typed_insert(AcceptRanges::bytes());
|
||||
|
||||
if let Some(range) = range {
|
||||
if let Some((start, end)) = range {
|
||||
file.seek(SeekFrom::Start(start)).await?;
|
||||
let range_size = end - start + 1;
|
||||
*res.status_mut() = StatusCode::PARTIAL_CONTENT;
|
||||
let content_range = format!("bytes {}-{}/{}", start, end, size);
|
||||
res.headers_mut()
|
||||
.insert(CONTENT_RANGE, content_range.parse()?);
|
||||
res.headers_mut()
|
||||
.insert(CONTENT_LENGTH, format!("{range_size}").parse()?);
|
||||
if head_only {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(ranges) = ranges {
|
||||
if let Some(ranges) = ranges {
|
||||
if ranges.len() == 1 {
|
||||
let (start, end) = ranges[0];
|
||||
file.seek(SeekFrom::Start(start)).await?;
|
||||
let range_size = end - start + 1;
|
||||
*res.status_mut() = StatusCode::PARTIAL_CONTENT;
|
||||
let content_range = format!("bytes {}-{}/{}", start, end, size);
|
||||
res.headers_mut()
|
||||
.insert(CONTENT_RANGE, content_range.parse()?);
|
||||
res.headers_mut()
|
||||
.insert(CONTENT_LENGTH, format!("{range_size}").parse()?);
|
||||
if head_only {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let stream_body = StreamBody::new(
|
||||
LengthLimitedStream::new(file, range_size as usize)
|
||||
.map_ok(Frame::data)
|
||||
.map_err(|err| anyhow!("{err}")),
|
||||
);
|
||||
let boxed_body = stream_body.boxed();
|
||||
*res.body_mut() = boxed_body;
|
||||
let stream_body = StreamBody::new(
|
||||
LengthLimitedStream::new(file, range_size as usize)
|
||||
.map_ok(Frame::data)
|
||||
.map_err(|err| anyhow!("{err}")),
|
||||
);
|
||||
let boxed_body = stream_body.boxed();
|
||||
*res.body_mut() = boxed_body;
|
||||
} else {
|
||||
*res.status_mut() = StatusCode::PARTIAL_CONTENT;
|
||||
let boundary = Uuid::new_v4();
|
||||
let mut body = Vec::new();
|
||||
let content_type = get_content_type(path).await?;
|
||||
for (start, end) in ranges {
|
||||
file.seek(SeekFrom::Start(start)).await?;
|
||||
let range_size = end - start + 1;
|
||||
let content_range = format!("bytes {}-{}/{}", start, end, size);
|
||||
let part_header = format!(
|
||||
"--{boundary}\r\nContent-Type: {content_type}\r\nContent-Range: {content_range}\r\n\r\n",
|
||||
);
|
||||
body.extend_from_slice(part_header.as_bytes());
|
||||
let mut buffer = vec![0; range_size as usize];
|
||||
file.read_exact(&mut buffer).await?;
|
||||
body.extend_from_slice(&buffer);
|
||||
body.extend_from_slice(b"\r\n");
|
||||
}
|
||||
body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
|
||||
res.headers_mut().insert(
|
||||
CONTENT_TYPE,
|
||||
format!("multipart/byteranges; boundary={boundary}").parse()?,
|
||||
);
|
||||
res.headers_mut()
|
||||
.insert(CONTENT_LENGTH, format!("{}", body.len()).parse()?);
|
||||
if head_only {
|
||||
return Ok(());
|
||||
}
|
||||
*res.body_mut() = body_full(body);
|
||||
}
|
||||
} else {
|
||||
*res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
|
||||
res.headers_mut()
|
||||
|
@ -1771,8 +1803,10 @@ fn parse_upload_offset(headers: &HeaderMap<HeaderValue>, size: u64) -> Result<Op
|
|||
if value == "append" {
|
||||
return Ok(Some(size));
|
||||
}
|
||||
let (start, _) = parse_range(value, size).ok_or_else(err)?;
|
||||
Ok(Some(start))
|
||||
// use the first range
|
||||
let ranges = parse_range(value, size).ok_or_else(err)?;
|
||||
let (start, _) = ranges.first().ok_or_else(err)?;
|
||||
Ok(Some(*start))
|
||||
}
|
||||
|
||||
async fn sha256_file(path: &Path) -> Result<String> {
|
||||
|
|
68
src/utils.rs
68
src/utils.rs
|
@ -100,36 +100,42 @@ pub fn load_private_key<T: AsRef<Path>>(filename: T) -> Result<PrivateKeyDer<'st
|
|||
anyhow::bail!("No supported private key in file");
|
||||
}
|
||||
|
||||
pub fn parse_range(range: &str, size: u64) -> Option<(u64, u64)> {
|
||||
let (unit, range) = range.split_once('=')?;
|
||||
if unit != "bytes" || range.contains(',') {
|
||||
pub fn parse_range(range: &str, size: u64) -> Option<Vec<(u64, u64)>> {
|
||||
let (unit, ranges) = range.split_once('=')?;
|
||||
if unit != "bytes" {
|
||||
return None;
|
||||
}
|
||||
let (start, end) = range.split_once('-')?;
|
||||
if start.is_empty() {
|
||||
let offset = end.parse::<u64>().ok()?;
|
||||
if offset <= size {
|
||||
Some((size - offset, size - 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let start = start.parse::<u64>().ok()?;
|
||||
if start < size {
|
||||
if end.is_empty() {
|
||||
Some((start, size - 1))
|
||||
|
||||
let mut result = Vec::new();
|
||||
for range in ranges.split(',') {
|
||||
let (start, end) = range.trim().split_once('-')?;
|
||||
if start.is_empty() {
|
||||
let offset = end.parse::<u64>().ok()?;
|
||||
if offset <= size {
|
||||
result.push((size - offset, size - 1));
|
||||
} else {
|
||||
let end = end.parse::<u64>().ok()?;
|
||||
if end < size {
|
||||
Some((start, end))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
None
|
||||
let start = start.parse::<u64>().ok()?;
|
||||
if start < size {
|
||||
if end.is_empty() {
|
||||
result.push((start, size - 1));
|
||||
} else {
|
||||
let end = end.parse::<u64>().ok()?;
|
||||
if end < size {
|
||||
result.push((start, end));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -162,13 +168,19 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_parse_range() {
|
||||
assert_eq!(parse_range("bytes=0-499", 500), Some((0, 499)));
|
||||
assert_eq!(parse_range("bytes=0-", 500), Some((0, 499)));
|
||||
assert_eq!(parse_range("bytes=299-", 500), Some((299, 499)));
|
||||
assert_eq!(parse_range("bytes=-500", 500), Some((0, 499)));
|
||||
assert_eq!(parse_range("bytes=-300", 500), Some((200, 499)));
|
||||
assert_eq!(parse_range("bytes=0-499", 500), Some(vec![(0, 499)]));
|
||||
assert_eq!(parse_range("bytes=0-", 500), Some(vec![(0, 499)]));
|
||||
assert_eq!(parse_range("bytes=299-", 500), Some(vec![(299, 499)]));
|
||||
assert_eq!(parse_range("bytes=-500", 500), Some(vec![(0, 499)]));
|
||||
assert_eq!(parse_range("bytes=-300", 500), Some(vec![(200, 499)]));
|
||||
assert_eq!(
|
||||
parse_range("bytes=0-199, 100-399, 400-, -200", 500),
|
||||
Some(vec![(0, 199), (100, 399), (400, 499), (300, 499)])
|
||||
);
|
||||
assert_eq!(parse_range("bytes=500-", 500), None);
|
||||
assert_eq!(parse_range("bytes=-501", 500), None);
|
||||
assert_eq!(parse_range("bytes=0-500", 500), None);
|
||||
assert_eq!(parse_range("bytes=0-199,", 500), None);
|
||||
assert_eq!(parse_range("bytes=0-199, 500-", 500), None);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ mod fixtures;
|
|||
mod utils;
|
||||
|
||||
use fixtures::{server, Error, TestServer};
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
|
@ -39,3 +39,68 @@ fn get_file_range_invalid(server: TestServer) -> Result<(), Error> {
|
|||
assert_eq!(resp.headers().get("content-range").unwrap(), "bytes */18");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_multipart_body<'a>(body: &'a str, boundary: &str) -> Vec<(HeaderMap, &'a str)> {
|
||||
body.split(&format!("--{}", boundary))
|
||||
.filter(|part| !part.is_empty() && *part != "--\r\n")
|
||||
.map(|part| {
|
||||
let (head, body) = part.trim_ascii().split_once("\r\n\r\n").unwrap();
|
||||
let headers = head
|
||||
.split("\r\n")
|
||||
.fold(HeaderMap::new(), |mut headers, header| {
|
||||
let (key, value) = header.split_once(":").unwrap();
|
||||
let key = HeaderName::from_bytes(key.as_bytes()).unwrap();
|
||||
let value = HeaderValue::from_str(value.trim_ascii_start()).unwrap();
|
||||
headers.insert(key, value);
|
||||
headers
|
||||
});
|
||||
(headers, body)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn get_file_multipart_range(server: TestServer) -> Result<(), Error> {
|
||||
let resp = fetch!(b"GET", format!("{}index.html", server.url()))
|
||||
.header("range", HeaderValue::from_static("bytes=0-11, 6-17"))
|
||||
.send()?;
|
||||
assert_eq!(resp.status(), 206);
|
||||
assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
|
||||
|
||||
let content_type = resp
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.unwrap()
|
||||
.to_str()?
|
||||
.to_string();
|
||||
assert!(content_type.starts_with("multipart/byteranges; boundary="));
|
||||
|
||||
let boundary = content_type.split_once('=').unwrap().1.trim_ascii_start();
|
||||
assert!(!boundary.is_empty());
|
||||
|
||||
let body = resp.text()?;
|
||||
let parts = parse_multipart_body(&body, boundary);
|
||||
assert_eq!(parts.len(), 2);
|
||||
|
||||
let (headers, body) = &parts[0];
|
||||
assert_eq!(headers.get("content-range").unwrap(), "bytes 0-11/18");
|
||||
assert_eq!(*body, "This is inde");
|
||||
|
||||
let (headers, body) = &parts[1];
|
||||
assert_eq!(headers.get("content-range").unwrap(), "bytes 6-17/18");
|
||||
assert_eq!(*body, "s index.html");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn get_file_multipart_range_invalid(server: TestServer) -> Result<(), Error> {
|
||||
let resp = fetch!(b"GET", format!("{}index.html", server.url()))
|
||||
.header("range", HeaderValue::from_static("bytes=0-6, 20-30"))
|
||||
.send()?;
|
||||
assert_eq!(resp.status(), 416);
|
||||
assert_eq!(resp.headers().get("content-range").unwrap(), "bytes */18");
|
||||
assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
|
||||
assert_eq!(resp.headers().get("content-length").unwrap(), "0");
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue