Skip to main content

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