Files
mdrs-client-rust/src/commands/config.rs
T
orrisrootandCopilot 0d474e7913 fix: align all command outputs with Python reference implementation
- fix(connection): fix create_folder API body (name, parent_id, description, template_id)
- feat(shared): add Unicode NFC normalization helper and find_subfolder_by_name()
- feat(Cargo): add unicode-normalization dependency
- fix(shared,mkdir,rm,cp,mv,upload): apply NFC normalization to path and name comparisons
- fix(labs): rewrite output as aligned table (Name/PI/Laboratory), remove cache fallback
- fix(mkdir): silent on success; align error message with Python
- fix(rm): silent on success; use find_subfolder_by_name for NFC-aware lookup
- fix(cp): silent on success; align all error messages; add no-op when src==dest
- fix(mv): silent on success; align all error messages; add no-op when src==dest
- fix(login): change output to 'Login Successful'
- fix(logout): remove all output (silent like Python)
- fix(chacl): remove success message (silent like Python)
- fix(metadata): use compact JSON output (to_string instead of to_string_pretty)
- fix(file_metadata): use compact JSON output
- fix(ls): use compact JSON output; add blank line after entries in recursive plain mode
- fix(config): silent on create/update/delete; add colon in list short format;
  remove empty-state messages; align error messages ('is already exists.' / 'is not exists.')

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 18:45:52 +09:00

154 lines
4.6 KiB
Rust

use configparser::ini::Ini;
use std::fs;
use std::path::PathBuf;
fn config_path() -> PathBuf {
crate::settings::SETTINGS.config_dirname.join("config.ini")
}
fn sanitize_config_file(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
if !path.exists() {
return Ok(());
}
let text = fs::read_to_string(path)?;
let lines: Vec<&str> = text.lines().collect();
if let Some(first) = lines.first() {
if first.trim() == path.to_string_lossy() {
// remove the first line and write atomically
let new_text = if lines.len() > 1 {
lines[1..].join("\n")
} else {
String::new()
};
let tmp = path.with_extension("tmp");
fs::write(&tmp, new_text.as_bytes())?;
fs::rename(&tmp, path)?;
}
}
Ok(())
}
fn write_ini_atomic(path: &PathBuf, ini: &Ini) -> Result<(), Box<dyn std::error::Error>> {
let tmp = path.with_extension("tmp");
// write to tmp path then rename for atomicity
ini.write(&tmp)?;
fs::rename(&tmp, path)?;
Ok(())
}
pub fn get_remote_url(remote: &str) -> Result<Option<String>, Box<dyn std::error::Error>> {
let path = config_path();
sanitize_config_file(&path)?;
let path_str = path.to_string_lossy().to_string();
let mut conf = Ini::new();
if path.exists() {
let _ = conf.load(&path_str)?;
}
Ok(conf.get(remote, "url"))
}
pub fn config_create(remote: &str, url: &str) -> Result<(), Box<dyn std::error::Error>> {
if !validate_url(url) {
return Err("Malformed URL".into());
}
let path = config_path();
sanitize_config_file(&path)?;
let path_str = path.to_string_lossy().to_string();
let mut conf = Ini::new();
if path.exists() {
let _ = conf.load(&path_str)?;
}
// check if section exists
let section_exists = conf
.get_map()
.map(|m| m.contains_key(remote))
.unwrap_or(false);
if section_exists {
return Err(format!("Remote host `{}` is already exists.", remote).into());
}
// set url
conf.set(remote, "url", Some(url.to_string()));
// ensure parent dir exists
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
write_ini_atomic(&path, &conf)?;
Ok(())
}
pub fn config_update(remote: &str, url: &str) -> Result<(), Box<dyn std::error::Error>> {
if !validate_url(url) {
return Err("Malformed URL".into());
}
let path = config_path();
sanitize_config_file(&path)?;
let path_str = path.to_string_lossy().to_string();
let mut conf = Ini::new();
if path.exists() {
let _ = conf.load(&path_str)?;
}
// ensure section exists
let section_exists = conf
.get_map()
.map(|m| m.contains_key(remote))
.unwrap_or(false);
if !section_exists {
return Err(format!("Remote host `{}` is not exists.", remote).into());
}
conf.set(remote, "url", Some(url.to_string()));
write_ini_atomic(&path, &conf)?;
Ok(())
}
pub fn config_list(long: bool) -> Result<(), Box<dyn std::error::Error>> {
let path = config_path();
if !path.exists() {
return Ok(());
}
sanitize_config_file(&path)?;
let path_str = path.to_string_lossy().to_string();
let mut conf = Ini::new();
let _ = conf.load(&path_str)?;
let map = conf.get_map().unwrap_or_default();
for (sec, props) in map.iter() {
if sec == "default" {
continue;
}
if !long {
println!("{}:", sec);
} else {
let url = props.get("url").and_then(|v| v.clone()).unwrap_or_default();
println!("{}:\t{}", sec, url);
}
}
Ok(())
}
pub fn config_delete(remote: &str) -> Result<(), Box<dyn std::error::Error>> {
let path = config_path();
sanitize_config_file(&path)?;
let path_str = path.to_string_lossy().to_string();
let mut conf = Ini::new();
if path.exists() {
let _ = conf.load(&path_str)?;
}
// fallback: reconstruct by removing the section in memory map and writing file
let mut map = conf.get_map().unwrap_or_default();
if map.remove(remote).is_none() {
return Err(format!("Remote host `{}` is not exists.", remote).into());
}
// build new Ini from map
let mut new_ini = Ini::new();
for (sec, props) in map.iter() {
for (k, v) in props.iter() {
new_ini.set(sec, k, v.clone());
}
}
write_ini_atomic(&path, &new_ini)?;
Ok(())
}
fn validate_url(url: &str) -> bool {
validators::url::Url::parse(url).is_ok()
}