radicle_cli/commands/
seed.rs1use std::collections::BTreeSet;
2use std::ffi::OsString;
3use std::time;
4
5use anyhow::anyhow;
6
7use nonempty::NonEmpty;
8use radicle::node::policy;
9use radicle::node::policy::{Policy, Scope};
10use radicle::node::Handle;
11use radicle::{prelude::*, Node};
12use radicle_term::Element as _;
13
14use crate::commands::sync;
15use crate::node::SyncSettings;
16use crate::terminal as term;
17use crate::terminal::args::{Args, Error, Help};
18
19pub const HELP: Help = Help {
20 name: "seed",
21 description: "Manage repository seeding policies",
22 version: env!("RADICLE_VERSION"),
23 usage: r#"
24Usage
25
26 rad seed [<rid>...] [--[no-]fetch] [--from <nid>] [--scope <scope>] [<option>...]
27
28 The `seed` command, when no Repository ID (<rid>) is provided, will list the
29 repositories being seeded.
30
31 When a Repository ID (<rid>) is provided it updates or creates the seeding policy for
32 that repository. To delete a seeding policy, use the `rad unseed` command.
33
34 When seeding a repository, a scope can be specified: this can be either `all` or
35 `followed`. When using `all`, all remote nodes will be followed for that repository.
36 On the other hand, with `followed`, only the repository delegates will be followed,
37 plus any remote that is explicitly followed via `rad follow <nid>`.
38
39Options
40
41 --[no-]fetch Fetch repository after updating seeding policy
42 --from <nid> Fetch from the given node (may be specified multiple times)
43 --timeout <secs> Fetch timeout in seconds (default: 9)
44 --scope <scope> Peer follow scope for this repository
45 --verbose, -v Verbose output
46 --help Print help
47"#,
48};
49
50#[derive(Debug)]
51pub enum Operation {
52 Seed {
53 rids: NonEmpty<RepoId>,
54 fetch: bool,
55 seeds: BTreeSet<NodeId>,
56 timeout: time::Duration,
57 scope: Scope,
58 },
59 List,
60}
61
62#[derive(Debug)]
63pub struct Options {
64 pub op: Operation,
65 pub verbose: bool,
66}
67
68impl Args for Options {
69 fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
70 use lexopt::prelude::*;
71
72 let mut parser = lexopt::Parser::from_args(args);
73 let mut rids: Vec<RepoId> = Vec::new();
74 let mut scope: Option<Scope> = None;
75 let mut fetch: Option<bool> = None;
76 let mut timeout = time::Duration::from_secs(9);
77 let mut seeds: BTreeSet<NodeId> = BTreeSet::new();
78 let mut verbose = false;
79
80 while let Some(arg) = parser.next()? {
81 match &arg {
82 Value(val) => {
83 let rid = term::args::rid(val)?;
84 rids.push(rid);
85 }
86 Long("scope") => {
87 let val = parser.value()?;
88 scope = Some(term::args::parse_value("scope", val)?);
89 }
90 Long("fetch") => {
91 fetch = Some(true);
92 }
93 Long("no-fetch") => {
94 fetch = Some(false);
95 }
96 Long("from") => {
97 let val = parser.value()?;
98 let nid = term::args::nid(&val)?;
99
100 seeds.insert(nid);
101 }
102 Long("timeout") | Short('t') => {
103 let value = parser.value()?;
104 let secs = term::args::parse_value("timeout", value)?;
105
106 timeout = time::Duration::from_secs(secs);
107 }
108 Long("verbose") | Short('v') => verbose = true,
109 Long("help") | Short('h') => {
110 return Err(Error::Help.into());
111 }
112 _ => {
113 return Err(anyhow!(arg.unexpected()));
114 }
115 }
116 }
117
118 let op = match NonEmpty::from_vec(rids) {
119 Some(rids) => Operation::Seed {
120 rids,
121 fetch: fetch.unwrap_or(true),
122 scope: scope.unwrap_or(Scope::All),
123 timeout,
124 seeds,
125 },
126 None => Operation::List,
127 };
128
129 Ok((Options { op, verbose }, vec![]))
130 }
131}
132
133pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
134 let profile = ctx.profile()?;
135 let mut node = radicle::Node::new(profile.socket());
136
137 match options.op {
138 Operation::Seed {
139 rids,
140 fetch,
141 scope,
142 timeout,
143 seeds,
144 } => {
145 for rid in rids {
146 update(rid, scope, &mut node, &profile)?;
147
148 if fetch && node.is_running() {
149 if let Err(e) = sync::fetch(
150 rid,
151 SyncSettings::default()
152 .seeds(seeds.clone())
153 .timeout(timeout)
154 .with_profile(&profile),
155 &mut node,
156 &profile,
157 ) {
158 term::error(e);
159 }
160 }
161 }
162 }
163 Operation::List => seeding(&profile)?,
164 }
165
166 Ok(())
167}
168
169pub fn update(
170 rid: RepoId,
171 scope: Scope,
172 node: &mut Node,
173 profile: &Profile,
174) -> Result<(), anyhow::Error> {
175 let updated = profile.seed(rid, scope, node)?;
176 let outcome = if updated { "updated" } else { "exists" };
177
178 if let Ok(repo) = profile.storage.repository(rid) {
179 if repo.identity_doc()?.is_public() {
180 profile.add_inventory(rid, node)?;
181 term::success!("Inventory updated with {}", term::format::tertiary(rid));
182 }
183 }
184
185 term::success!(
186 "Seeding policy {outcome} for {} with scope '{scope}'",
187 term::format::tertiary(rid),
188 );
189
190 Ok(())
191}
192
193pub fn seeding(profile: &Profile) -> anyhow::Result<()> {
194 let store = profile.policies()?;
195 let storage = &profile.storage;
196 let mut t = term::Table::new(term::table::TableOptions::bordered());
197
198 t.header([
199 term::format::default(String::from("Repository")),
200 term::format::default(String::from("Name")),
201 term::format::default(String::from("Policy")),
202 term::format::default(String::from("Scope")),
203 ]);
204 t.divider();
205
206 for policy in store.seed_policies()? {
207 match policy {
208 Ok(policy::SeedPolicy { rid, policy }) => {
209 let id = rid.to_string();
210 let name = storage
211 .repository(rid)
212 .and_then(|repo| repo.project().map(|proj| proj.name().to_string()))
213 .unwrap_or_default();
214 let scope = policy.scope().unwrap_or_default().to_string();
215 let policy = term::format::policy(&Policy::from(policy));
216
217 t.push([
218 term::format::tertiary(id),
219 name.into(),
220 policy,
221 term::format::dim(scope),
222 ])
223 }
224 Err(err) => {
225 term::error(format!("Failed to read a seeding policy: {err}"));
226 }
227 }
228 }
229
230 if t.is_empty() {
231 term::print(term::format::dim("No seeding policies to show."));
232 } else {
233 t.print();
234 }
235
236 Ok(())
237}