mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(credentials): quarantine corrupt auth profile store (#1514)
This commit is contained in:
@@ -373,13 +373,23 @@ impl AuthProfilesStore {
|
||||
return Ok(PersistedAuthProfiles::default());
|
||||
}
|
||||
|
||||
let mut persisted: PersistedAuthProfiles =
|
||||
serde_json::from_slice(&bytes).with_context(|| {
|
||||
format!(
|
||||
"Failed to parse auth profile store at {}",
|
||||
self.path.display()
|
||||
)
|
||||
})?;
|
||||
let mut persisted: PersistedAuthProfiles = match serde_json::from_slice(&bytes) {
|
||||
Ok(p) => p,
|
||||
Err(err) => {
|
||||
let quarantined = quarantine_corrupt_store(&self.path)?;
|
||||
let quarantined_file = quarantined
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("auth-profiles.corrupt");
|
||||
tracing::warn!(
|
||||
path_file = PROFILES_FILENAME,
|
||||
quarantined_file = quarantined_file,
|
||||
error = %err,
|
||||
"[credentials] auth profile store unparseable; quarantined and reset to empty"
|
||||
);
|
||||
return Ok(PersistedAuthProfiles::default());
|
||||
}
|
||||
};
|
||||
|
||||
if persisted.schema_version == 0 {
|
||||
persisted.schema_version = CURRENT_SCHEMA_VERSION;
|
||||
@@ -598,6 +608,33 @@ pub fn profile_id(provider: &str, profile_name: &str) -> String {
|
||||
format!("{}:{}", provider.trim(), profile_name.trim())
|
||||
}
|
||||
|
||||
fn quarantine_corrupt_store(path: &Path) -> Result<PathBuf> {
|
||||
let stem = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("auth-profiles");
|
||||
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("json");
|
||||
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
||||
let ts = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0);
|
||||
let mut candidate = parent.join(format!("{stem}.corrupt-{ts}.{ext}"));
|
||||
let mut suffix = 0u32;
|
||||
while candidate.exists() {
|
||||
suffix += 1;
|
||||
candidate = parent.join(format!("{stem}.corrupt-{ts}-{suffix}.{ext}"));
|
||||
}
|
||||
fs::rename(path, &candidate).with_context(|| {
|
||||
format!(
|
||||
"Failed to quarantine corrupt auth profile store {} -> {}",
|
||||
path.display(),
|
||||
candidate.display()
|
||||
)
|
||||
})?;
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "profiles_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -127,6 +127,33 @@ fn auth_profiles_data_default() {
|
||||
assert!(data.active_profiles.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_store_is_quarantined_and_reset() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let store = AuthProfilesStore::new(tmp.path(), false);
|
||||
let path = store.path().to_path_buf();
|
||||
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&path, b"{ not valid json").unwrap();
|
||||
|
||||
let data = store.load().unwrap();
|
||||
assert!(data.profiles.is_empty());
|
||||
assert_eq!(data.schema_version, CURRENT_SCHEMA_VERSION);
|
||||
|
||||
let parent = path.parent().unwrap();
|
||||
let quarantined: Vec<_> = std::fs::read_dir(parent)
|
||||
.unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_name().to_string_lossy().contains(".corrupt-"))
|
||||
.collect();
|
||||
assert_eq!(quarantined.len(), 1, "expected one quarantined file");
|
||||
|
||||
let profile = AuthProfile::new_token("openai", "default", "tok".into());
|
||||
store.upsert_profile(profile, true).unwrap();
|
||||
let reloaded = store.load().unwrap();
|
||||
assert_eq!(reloaded.profiles.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_nonexistent_profile_returns_false() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user