Skip to content

Commit 5302e33

Browse files
committed
feat: 检查响应头 etag 和 last_modified 是否与之前一致
1 parent 641cc2c commit 5302e33

File tree

5 files changed

+77
-20
lines changed

5 files changed

+77
-20
lines changed

crates/fast-down/src/http/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod puller;
33
pub use prefetch::*;
44
pub use puller::*;
55

6+
use crate::url_info::FileId;
67
use bytes::Bytes;
78
use fast_pull::ProgressEntry;
89
use std::{fmt::Debug, future::Future};
@@ -43,4 +44,5 @@ pub enum HttpError<Client: HttpClient> {
4344
Chunk(GetChunkError<Client>),
4445
GetHeader(GetHeaderError<Client>),
4546
Irrecoverable,
47+
MismatchedBody(FileId),
4648
}

crates/fast-down/src/http/prefetch.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::{
22
UrlInfo,
33
http::{GetResponse, HttpClient, HttpError, HttpHeaders, HttpRequestBuilder, HttpResponse},
4+
url_info::FileId,
45
};
56
use content_disposition;
67
use std::future::Future;
@@ -65,8 +66,7 @@ async fn prefetch<Client: HttpClient>(
6566
size,
6667
supports_range,
6768
fast_download: size > 0 && supports_range,
68-
etag: headers.get("etag").ok().map(|s| s.to_string()),
69-
last_modified: headers.get("last-modified").ok().map(|s| s.to_string()),
69+
file_id: FileId::new(headers.get("etag").ok(), headers.get("last-modified").ok()),
7070
},
7171
resp,
7272
))

crates/fast-down/src/http/puller.rs

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::http::{
2-
GetRequestError, GetResponse, HttpClient, HttpError, HttpRequestBuilder, HttpResponse,
2+
FileId, GetRequestError, GetResponse, HttpClient, HttpError, HttpHeaders, HttpRequestBuilder,
3+
HttpResponse,
34
};
45
use bytes::Bytes;
56
use fast_pull::{ProgressEntry, RandPuller, SeqPuller};
@@ -17,13 +18,20 @@ pub struct HttpPuller<Client: HttpClient> {
1718
pub(crate) client: Client,
1819
url: Url,
1920
resp: Arc<SpinMutex<Option<GetResponse<Client>>>>,
21+
file_id: FileId,
2022
}
2123
impl<Client: HttpClient> HttpPuller<Client> {
22-
pub fn new(url: Url, client: Client, resp: Option<GetResponse<Client>>) -> Self {
24+
pub fn new(
25+
url: Url,
26+
client: Client,
27+
resp: Option<GetResponse<Client>>,
28+
file_id: FileId,
29+
) -> Self {
2330
Self {
2431
client,
2532
url,
2633
resp: Arc::new(SpinMutex::new(resp)),
34+
file_id,
2735
}
2836
}
2937
}
@@ -54,6 +62,7 @@ impl<Client: HttpClient + 'static> RandPuller for HttpPuller<Client> {
5462
} else {
5563
ResponseState::None
5664
},
65+
file_id: self.file_id.clone(),
5766
}
5867
}
5968
}
@@ -63,6 +72,7 @@ struct RandRequestStream<Client: HttpClient + 'static> {
6372
start: u64,
6473
end: u64,
6574
state: ResponseState<Client>,
75+
file_id: FileId,
6676
}
6777
impl<Client: HttpClient> Stream for RandRequestStream<Client> {
6878
type Item = Result<Bytes, HttpError<Client>>;
@@ -73,8 +83,16 @@ impl<Client: HttpClient> Stream for RandRequestStream<Client> {
7383
return match resp.try_poll_unpin(cx) {
7484
Poll::Ready(resp) => match resp {
7585
Ok(resp) => {
76-
self.state = ResponseState::Ready(resp);
77-
self.poll_next(cx)
86+
let etag = resp.headers().get("etag").ok();
87+
let last_modified = resp.headers().get("last-modified").ok();
88+
let new_file_id = FileId::new(etag, last_modified);
89+
if new_file_id != self.file_id {
90+
self.state = ResponseState::None;
91+
Poll::Ready(Some(Err(HttpError::MismatchedBody(new_file_id))))
92+
} else {
93+
self.state = ResponseState::Ready(resp);
94+
self.poll_next(cx)
95+
}
7896
}
7997
Err(e) => {
8098
self.state = ResponseState::None;
@@ -118,21 +136,21 @@ impl<Client: HttpClient> Stream for RandRequestStream<Client> {
118136
impl<Client: HttpClient + 'static> SeqPuller for HttpPuller<Client> {
119137
type Error = HttpError<Client>;
120138
fn pull(&mut self) -> impl TryStream<Ok = Bytes, Error = Self::Error> + Send + Unpin {
121-
match self.resp.lock().take() {
122-
Some(resp) => SeqRequestStream {
123-
state: ResponseState::Ready(resp),
124-
},
125-
None => {
126-
let req = self.client.get(self.url.clone(), None).send();
127-
SeqRequestStream {
128-
state: ResponseState::Pending(Box::pin(req)),
139+
SeqRequestStream {
140+
state: match self.resp.lock().take() {
141+
Some(resp) => ResponseState::Ready(resp),
142+
None => {
143+
let req = self.client.get(self.url.clone(), None).send();
144+
ResponseState::Pending(Box::pin(req))
129145
}
130-
}
146+
},
147+
file_id: self.file_id.clone(),
131148
}
132149
}
133150
}
134151
struct SeqRequestStream<Client: HttpClient + 'static> {
135152
state: ResponseState<Client>,
153+
file_id: FileId,
136154
}
137155
impl<Client: HttpClient> Stream for SeqRequestStream<Client> {
138156
type Item = Result<Bytes, HttpError<Client>>;
@@ -143,8 +161,16 @@ impl<Client: HttpClient> Stream for SeqRequestStream<Client> {
143161
return match resp.try_poll_unpin(cx) {
144162
Poll::Ready(resp) => match resp {
145163
Ok(resp) => {
146-
self.state = ResponseState::Ready(resp);
147-
self.poll_next(cx)
164+
let etag = resp.headers().get("etag").ok();
165+
let last_modified = resp.headers().get("last-modified").ok();
166+
let new_file_id = FileId::new(etag, last_modified);
167+
if new_file_id != self.file_id {
168+
self.state = ResponseState::None;
169+
Poll::Ready(Some(Err(HttpError::MismatchedBody(new_file_id))))
170+
} else {
171+
self.state = ResponseState::Ready(resp);
172+
self.poll_next(cx)
173+
}
148174
}
149175
Err(e) => {
150176
self.state = ResponseState::None;

crates/fast-down/src/reqwest/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ pub enum ReqwestGetHeaderError {
9090
#[cfg(test)]
9191
mod tests {
9292
use super::*;
93-
use crate::http::{HttpError, HttpPuller, Prefetch};
93+
use crate::{
94+
http::{HttpError, HttpPuller, Prefetch},
95+
url_info::FileId,
96+
};
9497
use fast_pull::{
9598
Event, MergeProgress,
9699
mem::MemPusher,
@@ -185,6 +188,9 @@ mod tests {
185188
HttpError::Chunk(_) => unreachable!(),
186189
HttpError::GetHeader(_) => unreachable!(),
187190
HttpError::Irrecoverable => unreachable!(),
191+
HttpError::MismatchedBody(file_id) => {
192+
unreachable!("404 status code should not return mismatched body: {file_id:?}")
193+
}
188194
},
189195
}
190196
mock1.assert_async().await;
@@ -227,6 +233,7 @@ mod tests {
227233
format!("{}/concurrent", server.url()).parse().unwrap(),
228234
Client::new(),
229235
None,
236+
FileId::empty(),
230237
);
231238
let pusher = MemPusher::with_capacity(mock_data.len());
232239
#[allow(clippy::single_range_in_vec_init)]
@@ -280,6 +287,7 @@ mod tests {
280287
format!("{}/sequential", server.url()).parse().unwrap(),
281288
Client::new(),
282289
None,
290+
FileId::empty(),
283291
);
284292
let pusher = MemPusher::with_capacity(mock_data.len());
285293
#[allow(clippy::single_range_in_vec_init)]

crates/fast-down/src/url_info.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::sync::Arc;
12
use url::Url;
23

34
#[derive(Debug)]
@@ -7,6 +8,26 @@ pub struct UrlInfo {
78
pub supports_range: bool,
89
pub fast_download: bool,
910
pub final_url: Url,
10-
pub etag: Option<String>,
11-
pub last_modified: Option<String>,
11+
pub file_id: FileId,
12+
}
13+
14+
#[derive(Debug, Clone, PartialEq, Eq)]
15+
pub struct FileId {
16+
pub etag: Option<Arc<str>>,
17+
pub last_modified: Option<Arc<str>>,
18+
}
19+
20+
impl FileId {
21+
pub fn new(etag: Option<&str>, last_modified: Option<&str>) -> Self {
22+
Self {
23+
etag: etag.map(Arc::from),
24+
last_modified: last_modified.map(Arc::from),
25+
}
26+
}
27+
pub fn empty() -> Self {
28+
Self {
29+
etag: None,
30+
last_modified: None,
31+
}
32+
}
1233
}

0 commit comments

Comments
 (0)