mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
1 Commits
main
...
dan/conten
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bed1464b7f |
@@ -14,7 +14,11 @@ use manifest::{
|
||||
ArchiveDescriptor, ComponentSelection, OutputFileChecksum, SnapshotComponentType,
|
||||
SnapshotManifest,
|
||||
};
|
||||
use reqwest::{blocking::Client as BlockingClient, header::RANGE, Client, StatusCode};
|
||||
use reqwest::{
|
||||
blocking::Client as BlockingClient,
|
||||
header::{CONTENT_RANGE, RANGE},
|
||||
Client, StatusCode,
|
||||
};
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
|
||||
use reth_cli::chainspec::ChainSpecParser;
|
||||
use reth_cli_util::cancellation::CancellationToken;
|
||||
@@ -1086,6 +1090,123 @@ fn extract_from_file(path: &Path, format: CompressionFormat, target_dir: &Path)
|
||||
const MAX_DOWNLOAD_RETRIES: u32 = 10;
|
||||
const RETRY_BACKOFF_SECS: u64 = 5;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ContentRangeHeader {
|
||||
start: u64,
|
||||
end: u64,
|
||||
total_size: u64,
|
||||
}
|
||||
|
||||
impl ContentRangeHeader {
|
||||
fn parse(value: &str) -> Result<Self> {
|
||||
let Some(value) = value.trim().strip_prefix("bytes ") else {
|
||||
eyre::bail!("invalid Content-Range unit: {value}");
|
||||
};
|
||||
|
||||
let Some((range, total_size)) = value.split_once('/') else {
|
||||
eyre::bail!("invalid Content-Range format: {value}");
|
||||
};
|
||||
let Some((start, end)) = range.split_once('-') else {
|
||||
eyre::bail!("invalid Content-Range bounds: {value}");
|
||||
};
|
||||
|
||||
let start = start
|
||||
.parse::<u64>()
|
||||
.wrap_err_with(|| format!("invalid Content-Range start: {value}"))?;
|
||||
let end =
|
||||
end.parse::<u64>().wrap_err_with(|| format!("invalid Content-Range end: {value}"))?;
|
||||
let total_size = total_size
|
||||
.parse::<u64>()
|
||||
.wrap_err_with(|| format!("invalid Content-Range total size: {value}"))?;
|
||||
|
||||
if start > end {
|
||||
eyre::bail!("invalid Content-Range with start > end: {value}");
|
||||
}
|
||||
if end >= total_size {
|
||||
eyre::bail!("invalid Content-Range ending past total size: {value}");
|
||||
}
|
||||
|
||||
Ok(Self { start, end, total_size })
|
||||
}
|
||||
|
||||
const fn content_length(self) -> u64 {
|
||||
self.end - self.start + 1
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_response_content_range(
|
||||
response: &reqwest::blocking::Response,
|
||||
) -> Result<ContentRangeHeader> {
|
||||
let value = response
|
||||
.headers()
|
||||
.get(CONTENT_RANGE)
|
||||
.ok_or_else(|| eyre::eyre!("server returned 206 without Content-Range header"))?
|
||||
.to_str()
|
||||
.wrap_err("server returned non-UTF8 Content-Range header")?;
|
||||
ContentRangeHeader::parse(value)
|
||||
}
|
||||
|
||||
fn validate_content_range(
|
||||
content_range: ContentRangeHeader,
|
||||
expected_start: u64,
|
||||
expected_total_size: Option<u64>,
|
||||
actual_content_length: Option<u64>,
|
||||
) -> Result<()> {
|
||||
if content_range.start != expected_start {
|
||||
eyre::bail!(
|
||||
"server returned Content-Range starting at {} but {} was requested",
|
||||
content_range.start,
|
||||
expected_start
|
||||
);
|
||||
}
|
||||
|
||||
if content_range.end + 1 != content_range.total_size {
|
||||
eyre::bail!(
|
||||
"server returned partial tail {}-{} of {} for an open-ended range request",
|
||||
content_range.start,
|
||||
content_range.end,
|
||||
content_range.total_size
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(expected_total_size) = expected_total_size &&
|
||||
content_range.total_size != expected_total_size
|
||||
{
|
||||
eyre::bail!(
|
||||
"server returned total size {} but {} was expected",
|
||||
content_range.total_size,
|
||||
expected_total_size
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(actual_content_length) = actual_content_length &&
|
||||
content_range.content_length() != actual_content_length
|
||||
{
|
||||
eyre::bail!(
|
||||
"server returned Content-Range length {} but Content-Length was {}",
|
||||
content_range.content_length(),
|
||||
actual_content_length
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_partial_content_response(
|
||||
response: &reqwest::blocking::Response,
|
||||
expected_start: u64,
|
||||
expected_total_size: Option<u64>,
|
||||
) -> Result<ContentRangeHeader> {
|
||||
let content_range = parse_response_content_range(response)?;
|
||||
validate_content_range(
|
||||
content_range,
|
||||
expected_start,
|
||||
expected_total_size,
|
||||
response.content_length(),
|
||||
)?;
|
||||
Ok(content_range)
|
||||
}
|
||||
|
||||
/// Wrapper that tracks download progress while writing data.
|
||||
/// Used with [`io::copy`] to display progress during downloads.
|
||||
struct ProgressWriter<W> {
|
||||
@@ -1229,17 +1350,15 @@ fn resumable_download(
|
||||
|
||||
let is_partial = response.status() == StatusCode::PARTIAL_CONTENT;
|
||||
|
||||
let size = if is_partial {
|
||||
response
|
||||
.headers()
|
||||
.get("Content-Range")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.split('/').next_back())
|
||||
.and_then(|v| v.parse().ok())
|
||||
let content_range = if is_partial {
|
||||
Some(validate_partial_content_response(&response, existing_size, total_size)?)
|
||||
} else {
|
||||
response.content_length()
|
||||
None
|
||||
};
|
||||
|
||||
let size =
|
||||
content_range.map(|range| range.total_size).or_else(|| response.content_length());
|
||||
|
||||
if total_size.is_none() {
|
||||
total_size = size;
|
||||
if !quiet && let Some(s) = size {
|
||||
@@ -1305,6 +1424,23 @@ fn resumable_download(
|
||||
continue;
|
||||
}
|
||||
|
||||
let downloaded_size = fs::metadata(&part_path).map(|m| m.len()).unwrap_or(0);
|
||||
if downloaded_size != current_total {
|
||||
last_error = Some(eyre::eyre!(
|
||||
"download ended with {} bytes but expected {}",
|
||||
downloaded_size,
|
||||
current_total
|
||||
));
|
||||
if attempt < MAX_DOWNLOAD_RETRIES {
|
||||
info!(target: "reth::cli",
|
||||
file = %file_name,
|
||||
"Download ended early, retrying in {RETRY_BACKOFF_SECS}s..."
|
||||
);
|
||||
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return finalize_download(current_total);
|
||||
}
|
||||
|
||||
@@ -2160,4 +2296,50 @@ mod tests {
|
||||
assert_eq!(planned[1].ty, SnapshotComponentType::RocksdbIndices);
|
||||
assert_eq!(planned[2].ty, SnapshotComponentType::Transactions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_range_header_parses_valid_range() {
|
||||
let parsed = ContentRangeHeader::parse("bytes 5-9/10").unwrap();
|
||||
assert_eq!(parsed, ContentRangeHeader { start: 5, end: 9, total_size: 10 });
|
||||
assert_eq!(parsed.content_length(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_content_range_rejects_wrong_start() {
|
||||
let err = validate_content_range(
|
||||
ContentRangeHeader { start: 0, end: 99, total_size: 100 },
|
||||
10,
|
||||
Some(100),
|
||||
Some(100),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("starting at 0 but 10 was requested"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_content_range_rejects_open_ended_range_that_does_not_reach_eof() {
|
||||
let err = validate_content_range(
|
||||
ContentRangeHeader { start: 50, end: 74, total_size: 100 },
|
||||
50,
|
||||
Some(100),
|
||||
Some(25),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("open-ended range request"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_content_range_rejects_content_length_mismatch() {
|
||||
let err = validate_content_range(
|
||||
ContentRangeHeader { start: 10, end: 99, total_size: 100 },
|
||||
10,
|
||||
Some(100),
|
||||
Some(89),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("Content-Range length 90 but Content-Length was 89"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user