1use crate::chain::FilecoinSnapshotVersion;
5use crate::chain_sync::chain_muxer::DEFAULT_RECENT_STATE_ROOTS;
6use crate::cli_shared::snapshot::{self, TrustedVendor};
7use crate::db::car::forest::tmp_exporting_forest_car_path;
8use crate::ipld::ChainExportState;
9use crate::networks::calibnet;
10use crate::prelude::*;
11use crate::rpc::chain::ForestChainExportDiffParams;
12use crate::rpc::types::ApiExportResult;
13use crate::rpc::{self, chain::ForestChainExportParams, prelude::*};
14use crate::shim::policy::policy_constants::CHAIN_FINALITY;
15use chrono::DateTime;
16use clap::Subcommand;
17use indicatif::{ProgressBar, ProgressStyle};
18use std::{path::PathBuf, time::Duration};
19use tokio_util::sync::CancellationToken;
20
21#[derive(Debug, Clone, clap::ValueEnum)]
22pub enum Format {
23 Json,
24 Text,
25}
26
27#[derive(Debug, Subcommand)]
28pub enum SnapshotCommands {
29 Export {
31 #[arg(short, long, default_value = ".", verbatim_doc_comment)]
33 output_path: PathBuf,
34 #[arg(long)]
36 skip_checksum: bool,
37 #[arg(long)]
39 dry_run: bool,
40 #[arg(short, long)]
42 tipset: Option<i64>,
43 #[arg(short, long, default_value_t = DEFAULT_RECENT_STATE_ROOTS)]
45 depth: crate::chain::ChainEpochDelta,
46 #[arg(long, value_enum, default_value_t = FilecoinSnapshotVersion::V2)]
48 format: FilecoinSnapshotVersion,
49 #[arg(long)]
51 augmented_snapshot: bool,
52 #[arg(long)]
54 tipset_lookup: bool,
55 },
56 ExportStatus {
58 #[arg(long)]
60 wait: bool,
61 #[arg(long, value_enum, default_value_t = Format::Text)]
63 format: Format,
64 },
65 ExportCancel {},
67 ExportDiff {
69 #[arg(short, long, default_value = ".", verbatim_doc_comment)]
71 output_path: PathBuf,
72 #[arg(long)]
74 from: i64,
75 #[arg(long)]
77 to: i64,
78 #[arg(short, long)]
80 depth: Option<crate::chain::ChainEpochDelta>,
81 },
82}
83
84impl SnapshotCommands {
85 pub async fn run(self, client: rpc::Client) -> anyhow::Result<()> {
86 match self {
87 Self::Export {
88 output_path,
89 skip_checksum,
90 dry_run,
91 tipset,
92 depth,
93 format,
94 augmented_snapshot,
95 tipset_lookup,
96 } => {
97 anyhow::ensure!(
98 depth >= 0,
99 "--depth must be non-negative; use 0 for spine-only snapshots"
100 );
101
102 if depth < CHAIN_FINALITY {
103 tracing::warn!(
104 "Depth {depth} should be no less than CHAIN_FINALITY {CHAIN_FINALITY} to export a valid lite snapshot"
105 );
106 }
107
108 let raw_network_name = StateNetworkName::call(&client, ()).await?;
109 let chain_name = if raw_network_name == calibnet::NETWORK_GENESIS_NAME {
112 calibnet::NETWORK_COMMON_NAME
113 } else {
114 raw_network_name.as_str()
115 };
116
117 let tipset = if let Some(epoch) = tipset {
118 client
120 .call(
121 ChainGetTipSetByHeight::request((epoch, Default::default()))?
122 .with_timeout(Duration::from_secs(60 * 15)),
123 )
124 .await?
125 } else {
126 ChainHead::call(&client, ()).await?
127 };
128
129 let output_path = std::path::absolute(match output_path.is_dir() {
130 true => output_path.join(snapshot::filename(
131 TrustedVendor::Forest,
132 chain_name,
133 DateTime::from_timestamp(tipset.min_ticket_block().timestamp as i64, 0)
134 .unwrap_or_default()
135 .naive_utc()
136 .date(),
137 tipset.epoch(),
138 true,
139 )),
140 false => output_path.clone(),
141 })
142 .context("failed to make output path absolute")?;
143
144 let params = ForestChainExportParams {
145 version: format,
146 epoch: tipset.epoch(),
147 recent_roots: depth,
148 output_path: output_path.clone(),
149 tipset_keys: tipset.key().clone().into(),
150 include_receipts: false,
151 include_events: false,
152 include_tipset_keys: false,
153 augmented_snapshot,
154 tipset_lookup,
155 skip_checksum,
156 dry_run,
157 };
158
159 let pb = ProgressBar::new_spinner().with_style(
160 ProgressStyle::with_template(
161 "{spinner} {msg} {binary_total_bytes} written in {elapsed} ({binary_bytes_per_sec})",
162 )
163 .expect("indicatif template must be valid"),
164 ).with_message(format!("Exporting v{} snapshot to {} ...", format as u64, output_path.display()));
165 pb.enable_steady_tick(std::time::Duration::from_millis(80));
166 let handle = tokio::spawn({
167 let path = tmp_exporting_forest_car_path(&output_path);
168 let pb = pb.clone();
169 let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
170 async move {
171 loop {
172 interval.tick().await;
173 if let Ok(meta) = std::fs::metadata(&path) {
174 pb.set_position(meta.len());
175 }
176 }
177 }
178 });
179
180 let export_result = client
183 .call(ForestChainExport::request((params,))?.with_timeout(Duration::MAX))
184 .await?;
185
186 handle.abort();
187 pb.finish();
188 _ = handle.await;
189
190 match export_result {
191 ApiExportResult::Done => {
192 println!("Export completed.");
193 }
194 ApiExportResult::Cancelled => {
195 println!("Export cancelled.");
196 }
197 }
198 Ok(())
199 }
200 Self::ExportStatus { wait, format } => {
201 let result = client
202 .call(
203 ForestChainExportStatus::request(())?.with_timeout(Duration::from_secs(30)),
204 )
205 .await?;
206 if !wait || result.state != ChainExportState::Running {
209 match format {
210 Format::Text => println!("{result}"),
211 Format::Json => println!("{}", serde_json::to_string_pretty(&result)?),
212 }
213 return Ok(());
214 }
215 let watched_start_time = result.start_time;
216 let elapsed = chrono::Utc::now()
217 .signed_duration_since(watched_start_time.unwrap_or_default())
218 .to_std()
219 .unwrap_or(Duration::ZERO);
220 let pb = ProgressBar::new(10000)
221 .with_elapsed(elapsed)
222 .with_message("Exporting");
223 pb.set_style(
224 ProgressStyle::with_template(
225 "[{elapsed_precise}] [{wide_bar}] {percent}% {msg} ",
226 )
227 .expect("indicatif template must be valid")
228 .progress_chars("#>-"),
229 );
230 let last = loop {
231 let result = client
232 .call(
233 ForestChainExportStatus::request(())?
234 .with_timeout(Duration::from_secs(30)),
235 )
236 .await?;
237 if result.start_time != watched_start_time {
238 pb.abandon_with_message("Export ended; another export has taken its place");
241 return Ok(());
242 }
243 let position = (result.progress.clamp(0.0, 1.0) * 10000.0).trunc() as u64;
244 pb.set_position(position);
245
246 if result.state != ChainExportState::Running {
247 break result;
248 }
249 tokio::time::sleep(Duration::from_millis(500)).await;
250 };
251 match last.state {
252 ChainExportState::Succeeded => pb.finish_with_message("Export completed"),
253 ChainExportState::Cancelled => pb.abandon_with_message("Export cancelled"),
254 _ => {
255 pb.abandon_with_message("Export failed");
256 anyhow::bail!(
257 "export failed: {}",
258 last.error.as_deref().unwrap_or("unknown error")
259 );
260 }
261 }
262
263 Ok(())
264 }
265 Self::ExportCancel {} => {
266 let result = client
267 .call(
268 ForestChainExportCancel::request(())?.with_timeout(Duration::from_secs(30)),
269 )
270 .await?;
271 if result {
272 println!("Export cancelled.");
273 } else {
274 println!("No export in progress to cancel.");
275 }
276 Ok(())
277 }
278 Self::ExportDiff {
279 output_path,
280 from,
281 to,
282 depth,
283 } => {
284 let raw_network_name = StateNetworkName::call(&client, ()).await?;
285
286 let chain_name = if raw_network_name == calibnet::NETWORK_GENESIS_NAME {
289 calibnet::NETWORK_COMMON_NAME
290 } else {
291 raw_network_name.as_str()
292 };
293
294 let depth = depth.unwrap_or_else(|| from - to);
295 anyhow::ensure!(depth > 0, "depth must be positive");
296
297 let output_path = std::path::absolute(match output_path.is_dir() {
298 true => output_path.join(format!(
299 "forest_snapshot_diff_{chain_name}_{from}_{to}+{depth}.car.zst"
300 )),
301 false => output_path.clone(),
302 })
303 .context("failed to make output path absolute")?;
304
305 let params = ForestChainExportDiffParams {
306 output_path: output_path.clone(),
307 from,
308 to,
309 depth,
310 };
311
312 let pb = ProgressBar::new_spinner().with_style(
313 ProgressStyle::with_template(
314 "{spinner} {msg} {binary_total_bytes} written in {elapsed} ({binary_bytes_per_sec})",
315 )
316 .expect("indicatif template must be valid"),
317 ).with_message(format!("Exporting {} ...", output_path.display()));
318 pb.enable_steady_tick(std::time::Duration::from_millis(80));
319 let cancellation_token = CancellationToken::new();
320 let _cancellation_token_drop_guard = cancellation_token.drop_guard_ref();
322 let handle = tokio::spawn({
323 let cancellation_token = cancellation_token.clone();
324 let path = tmp_exporting_forest_car_path(&output_path);
325 let pb = pb.clone();
326 let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
327 async move {
328 while !cancellation_token.is_cancelled() {
329 interval.tick().await;
330 if let Ok(meta) = std::fs::metadata(&path) {
331 pb.set_position(meta.len());
332 }
333 }
334 }
335 });
336 let export_result = client
339 .call(ForestChainExportDiff::request((params,))?.with_timeout(Duration::MAX))
340 .await?;
341 cancellation_token.cancel();
343 pb.finish();
344 _ = handle.await;
345
346 match export_result {
347 ApiExportResult::Done => {
348 println!("Export completed.");
349 }
350 ApiExportResult::Cancelled => {
351 println!("Export cancelled.");
352 }
353 }
354 Ok(())
355 }
356 }
357 }
358}