Skip to main content

decdn_common/cli/
origin.rs

1//! Arguments for the `decdn origin` command group (issue #1904).
2//!
3//! `origin import` seeds a cache **origin store** with local content: for each
4//! blob it writes the sharded data object `{hex[0..2]}/{hex}` and its sibling
5//! pre-order bao outboard `{hex}.obao4` (per ADR 037 §Origin-tier pull-through),
6//! the exact layout the daemon reads back. It is offline, config-free local
7//! work — no running daemon, no `node.toml`, no chain — so it lives in its own
8//! group rather than under `decdn node` (whose every subcommand talks to a
9//! running node over the loopback admin surface, `appendix-local-admin-http.md`).
10//!
11//! `--to` is a local filesystem directory: the store is written to disk and
12//! nowhere else. An S3 origin needs no separate writer, because the fs and S3
13//! object layouts are byte-identical — import to a directory and `aws s3 sync`
14//! it up to the bucket.
15
16use std::path::PathBuf;
17
18use clap::{Args, Subcommand};
19
20/// Top-level `decdn origin` group.
21#[derive(Args, Debug)]
22pub struct OriginArgs {
23    /// The `decdn origin` subcommand to run.
24    #[command(subcommand)]
25    pub cmd: OriginCommand,
26}
27
28/// Subcommands under `decdn origin`.
29#[derive(Subcommand, Debug)]
30pub enum OriginCommand {
31    /// Import local content into a cache origin store, writing each blob's
32    /// sharded data object and its `{hex}.obao4` outboard in the layout the
33    /// daemon reads. A directory is walked, every regular file imported, and a
34    /// canonical bundle manifest emitted (with `--dry-run`, printed to stdout
35    /// instead) and itself imported, so the whole tree is retrievable by one
36    /// bundle hash.
37    Import(OriginImportArgs),
38}
39
40/// `decdn origin import` — seed a cache origin store from a local file or tree.
41// The flags (`--move`, `--force`, `--follow-symlinks`, `--json`) are independent
42// operator toggles on one offline command, not a state machine — a bitflags or
43// two-variant-enum refactor would only obscure the clap surface.
44#[expect(
45    clippy::struct_excessive_bools,
46    reason = "independent CLI flags, not modelled state"
47)]
48#[derive(Args, Debug)]
49pub struct OriginImportArgs {
50    /// Local file or directory to import. A directory is walked recursively
51    /// (see `--follow-symlinks` / `--exclude`); every regular file becomes a
52    /// content-addressed blob in the target.
53    #[arg(short = 'i', long, value_name = "PATH")]
54    pub input: PathBuf,
55
56    /// Directory to populate — a local filesystem origin store rooted here,
57    /// created if absent. This is the only write backend; pass the path
58    /// directly (`--to /var/lib/decdn/origin`), with no scheme prefix.
59    ///
60    /// To populate an S3 origin, import to a local directory and then
61    /// `aws s3 sync <dir> s3://<bucket>/<prefix>`: the fs and S3 object layouts
62    /// are byte-identical, so the sync needs no renaming. An `s3://` or
63    /// `http(s)://` value is rejected with that recipe rather than treated as a
64    /// path.
65    ///
66    /// Required unless `--dry-run`.
67    ///
68    /// A `PathBuf`, not a `String`, so a non-UTF-8 directory path is accepted
69    /// on Unix like `--input` is. Prefix sniffing (`s3://` / `http(s)://` /
70    /// `fs:`) runs on the UTF-8 view; a non-UTF-8 path cannot be one of those,
71    /// so it is unambiguously a filesystem directory.
72    #[arg(long, value_name = "DIR")]
73    pub to: Option<PathBuf>,
74
75    /// Move each source file into the target instead of copying it. Across
76    /// filesystems this falls back to copy-then-unlink. A source whose content
77    /// is already present in the target is removed too (the blob is there).
78    #[arg(long = "move")]
79    pub move_source: bool,
80
81    /// When importing a directory, also write the canonical bundle manifest to
82    /// this file (in addition to importing the manifest blob into the target).
83    /// Ignored for a single-file import.
84    #[arg(long, value_name = "FILE")]
85    pub bundle: Option<PathBuf>,
86
87    /// Overwrite a target object even when one already exists at its hash. By
88    /// default an existing object is left in place (content-addressed: the same
89    /// hash is the same bytes); a size mismatch is refused unless `--force`.
90    #[arg(long)]
91    pub force: bool,
92
93    /// Follow symlinks during a directory walk. Off by default. Even when on, a
94    /// symlink resolving outside the input root is a hard error: a manifest's
95    /// relative-path field cannot describe an external target.
96    #[arg(long)]
97    pub follow_symlinks: bool,
98
99    /// Glob pattern of paths to exclude during a directory walk, matched against
100    /// the POSIX relative path (`a/b.txt`, never the absolute path). Repeatable
101    /// — every pattern is OR-ed together.
102    #[arg(long = "exclude", value_name = "GLOB", action = clap::ArgAction::Append)]
103    pub exclude: Vec<String>,
104
105    /// Prefix every manifest entry's path with this folder, so a bundle
106    /// `decdn get` materializes the whole bundle under one directory
107    /// (`<out>/<subfolder>/…`) rather than spilling its files directly into the
108    /// output root. Must be a relative POSIX path with `/` separators — `..`,
109    /// absolute paths, root
110    /// prefixes, and backslashes are rejected; nesting (`a/b`) is allowed.
111    /// Applies to a single-file import too (the one entry becomes
112    /// `<subfolder>/<name>`).
113    #[arg(long, value_name = "DIR")]
114    pub subfolder: Option<String>,
115
116    /// Emit a one-line JSON status report instead of a human summary. Written to
117    /// stdout normally; under `--dry-run` it goes to stderr, because stdout then
118    /// carries the canonical manifest bytes. `origin` is the `--to` target, or
119    /// the literal `(dry-run)` when a dry run runs without `--to`.
120    /// Shape:
121    /// `{"imported":<n>,"bytes":<total>,"origin":"<target>","files":{"<path>":"b3:<hex>",...},"bundle_hash":"b3:<hex>"|null,"moved":<bool>,"optimized":<bool>,"chunks_total":<n>,"skipped_symlinks":<n>}`.
122    #[arg(long)]
123    pub json: bool,
124
125    /// Content-defined-chunk each file and record the chunk boundaries as
126    /// `{hash,size}` hints in the manifest entry; each file is still stored
127    /// as one whole-file blob. A pull uses the hints to splice ranges it
128    /// already holds instead of re-fetching them. Off by default (no hints).
129    #[arg(long)]
130    pub optimize: bool,
131
132    /// Target average chunk size (`--optimize` only). The primary dial. Must be
133    /// a power of two, at most 4 MiB (fastcdc's ceiling). Defaults to 4 MiB when
134    /// `--optimize` is set (applied in the import wiring, not by clap, so an
135    /// explicit `--chunk-avg` without `--optimize` is a detectable error).
136    #[arg(long, value_name = "SIZE", value_parser = super::parse_byte_size)]
137    pub chunk_avg: Option<u64>,
138
139    /// Minimum chunk size override (`--optimize` only). Defaults to `avg/4`.
140    /// At least 1 MiB (the payment interval), at most 1 MiB (fastcdc's ceiling).
141    #[arg(long, value_name = "SIZE", value_parser = super::parse_byte_size)]
142    pub chunk_min: Option<u64>,
143
144    /// Maximum chunk size override (`--optimize` only). Defaults to `2*avg`.
145    /// At most 16 MiB (fastcdc's ceiling).
146    #[arg(long, value_name = "SIZE", value_parser = super::parse_byte_size)]
147    pub chunk_max: Option<u64>,
148
149    /// Compute and emit the manifest without writing any blobs. Prints the
150    /// canonical manifest bytes to stdout (status goes to stderr). Makes `--to`
151    /// optional — this is the local "just make me a manifest" path.
152    #[arg(long)]
153    pub dry_run: bool,
154}
155
156#[cfg(test)]
157#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
158mod tests;