feat: update provider/model config handling

This commit is contained in:
KimiSwitch Dev
2026-07-15 19:15:04 +08:00
parent a394e5f730
commit 211986a1e5
7 changed files with 182 additions and 67 deletions
+83 -1
View File
@@ -1,2 +1,84 @@
//! Configuration file I/O for KimiSwitch.
//! Configuration file I/O utilities for KimiSwitch.
//! Reads and writes the global config file and profile files.
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use chrono::{Duration, Local};
/// Creates a backup of `path` inside a `backups/` subdirectory next to the
/// original file, then removes backup files older than `retention_days`.
pub fn backup_file(path: &Path, retention_days: i64) -> Result<PathBuf> {
if !path.exists() {
return Ok(PathBuf::new());
}
let parent = path.parent().context("path has no parent directory")?;
let backups_dir = parent.join("backups");
std::fs::create_dir_all(&backups_dir)
.with_context(|| format!("failed to create backups directory {}", backups_dir.display()))?;
let file_name = path.file_name().context("path has no file name")?;
let timestamp = Local::now().format("%Y%m%d_%H%M%S_%.3f").to_string();
let backup_name = format!("{}.bak.{}", file_name.to_string_lossy(), timestamp);
let mut backup_path = backups_dir.join(&backup_name);
if backup_path.exists() {
for n in 1..1000 {
let candidate = backups_dir.join(format!(
"{}.bak.{}.{:03}",
file_name.to_string_lossy(),
timestamp,
n
));
if !candidate.exists() {
backup_path = candidate;
break;
}
}
}
std::fs::copy(path, &backup_path).with_context(|| {
format!(
"failed to back up {} to {}",
path.display(),
backup_path.display()
)
})?;
cleanup_old_backups(&backups_dir, retention_days)?;
Ok(backup_path)
}
/// Removes backup files in `dir` older than `retention_days`, based on file
/// modification time. Only files whose names contain `.bak.` are deleted.
fn cleanup_old_backups(dir: &Path, retention_days: i64) -> Result<()> {
let cutoff = Local::now() - Duration::days(retention_days);
let entries = std::fs::read_dir(dir)
.with_context(|| format!("failed to read backups directory {}", dir.display()))?;
for entry in entries {
let entry = entry.context("failed to read directory entry")?;
let path = entry.path();
if !path.is_file() {
continue;
}
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !name.contains(".bak.") {
continue;
}
let metadata = entry
.metadata()
.with_context(|| format!("failed to read metadata for {}", path.display()))?;
let modified = metadata
.modified()
.with_context(|| format!("failed to read modified time for {}", path.display()))?;
let modified: chrono::DateTime<Local> = modified.into();
if modified < cutoff {
std::fs::remove_file(&path)
.with_context(|| format!("failed to remove old backup {}", path.display()))?;
}
}
Ok(())
}
+4 -15
View File
@@ -11,6 +11,7 @@ use indexmap::IndexMap;
use serde_json::Value;
use toml::value::{Table, Value as TomlValue};
use crate::config_io::backup_file;
use crate::models::{Agent, Config, Model, Provider, ProviderType};
pub type KimiResult<T> = anyhow::Result<T>;
@@ -51,21 +52,9 @@ pub fn save_kimi_code_config(value: &TomlValue) -> KimiResult<()> {
}
if path.exists() {
let timestamp = chrono::Local::now()
.format("%Y%m%d_%H%M%S_%.3f")
.to_string();
let mut backup_path = path.with_extension(format!("toml.bak.{}", timestamp));
if backup_path.exists() {
for n in 1..1000 {
let candidate = path.with_extension(format!("toml.bak.{}.{:03}", timestamp, n));
if !candidate.exists() {
backup_path = candidate;
break;
}
}
}
std::fs::copy(&path, &backup_path)
.with_context(|| format!("failed to back up {} to {}", path.display(), backup_path.display()))?;
backup_file(&path, 7).with_context(|| {
format!("failed to back up Kimi Code config {}", path.display())
})?;
}
let content = toml::to_string_pretty(value)
+1
View File
@@ -1,4 +1,5 @@
pub mod commands;
pub mod config_io;
pub mod db;
pub mod kimi_code_io;
pub mod models;
+7 -30
View File
@@ -11,6 +11,7 @@ use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::config_io::backup_file;
use crate::models::{Config, Model, Provider, ProviderType};
pub type PiResult<T> = anyhow::Result<T>;
@@ -132,21 +133,9 @@ pub fn save_pi_models(file: &PiModelsFile) -> PiResult<()> {
}
if path.exists() {
let timestamp = chrono::Local::now()
.format("%Y%m%d_%H%M%S_%.3f")
.to_string();
let mut backup_path = path.with_extension(format!("json.bak.{}", timestamp));
if backup_path.exists() {
for n in 1..1000 {
let candidate = path.with_extension(format!("json.bak.{}.{:03}", timestamp, n));
if !candidate.exists() {
backup_path = candidate;
break;
}
}
}
std::fs::copy(&path, &backup_path)
.with_context(|| format!("failed to back up {} to {}", path.display(), backup_path.display()))?;
backup_file(&path, 7).with_context(|| {
format!("failed to back up Pi models {}", path.display())
})?;
}
let content = serde_json::to_string_pretty(file)
@@ -196,21 +185,9 @@ pub fn save_pi_settings(file: &PiSettingsFile) -> PiResult<()> {
}
if path.exists() {
let timestamp = chrono::Local::now()
.format("%Y%m%d_%H%M%S_%.3f")
.to_string();
let mut backup_path = path.with_extension(format!("json.bak.{}", timestamp));
if backup_path.exists() {
for n in 1..1000 {
let candidate = path.with_extension(format!("json.bak.{}.{:03}", timestamp, n));
if !candidate.exists() {
backup_path = candidate;
break;
}
}
}
std::fs::copy(&path, &backup_path)
.with_context(|| format!("failed to back up {} to {}", path.display(), backup_path.display()))?;
backup_file(&path, 7).with_context(|| {
format!("failed to back up Pi settings {}", path.display())
})?;
}
let content = serde_json::to_string_pretty(file)