Skip to content

Commit e6aeba0

Browse files
authored
feat: support custom Download command defaults (#19437)
1 parent dee0eca commit e6aeba0

File tree

2 files changed

+164
-18
lines changed

2 files changed

+164
-18
lines changed

crates/cli/commands/src/download.rs

Lines changed: 163 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ use reth_chainspec::{EthChainSpec, EthereumHardforks};
77
use reth_cli::chainspec::ChainSpecParser;
88
use reth_fs_util as fs;
99
use std::{
10+
borrow::Cow,
1011
io::{self, Read, Write},
1112
path::Path,
12-
sync::Arc,
13+
sync::{Arc, OnceLock},
1314
time::{Duration, Instant},
1415
};
1516
use tar::Archive;
@@ -22,24 +23,109 @@ const MERKLE_BASE_URL: &str = "https://downloads.merkle.io";
2223
const EXTENSION_TAR_LZ4: &str = ".tar.lz4";
2324
const EXTENSION_TAR_ZSTD: &str = ".tar.zst";
2425

26+
/// Global static download defaults
27+
static DOWNLOAD_DEFAULTS: OnceLock<DownloadDefaults> = OnceLock::new();
28+
29+
/// Download configuration defaults
30+
///
31+
/// Global defaults can be set via [`DownloadDefaults::try_init`].
32+
#[derive(Debug, Clone)]
33+
pub struct DownloadDefaults {
34+
/// List of available snapshot sources
35+
pub available_snapshots: Vec<Cow<'static, str>>,
36+
/// Default base URL for snapshots
37+
pub default_base_url: Cow<'static, str>,
38+
/// Optional custom long help text that overrides the generated help
39+
pub long_help: Option<String>,
40+
}
41+
42+
impl DownloadDefaults {
43+
/// Initialize the global download defaults with this configuration
44+
pub fn try_init(self) -> Result<(), Self> {
45+
DOWNLOAD_DEFAULTS.set(self)
46+
}
47+
48+
/// Get a reference to the global download defaults
49+
pub fn get_global() -> &'static DownloadDefaults {
50+
DOWNLOAD_DEFAULTS.get_or_init(DownloadDefaults::default_download_defaults)
51+
}
52+
53+
/// Default download configuration with defaults from merkle.io and publicnode
54+
pub fn default_download_defaults() -> Self {
55+
Self {
56+
available_snapshots: vec![
57+
Cow::Borrowed("https://www.merkle.io/snapshots (default, mainnet archive)"),
58+
Cow::Borrowed("https://publicnode.com/snapshots (full nodes & testnets)"),
59+
],
60+
default_base_url: Cow::Borrowed(MERKLE_BASE_URL),
61+
long_help: None,
62+
}
63+
}
64+
65+
/// Generates the long help text for the download URL argument using these defaults.
66+
///
67+
/// If a custom long_help is set, it will be returned. Otherwise, help text is generated
68+
/// from the available_snapshots list.
69+
pub fn long_help(&self) -> String {
70+
if let Some(ref custom_help) = self.long_help {
71+
return custom_help.clone();
72+
}
73+
74+
let mut help = String::from(
75+
"Specify a snapshot URL or let the command propose a default one.\n\nAvailable snapshot sources:\n",
76+
);
77+
78+
for source in &self.available_snapshots {
79+
help.push_str("- ");
80+
help.push_str(source);
81+
help.push('\n');
82+
}
83+
84+
help.push_str(
85+
"\nIf no URL is provided, the latest mainnet archive snapshot\nwill be proposed for download from ",
86+
);
87+
help.push_str(self.default_base_url.as_ref());
88+
help
89+
}
90+
91+
/// Add a snapshot source to the list
92+
pub fn with_snapshot(mut self, source: impl Into<Cow<'static, str>>) -> Self {
93+
self.available_snapshots.push(source.into());
94+
self
95+
}
96+
97+
/// Replace all snapshot sources
98+
pub fn with_snapshots(mut self, sources: Vec<Cow<'static, str>>) -> Self {
99+
self.available_snapshots = sources;
100+
self
101+
}
102+
103+
/// Set the default base URL, e.g. `https://downloads.merkle.io`.
104+
pub fn with_base_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
105+
self.default_base_url = url.into();
106+
self
107+
}
108+
109+
/// Builder: Set custom long help text, overriding the generated help
110+
pub fn with_long_help(mut self, help: impl Into<String>) -> Self {
111+
self.long_help = Some(help.into());
112+
self
113+
}
114+
}
115+
116+
impl Default for DownloadDefaults {
117+
fn default() -> Self {
118+
Self::default_download_defaults()
119+
}
120+
}
121+
25122
#[derive(Debug, Parser)]
26123
pub struct DownloadCommand<C: ChainSpecParser> {
27124
#[command(flatten)]
28125
env: EnvironmentArgs<C>,
29126

30-
#[arg(
31-
long,
32-
short,
33-
help = "Custom URL to download the snapshot from",
34-
long_help = "Specify a snapshot URL or let the command propose a default one.\n\
35-
\n\
36-
Available snapshot sources:\n\
37-
- https://www.merkle.io/snapshots (default, mainnet archive)\n\
38-
- https://publicnode.com/snapshots (full nodes & testnets)\n\
39-
\n\
40-
If no URL is provided, the latest mainnet archive snapshot\n\
41-
will be proposed for download from merkle.io"
42-
)]
127+
/// Custom URL to download the snapshot from
128+
#[arg(long, short, long_help = DownloadDefaults::get_global().long_help())]
43129
url: Option<String>,
44130
}
45131

@@ -207,9 +293,10 @@ async fn stream_and_extract(url: &str, target_dir: &Path) -> Result<()> {
207293
Ok(())
208294
}
209295

210-
// Builds default URL for latest mainnet archive snapshot
296+
// Builds default URL for latest mainnet archive snapshot using configured defaults
211297
async fn get_latest_snapshot_url() -> Result<String> {
212-
let latest_url = format!("{MERKLE_BASE_URL}/latest.txt");
298+
let base_url = &DownloadDefaults::get_global().default_base_url;
299+
let latest_url = format!("{base_url}/latest.txt");
213300
let filename = Client::new()
214301
.get(latest_url)
215302
.send()
@@ -220,5 +307,64 @@ async fn get_latest_snapshot_url() -> Result<String> {
220307
.trim()
221308
.to_string();
222309

223-
Ok(format!("{MERKLE_BASE_URL}/{filename}"))
310+
Ok(format!("{base_url}/{filename}"))
311+
}
312+
313+
#[cfg(test)]
314+
mod tests {
315+
use super::*;
316+
317+
#[test]
318+
fn test_download_defaults_builder() {
319+
let defaults = DownloadDefaults::default()
320+
.with_snapshot("https://example.com/snapshots (example)")
321+
.with_base_url("https://example.com");
322+
323+
assert_eq!(defaults.default_base_url, "https://example.com");
324+
assert_eq!(defaults.available_snapshots.len(), 3); // 2 defaults + 1 added
325+
}
326+
327+
#[test]
328+
fn test_download_defaults_replace_snapshots() {
329+
let defaults = DownloadDefaults::default().with_snapshots(vec![
330+
Cow::Borrowed("https://custom1.com"),
331+
Cow::Borrowed("https://custom2.com"),
332+
]);
333+
334+
assert_eq!(defaults.available_snapshots.len(), 2);
335+
assert_eq!(defaults.available_snapshots[0], "https://custom1.com");
336+
}
337+
338+
#[test]
339+
fn test_long_help_generation() {
340+
let defaults = DownloadDefaults::default();
341+
let help = defaults.long_help();
342+
343+
assert!(help.contains("Available snapshot sources:"));
344+
assert!(help.contains("merkle.io"));
345+
assert!(help.contains("publicnode.com"));
346+
}
347+
348+
#[test]
349+
fn test_long_help_override() {
350+
let custom_help = "This is custom help text for downloading snapshots.";
351+
let defaults = DownloadDefaults::default().with_long_help(custom_help);
352+
353+
let help = defaults.long_help();
354+
assert_eq!(help, custom_help);
355+
assert!(!help.contains("Available snapshot sources:"));
356+
}
357+
358+
#[test]
359+
fn test_builder_chaining() {
360+
let defaults = DownloadDefaults::default()
361+
.with_base_url("https://custom.example.com")
362+
.with_snapshot("https://snapshot1.com")
363+
.with_snapshot("https://snapshot2.com")
364+
.with_long_help("Custom help for snapshots");
365+
366+
assert_eq!(defaults.default_base_url, "https://custom.example.com");
367+
assert_eq!(defaults.available_snapshots.len(), 4); // 2 defaults + 2 added
368+
assert_eq!(defaults.long_help, Some("Custom help for snapshots".to_string()));
369+
}
224370
}

docs/vocs/docs/pages/cli/reth/download.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Database:
8181
- https://publicnode.com/snapshots (full nodes & testnets)
8282
8383
If no URL is provided, the latest mainnet archive snapshot
84-
will be proposed for download from merkle.io
84+
will be proposed for download from https://downloads.merkle.io
8585
8686
Logging:
8787
--log.stdout.format <FORMAT>

0 commit comments

Comments
 (0)