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.
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}