- 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>
154 lines
4.6 KiB
Rust
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()
|
|
}
|