]> OzVa Git service - ozva-cloud/commitdiff
feat: support multipart ranges (#535)
author45gfg9 <Mc45@qq.com>
Sat, 1 Feb 2025 00:28:34 +0000 (08:28 +0800)
committerGitHub <noreply@github.com>
Sat, 1 Feb 2025 00:28:34 +0000 (08:28 +0800)
src/server.rs
src/utils.rs
tests/range.rs

index 5bb195a9bf502532d7b1904061227cedc4816c8b..da9da9a64cba47b2b721ede9a5d04e7cdcbedcec 100644 (file)
@@ -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> {
index edf8544d5175975fa7c1f7e51145bad110fbfb5e..600f1b37555e65fa393cba68a1cf77593369696e 100644 (file)
@@ -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))
+                return None;
+            }
+        } else {
+            let start = start.parse::<u64>().ok()?;
+            if start < size {
+                if end.is_empty() {
+                    result.push((start, size - 1));
                 } else {
-                    None
+                    let end = end.parse::<u64>().ok()?;
+                    if end < size {
+                        result.push((start, end));
+                    } else {
+                        return None;
+                    }
                 }
+            } else {
+                return None;
             }
-        } else {
-            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);
     }
 }
index 511c2446ec9293a4c772b3892c6fedb81eccdc54..cb568895cb159131e6870e996f5deec2a01e6343 100644 (file)
@@ -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(())
+}