radicle_cli/commands/
seed.rs

1use 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}