reth_cli_commands/download/
source.rs1use super::{
2 download_command, manifest::SnapshotManifest, progress::DownloadProgress, DownloadDefaults,
3};
4use eyre::{Result, WrapErr};
5use reqwest::Client;
6use reth_fs_util as fs;
7use std::path::{Path, PathBuf};
8use tracing::info;
9use url::Url;
10
11#[derive(serde::Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub(crate) struct SnapshotApiEntry {
15 #[serde(deserialize_with = "deserialize_string_or_u64")]
16 chain_id: u64,
17 #[serde(deserialize_with = "deserialize_string_or_u64")]
18 block: u64,
19 #[serde(default)]
20 date: Option<String>,
21 #[serde(default)]
22 profile: Option<String>,
23 metadata_url: String,
24 #[serde(default)]
25 size: u64,
26}
27
28impl SnapshotApiEntry {
29 fn is_modular(&self) -> bool {
31 self.metadata_url.ends_with("manifest.json")
32 }
33}
34
35pub(crate) async fn discover_manifest_url(chain_id: u64) -> Result<String> {
40 let defaults = DownloadDefaults::get_global();
41 let api_url = &*defaults.snapshot_api_url;
42 let snapshot_source_url = defaults.snapshot_source_url();
43
44 info!(target: "reth::cli", %api_url, %chain_id, "Discovering latest snapshot manifest");
45
46 let entries = fetch_snapshot_api_entries(chain_id).await?;
47 let entry =
48 entries.iter().filter(|s| s.is_modular()).max_by_key(|s| s.block).ok_or_else(|| {
49 eyre::eyre!(
50 "No modular snapshot manifest found for chain \
51 {chain_id} at {api_url}\n\n\
52 You can provide a manifest URL directly with --manifest-url, or\n\
53 use a direct snapshot URL with -u from:\n\
54 \t- {}\n\n\
55 Use --list to see all available snapshots.",
56 snapshot_source_url,
57 )
58 })?;
59
60 info!(target: "reth::cli",
61 block = entry.block,
62 url = %entry.metadata_url,
63 "Found latest snapshot manifest"
64 );
65
66 Ok(entry.metadata_url.clone())
67}
68
69fn deserialize_string_or_u64<'de, D>(deserializer: D) -> std::result::Result<u64, D::Error>
71where
72 D: serde::Deserializer<'de>,
73{
74 use serde::Deserialize;
75 let value = serde_json::Value::deserialize(deserializer)?;
76 match &value {
77 serde_json::Value::Number(n) => {
78 n.as_u64().ok_or_else(|| serde::de::Error::custom("expected u64"))
79 }
80 serde_json::Value::String(s) => {
81 s.parse::<u64>().map_err(|_| serde::de::Error::custom("expected numeric string"))
82 }
83 _ => Err(serde::de::Error::custom("expected number or string")),
84 }
85}
86
87pub(crate) async fn fetch_snapshot_api_entries(chain_id: u64) -> Result<Vec<SnapshotApiEntry>> {
89 let api_url = &*DownloadDefaults::get_global().snapshot_api_url;
90
91 let entries: Vec<SnapshotApiEntry> = Client::new()
92 .get(api_url)
93 .send()
94 .await
95 .and_then(|r| r.error_for_status())
96 .wrap_err_with(|| format!("Failed to fetch snapshot listing from {api_url}"))?
97 .json()
98 .await?;
99
100 Ok(entries.into_iter().filter(|entry| entry.chain_id == chain_id).collect())
101}
102
103pub(crate) fn print_snapshot_listing(entries: &[SnapshotApiEntry], chain_id: u64) {
105 let modular: Vec<_> = entries.iter().filter(|entry| entry.is_modular()).collect();
106
107 let snapshot_source_url = DownloadDefaults::get_global().snapshot_source_url();
108 println!("Available snapshots for chain {chain_id} ({}):\n", snapshot_source_url,);
109 println!("{:<12} {:>10} {:<10} {:>10} MANIFEST URL", "DATE", "BLOCK", "PROFILE", "SIZE");
110 println!("{}", "-".repeat(100));
111
112 for entry in &modular {
113 let date = entry.date.as_deref().unwrap_or("-");
114 let profile = entry.profile.as_deref().unwrap_or("-");
115 let size = if entry.size > 0 {
116 DownloadProgress::format_size(entry.size)
117 } else {
118 "-".to_string()
119 };
120
121 println!(
122 "{date:<12} {:>10} {profile:<10} {size:>10} {}",
123 entry.block, entry.metadata_url
124 );
125 }
126
127 if modular.is_empty() {
128 println!(" (no modular snapshots found)");
129 }
130
131 println!(
132 "\nTo download a specific snapshot, copy its manifest URL and run:\n \
133 {} --manifest-url <URL>",
134 download_command()
135 );
136}
137
138pub(crate) async fn fetch_manifest_from_source(source: &str) -> Result<SnapshotManifest> {
140 if let Ok(parsed) = Url::parse(source) {
141 return match parsed.scheme() {
142 "http" | "https" => {
143 let response = Client::new()
144 .get(source)
145 .send()
146 .await
147 .and_then(|r| r.error_for_status())
148 .wrap_err_with(|| {
149 let sources = DownloadDefaults::get_global()
150 .available_snapshots
151 .iter()
152 .map(|snapshot| format!("\t- {snapshot}"))
153 .collect::<Vec<_>>()
154 .join("\n");
155 format!(
156 "Failed to fetch snapshot manifest from {source}\n\n\
157 The manifest endpoint may not be available for this snapshot source.\n\
158 You can use a direct snapshot URL instead:\n\n\
159 \t{} -u <snapshot-url>\n\n\
160 Available snapshot sources:\n{sources}",
161 download_command()
162 )
163 })?;
164 Ok(response.json().await?)
165 }
166 "file" => {
167 let path = parsed
168 .to_file_path()
169 .map_err(|_| eyre::eyre!("Invalid file:// manifest path: {source}"))?;
170 let content = fs::read_to_string(path)?;
171 Ok(serde_json::from_str(&content)?)
172 }
173 _ => Err(eyre::eyre!("Unsupported manifest URL scheme: {}", parsed.scheme())),
174 };
175 }
176
177 let content = fs::read_to_string(source)?;
178 Ok(serde_json::from_str(&content)?)
179}
180
181pub(crate) fn resolve_manifest_base_url(
183 manifest: &SnapshotManifest,
184 source: &str,
185) -> Result<String> {
186 if let Some(base_url) = manifest.base_url.as_deref() &&
187 !base_url.is_empty()
188 {
189 return Ok(base_url.trim_end_matches('/').to_string());
190 }
191
192 if let Ok(mut url) = Url::parse(source) {
193 if url.scheme() == "file" {
194 let mut path = url
195 .to_file_path()
196 .map_err(|_| eyre::eyre!("Invalid file:// manifest path: {source}"))?;
197 path.pop();
198 let mut base = Url::from_directory_path(path)
199 .map_err(|_| eyre::eyre!("Invalid manifest directory for source: {source}"))?
200 .to_string();
201 if base.ends_with('/') {
202 base.pop();
203 }
204 return Ok(base);
205 }
206
207 {
208 let mut segments = url
209 .path_segments_mut()
210 .map_err(|_| eyre::eyre!("manifest_url must have a hierarchical path"))?;
211 segments.pop_if_empty();
212 segments.pop();
213 }
214 return Ok(url.as_str().trim_end_matches('/').to_string());
215 }
216
217 let path = Path::new(source);
218 let manifest_dir = if path.is_absolute() {
219 path.parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."))
220 } else {
221 let joined = std::env::current_dir()?.join(path);
222 joined.parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."))
223 };
224
225 let mut base = Url::from_directory_path(&manifest_dir)
226 .map_err(|_| eyre::eyre!("Invalid manifest directory: {}", manifest_dir.display()))?
227 .to_string();
228 if base.ends_with('/') {
229 base.pop();
230 }
231 Ok(base)
232}