use std::collections::BTreeMap;
use std::fs;
use std::io::Read;
use mnem_transport::remote::{RemoteConfigFile, RemoteSection, parse_config, serialize_config};
use super::*;
#[derive(clap::Subcommand, Debug)]
pub(crate) enum RemoteCmd {
Add {
name: String,
url: String,
#[arg(long)]
token_env: Option<String>,
},
List,
Show {
name: String,
},
Remove {
name: String,
},
}
pub(crate) fn run(override_path: Option<&Path>, cmd: RemoteCmd) -> Result<()> {
let data_dir = repo::locate_data_dir(override_path)?;
match cmd {
RemoteCmd::Add {
name,
url,
token_env,
} => add_remote(&data_dir, &name, &url, token_env.as_deref()),
RemoteCmd::List => list_remotes(&data_dir),
RemoteCmd::Show { name } => show_remote(&data_dir, &name),
RemoteCmd::Remove { name } => remove_remote(&data_dir, &name),
}
}
fn load_section(data_dir: &std::path::Path) -> Result<(RemoteSection, String)> {
let path = data_dir.join(config::CONFIG_FILE);
if !path.exists() {
return Ok((RemoteSection::default(), String::new()));
}
let mut s = String::new();
fs::File::open(&path)
.with_context(|| format!("opening {}", path.display()))?
.read_to_string(&mut s)
.with_context(|| format!("reading {}", path.display()))?;
let section = parse_config(&s).with_context(|| format!("parsing {}", path.display()))?;
Ok((section, s))
}
fn save_section(data_dir: &std::path::Path, section: &RemoteSection) -> Result<()> {
let path = data_dir.join(config::CONFIG_FILE);
let mut root: toml::Value = if path.exists() {
let text =
fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
toml::from_str(&text).with_context(|| format!("parsing {}", path.display()))?
} else {
toml::Value::Table(toml::map::Map::new())
};
let remote_text = serialize_config(section).context("serialising remote section")?;
let remote_root: toml::Value =
toml::from_str(&remote_text).context("re-parsing remote section")?;
let table = root
.as_table_mut()
.ok_or_else(|| anyhow!("config.toml root is not a table"))?;
table.remove("remote");
if !section.remote.is_empty()
&& let Some(new_remote) = remote_root.get("remote").cloned()
{
table.insert("remote".into(), new_remote);
}
let text = toml::to_string_pretty(&root).context("serialising config.toml")?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
}
fs::write(&path, text).with_context(|| format!("writing {}", path.display()))
}
fn add_remote(
data_dir: &std::path::Path,
name: &str,
url: &str,
token_env: Option<&str>,
) -> Result<()> {
if name.is_empty() {
bail!("remote name must not be empty");
}
if name.contains('.') || name.contains('[') || name.contains(']') {
bail!("remote name must not contain '.', '[' or ']'; got `{name}`");
}
if url.starts_with("file://") || url.starts_with("file:/") {
bail!(
"file:// remotes are not supported by `mnem fetch` / `mnem pull`\n\
hint: use `mnem clone {url} <dest>` for a one-shot local CAR mirror,\n\
or host the CAR archive over HTTP and point the remote at that URL"
);
}
let (mut section, _) = load_section(data_dir)?;
if section.remote.contains_key(name) {
bail!("remote `{name}` already exists; use `mnem remote remove {name}` first");
}
section.remote.insert(
name.to_string(),
RemoteConfigFile {
url: url.to_string(),
capabilities: None,
token_env: token_env.map(str::to_string),
},
);
save_section(data_dir, §ion)?;
println!("added remote {name} -> {url}");
Ok(())
}
fn list_remotes(data_dir: &std::path::Path) -> Result<()> {
let (section, _) = load_section(data_dir)?;
if section.remote.is_empty() {
println!("<no remotes>");
return Ok(());
}
let max_name = section.remote.keys().map(String::len).max().unwrap_or(0);
for (name, file) in §ion.remote {
println!("{name:<width$} {}", file.url, width = max_name.max(6));
}
Ok(())
}
fn show_remote(data_dir: &std::path::Path, name: &str) -> Result<()> {
let (section, _) = load_section(data_dir)?;
let file = section
.remote
.get(name)
.ok_or_else(|| anyhow!("remote `{name}` not found"))?;
println!("name {name}");
println!("url {}", file.url);
match &file.capabilities {
None => println!("capabilities <all built-in>"),
Some(list) => {
println!("capabilities ({})", list.len());
for c in list {
println!(" {c}");
}
}
}
match &file.token_env {
None => println!("token_env <none>"),
Some(var) => {
let present = std::env::var(var).is_ok();
println!(
"token_env {var} ({})",
if present { "present in env" } else { "NOT set" }
);
}
}
let _ = BTreeMap::<String, String>::new;
Ok(())
}
fn remove_remote(data_dir: &std::path::Path, name: &str) -> Result<()> {
let (mut section, _) = load_section(data_dir)?;
if section.remote.remove(name).is_none() {
bail!("remote `{name}` not found");
}
save_section(data_dir, §ion)?;
println!("removed remote {name}");
Ok(())
}