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