1use std::path::PathBuf;
2use std::time::Duration;
3
4use crate::client::{decode_status, SyncClient};
5use crate::config::ClientConfig;
6use crate::logging;
7use crate::protocol;
8use crate::util::{default_reticulum_dir, default_rngit_dir, parse_rns_url_with_aliases};
9use crate::{Error, Result};
10
11pub fn main<I>(args: I) -> Result<()>
12where
13 I: IntoIterator<Item = String>,
14{
15 let options = CreateOptions::parse(args)?;
16 let rngit_dir = options.config_dir.unwrap_or_else(default_rngit_dir);
17 let rns_dir = options.rns_config_dir.or_else(default_reticulum_dir);
18 let mut config = ClientConfig::load_or_create_for_run(rngit_dir, rns_dir)?;
19 logging::init_file_logger(&config.dir.join("client_log"), config.log_level)?;
20 let (dest_hash, repository) =
21 parse_rns_url_with_aliases(&options.remote, &config.destination_aliases)?;
22 if let Some(identity_path) = options.identity_path {
23 config.identity_path = identity_path;
24 }
25
26 let client = SyncClient::connect(config, dest_hash)?;
27 let response = client.request_with_timeout(
28 protocol::PATH_CREATE,
29 protocol::repository_request(&repository),
30 Duration::from_secs(120),
31 )?;
32 let bytes = protocol::response_bin(&response.data)?;
33 decode_status(bytes)?;
34 println!("Repository {repository} created");
35 Ok(())
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39struct CreateOptions {
40 config_dir: Option<PathBuf>,
41 rns_config_dir: Option<PathBuf>,
42 identity_path: Option<PathBuf>,
43 remote: String,
44}
45
46impl CreateOptions {
47 fn parse<I>(args: I) -> Result<Self>
48 where
49 I: IntoIterator<Item = String>,
50 {
51 let mut config_dir = None;
52 let mut rns_config_dir = None;
53 let mut identity_path = None;
54 let mut remote = None;
55 let mut iter = args.into_iter();
56 while let Some(arg) = iter.next() {
57 match arg.as_str() {
58 "--config" => config_dir = Some(next_path(&mut iter, "--config")?),
59 "--rnsconfig" => rns_config_dir = Some(next_path(&mut iter, "--rnsconfig")?),
60 "-i" | "--identity" => identity_path = Some(next_path(&mut iter, "--identity")?),
61 "-h" | "--help" => return Err(Error::msg(usage())),
62 other if other.starts_with('-') => {
63 return Err(Error::msg(format!("unknown create option {other}")));
64 }
65 _ => {
66 if remote.replace(arg).is_some() {
67 return Err(Error::msg("create takes exactly one repository URL"));
68 }
69 }
70 }
71 }
72 Ok(Self {
73 config_dir,
74 rns_config_dir,
75 identity_path,
76 remote: remote.ok_or_else(|| Error::msg(usage()))?,
77 })
78 }
79}
80
81fn next_path(iter: &mut impl Iterator<Item = String>, flag: &str) -> Result<PathBuf> {
82 iter.next()
83 .map(PathBuf::from)
84 .ok_or_else(|| Error::msg(format!("{flag} requires a value")))
85}
86
87fn usage() -> &'static str {
88 "usage: rngit create [--config DIR] [--rnsconfig DIR] [-i|--identity PATH] <rns://destination/group/repo>"
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn parses_create_options() {
97 let opts = CreateOptions::parse([
98 "--config".into(),
99 "cfg".into(),
100 "--rnsconfig".into(),
101 "rns".into(),
102 "--identity".into(),
103 "id".into(),
104 "rns://00112233445566778899aabbccddeeff/group/repo".into(),
105 ])
106 .unwrap();
107 assert_eq!(opts.config_dir, Some(PathBuf::from("cfg")));
108 assert_eq!(opts.rns_config_dir, Some(PathBuf::from("rns")));
109 assert_eq!(opts.identity_path, Some(PathBuf::from("id")));
110 assert_eq!(
111 opts.remote,
112 "rns://00112233445566778899aabbccddeeff/group/repo"
113 );
114 }
115
116 #[test]
117 fn rejects_missing_create_remote() {
118 assert!(CreateOptions::parse(Vec::<String>::new()).is_err());
119 }
120}