decdn_common/cli/get.rs
1//! Arguments for `decdn get <URI>` (#2449): one command for a blob, a whole
2//! bundle, or one bundle entry, named by a `decdn:` URI
3//! (`adr/appendix-bundles.md` ยง `decdn:` URIs).
4//!
5//! The URI stays a `String` here: `decdn-common` does not depend on
6//! `decdn-client`, which owns the `DecdnUri` parser. The `cli` binary parses it
7//! and routes the target to the blob fetch or the bundle pull.
8
9use std::path::PathBuf;
10
11use clap::Args;
12
13use super::fetch::ClientFetchArgs;
14
15/// The default `--jobs` of a bundle pull.
16pub const DEFAULT_JOBS: usize = 4;
17
18/// The default `--max-lane-streams` of a bundle pull.
19pub const DEFAULT_MAX_LANE_STREAMS: u16 = 4;
20
21/// `decdn get` โ pull what a `decdn:` URI names over the paid `cdn/client/v1`
22/// path.
23///
24/// - `decdn:<hash>`: the raw blob. `-o` is the destination file, or `-` for
25/// stdout.
26/// - `decdn:<hash>/`: every entry of the bundle whose manifest has this hash.
27/// `-o` is the output directory.
28/// - `decdn:<hash>/<path>`: that one bundle entry. `-o` is the output
29/// directory, and the file lands at `<OUT>/<path>`.
30///
31/// A `?ns=<id>` in the URI is the namespace routing hint (ADR 002) for every
32/// request of the run: the manifest blob and each entry. The bundle-only flags
33/// are an error on a blob URI, and the entry filters (`--include`,
34/// `--exclude`, `--select`) are an error on an entry URI.
35#[derive(Args, Debug)]
36#[expect(
37 clippy::struct_excessive_bools,
38 reason = "independent CLI flags (--overwrite/--dry-run/--json/--select), not a state machine"
39)]
40pub struct GetArgs {
41 /// The `decdn:` URI to pull: `decdn:<hash>[/<path>][?ns=<id>]`.
42 #[arg(value_name = "URI")]
43 pub uri: String,
44
45 /// The destination. For a blob URI, the file to write (atomically: temp
46 /// in the same directory, then rename), or `-` to stream verified bytes to
47 /// stdout. For a bundle or entry URI, the output directory (created if
48 /// absent); each entry's relative path resolves within it, and
49 /// `..`/absolute/escaping paths are rejected.
50 #[arg(short = 'o', long, value_name = "PATH")]
51 pub output: PathBuf,
52
53 /// Bundle only: maximum concurrent entry fetches across the whole run
54 /// [default: 4]. Each distinct blob is one unit of work holding one slot:
55 /// a plain whole-file entry, or a range-dedup entry across its complement
56 /// drive, donor splice, and any re-fetch. A range-dedup entry that only
57 /// waits for a sibling to finish the chunks it splices gives its slot to a
58 /// queued entry until then. A byte range a sibling entry already holds is
59 /// spliced from disk, not fetched, so it never takes a slot of its own.
60 #[arg(long, value_name = "N")]
61 pub jobs: Option<usize>,
62
63 /// Bundle only: maximum concurrent streams to one `(pool, signer,
64 /// provider)` lane [default: 4]. Same-lane streams share one cumulative
65 /// voucher watermark, and the serving node credits each stream's delivered
66 /// bytes from that shared lane fairly (a slow stream is paid from the
67 /// headroom a faster sibling opened), so same-lane concurrency is safe and
68 /// this only bounds how many streams touch one provider at once.
69 /// Cross-lane parallelism (distinct providers) is bounded by `--jobs`
70 /// regardless.
71 ///
72 /// One connection per node carries every stream of the command, so the
73 /// bound (1 to 64) stays below the node's limit of concurrent streams on
74 /// one connection.
75 #[arg(long, value_name = "N", value_parser = clap::value_parser!(u16).range(1..=64))]
76 pub max_lane_streams: Option<u16>,
77
78 /// Whole-bundle only: pull only entries whose POSIX relative path matches
79 /// one of these globs (`models/*.bin`), matched against the manifest's
80 /// `path` field, never the on-disk absolute path. Repeatable: an entry
81 /// passes the include gate if it matches any one. Absent => every entry
82 /// passes the gate. gitignore semantics: `*` does not cross `/`, `**`
83 /// recurses.
84 #[arg(long = "include", value_name = "GLOB", action = clap::ArgAction::Append)]
85 pub include: Vec<String>,
86
87 /// Whole-bundle only: skip entries whose POSIX relative path matches one
88 /// of these globs, matched against the manifest's `path` field.
89 /// Repeatable: every pattern is OR-ed. `--exclude` wins over `--include`:
90 /// an entry matching both is skipped. Same gitignore glob semantics as
91 /// `--include`.
92 #[arg(long = "exclude", value_name = "GLOB", action = clap::ArgAction::Append)]
93 pub exclude: Vec<String>,
94
95 /// Whole-bundle only: choose which files to pull interactively. Opens the
96 /// (post-`--include`/`--exclude`) file list in `$VISUAL`/`$EDITOR`, one
97 /// `path` per line. Comment out (prefix `#`) or delete any line to skip
98 /// that file; save and exit to pull the rest. Requires an interactive
99 /// terminal; incompatible with `--json` and `--dry-run`.
100 #[arg(long)]
101 pub select: bool,
102
103 /// Bundle only: re-fetch and overwrite entries whose destination file
104 /// already exists. Default is skip-existing (resume-friendly): a path
105 /// skips when its saved `.decdn-manifest.json` record still matches the
106 /// file's hash, size, and mtime โ the fast path, which trusts that gate
107 /// without re-hashing โ or, failing that, when re-hashing its on-disk
108 /// bytes matches the manifest. A file byte-identical to one already
109 /// anywhere else in the output root is likewise reused with no download:
110 /// it is linked (or copied) from that on-disk file once its hash is
111 /// confirmed by a re-hash. Use `--overwrite` to force a full re-fetch of
112 /// every entry, for example when the output directory may have been
113 /// modified outside `decdn` (an in-place edit that keeps a file's size and
114 /// mtime is otherwise trusted by the fast path). It skips consulting
115 /// `.decdn-manifest.json` for skip decisions, so it disables both the
116 /// skip-existing path and the whole-root reuse above; the file is still
117 /// read and rewritten with the run's results. It also fetches the
118 /// manifest again instead of reading the output root's cached copy.
119 #[arg(long)]
120 pub overwrite: bool,
121
122 /// Bundle only: print the entries the run would pull (paths + sizes, after
123 /// the entry filters; files already present are still listed) and exit
124 /// without any network or chain activity. Entries are listed only when an
125 /// earlier pull into the same output directory cached the bundle manifest.
126 #[arg(long)]
127 pub dry_run: bool,
128
129 /// Bundle only: emit a one-line JSON summary instead of human-readable
130 /// output.
131 #[arg(long)]
132 pub json: bool,
133
134 /// Retrieval flags: peer discovery, chain and payment-pool coordinates,
135 /// the source cap and the give-up limit.
136 #[command(flatten)]
137 pub common: ClientFetchArgs,
138}