Skip to main content

range_requests/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3use std::ops::Range;
4
5use bytes::Bytes;
6
7pub mod headers;
8
9use crate::headers::{
10    content_range::{Bound, HttpContentRange, Unsatisfiable},
11    range::HttpRange,
12};
13
14/// Returns a [`BodyRange`] of [`Bytes`] if the provided [`HttpRange`] is satisfiable, otherwise it returns [`UnsatisfiableRange`].
15///
16/// [`HttpRange`]: crate::headers::range::HttpRange
17pub fn serve_file_with_http_range(
18    body: Bytes,
19    http_range: Option<HttpRange>,
20) -> Result<BodyRange<Bytes>, UnsatisfiableRange> {
21    let size = u64::try_from(body.len()).expect("we do not support 128bit usize");
22
23    let content_range = file_range(size, http_range)?;
24
25    let start = usize::try_from(content_range.range.start).expect("u64 doesn't fit usize");
26    let end = usize::try_from(content_range.range.end).expect("u64 doesn't fit usize");
27
28    Ok(BodyRange {
29        body: body.slice(start..end),
30        header: content_range.header,
31    })
32}
33
34/// Returns a [`ContentRange`] if the provided [`HttpRange`] is satisfiable, otherwise it returns [`UnsatisfiableRange`].
35///
36/// [`HttpRange`]: crate::headers::range::HttpRange
37pub fn file_range(
38    size: u64,
39    http_range: Option<HttpRange>,
40) -> Result<ContentRange, UnsatisfiableRange> {
41    let Some(http_range) = http_range else {
42        return Ok(ContentRange {
43            header: None,
44            range: 0..size,
45        });
46    };
47
48    let range = match http_range {
49        HttpRange::StartingPoint(start) if start < size => start..size,
50        HttpRange::Range(range) if range.start() < size => {
51            range.start()..range.end().saturating_add(1).min(size)
52        }
53        HttpRange::Suffix(suffix) if suffix > 0 && size > 0 => size.saturating_sub(suffix)..size,
54        _ => {
55            let content_range = HttpContentRange::Unsatisfiable(Unsatisfiable::new(size));
56            return Err(UnsatisfiableRange(content_range));
57        }
58    };
59
60    let content_range =
61        HttpContentRange::Bound(Bound::new(range.start..=range.end - 1, Some(size)).unwrap());
62
63    Ok(ContentRange {
64        header: Some(content_range),
65        range,
66    })
67}
68
69/// A container for the payload slice and the optional `Content-Range` header.
70///
71/// The header is `None` only if the body was not sliced.
72///
73/// If the `axum` feature is enabled this struct also implements `IntoResponse`.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct BodyRange<T> {
76    body: T,
77    header: Option<HttpContentRange>,
78}
79
80impl<T> BodyRange<T> {
81    /// Returns the sliced body.
82    pub fn body(&self) -> &T {
83        &self.body
84    }
85
86    pub fn into_body(self) -> T {
87        self.body
88    }
89
90    /// Returns an option of [`HttpContentRange`].
91    /// If it's None the provided [`HttpRange`] was None too.
92    pub fn header(&self) -> Option<HttpContentRange> {
93        self.header
94    }
95}
96
97/// A container for the payload range and the optional `Content-Range` header.
98///
99/// The header is `None` only if the body was not sliced.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ContentRange {
102    header: Option<HttpContentRange>,
103    range: Range<u64>,
104}
105
106impl ContentRange {
107    /// Returns an option of [`HttpContentRange`].
108    /// If it's None the provided [`HttpRange`] was None too.
109    pub fn header(&self) -> Option<HttpContentRange> {
110        self.header
111    }
112
113    /// Returns a [`Range`] of `u64` useful to manually slice the response body.
114    pub fn range(&self) -> &Range<u64> {
115        &self.range
116    }
117}
118
119/// An unsatisfiable range request.
120///
121/// If the `axum` feature is enabled this struct also implements `IntoResponse`.
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct UnsatisfiableRange(HttpContentRange);
124
125impl UnsatisfiableRange {
126    /// Returns the [`HttpContentRange`] header.
127    pub fn header(&self) -> HttpContentRange {
128        self.0
129    }
130}
131
132#[cfg(feature = "axum")]
133mod axum {
134    use crate::{BodyRange, UnsatisfiableRange};
135
136    use axum_core::response::{IntoResponse, Response};
137    use bytes::Bytes;
138    use http::{HeaderValue, StatusCode, header::CONTENT_RANGE};
139
140    impl IntoResponse for BodyRange<Bytes> {
141        fn into_response(self) -> Response {
142            match self.header {
143                Some(range) => (
144                    StatusCode::PARTIAL_CONTENT,
145                    [(CONTENT_RANGE, HeaderValue::from(&range))],
146                    self.body,
147                )
148                    .into_response(),
149                None => (StatusCode::OK, self.body).into_response(),
150            }
151        }
152    }
153
154    impl IntoResponse for UnsatisfiableRange {
155        fn into_response(self) -> Response {
156            (
157                StatusCode::RANGE_NOT_SATISFIABLE,
158                [(CONTENT_RANGE, HeaderValue::from(&self.0))],
159            )
160                .into_response()
161        }
162    }
163}