mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-31 19:22:15 +08:00
feat: add session deletion with per-provider cleanup and path safety
Add delete_session Tauri command dispatching to provider-specific deletion logic for all 5 providers (Claude, Codex, Gemini, OpenCode, OpenClaw). Includes path traversal protection via canonicalize + starts_with validation, session ID verification against file contents, frontend confirmation dialog with optimistic cache updates, i18n keys (zh/en/ja), and component tests.
This commit is contained in:
@@ -12,7 +12,7 @@ const PROVIDER_ID: &str = "opencode";
|
||||
///
|
||||
/// Respects `XDG_DATA_HOME` on all platforms; falls back to
|
||||
/// `~/.local/share/opencode/storage/`.
|
||||
fn get_opencode_data_dir() -> PathBuf {
|
||||
pub(crate) fn get_opencode_data_dir() -> PathBuf {
|
||||
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
|
||||
if !xdg.is_empty() {
|
||||
return PathBuf::from(xdg).join("opencode").join("storage");
|
||||
@@ -111,6 +111,71 @@ pub fn load_messages(path: &Path) -> Result<Vec<SessionMessage>, String> {
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
pub fn delete_session(storage: &Path, path: &Path, session_id: &str) -> Result<bool, String> {
|
||||
if path.file_name().and_then(|name| name.to_str()) != Some(session_id) {
|
||||
return Err(format!(
|
||||
"OpenCode session path does not match session ID: expected {session_id}, found {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
let mut message_files = Vec::new();
|
||||
collect_json_files(path, &mut message_files);
|
||||
|
||||
let mut message_ids = Vec::new();
|
||||
for message_path in &message_files {
|
||||
let data = match std::fs::read_to_string(message_path) {
|
||||
Ok(data) => data,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let value: Value = match serde_json::from_str(&data) {
|
||||
Ok(value) => value,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let Some(message_id) = value.get("id").and_then(Value::as_str) {
|
||||
message_ids.push(message_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
for message_id in &message_ids {
|
||||
let part_dir = storage.join("part").join(message_id);
|
||||
remove_dir_all_if_exists(&part_dir).map_err(|e| {
|
||||
format!(
|
||||
"Failed to delete OpenCode part directory {}: {e}",
|
||||
part_dir.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
let session_diff_path = storage
|
||||
.join("session_diff")
|
||||
.join(format!("{session_id}.json"));
|
||||
remove_file_if_exists(&session_diff_path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to delete OpenCode session diff {}: {e}",
|
||||
session_diff_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
remove_dir_all_if_exists(path).map_err(|e| {
|
||||
format!(
|
||||
"Failed to delete OpenCode message directory {}: {e}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(session_file) = find_session_file(storage, session_id) {
|
||||
remove_file_if_exists(&session_file).map_err(|e| {
|
||||
format!(
|
||||
"Failed to delete OpenCode session file {}: {e}",
|
||||
session_file.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn parse_session(storage: &Path, path: &Path) -> Option<SessionMeta> {
|
||||
let data = std::fs::read_to_string(path).ok()?;
|
||||
let value: Value = serde_json::from_str(&data).ok()?;
|
||||
@@ -274,3 +339,97 @@ fn collect_json_files(root: &Path, files: &mut Vec<PathBuf>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_session_file(storage: &Path, session_id: &str) -> Option<PathBuf> {
|
||||
let session_root = storage.join("session");
|
||||
let mut files = Vec::new();
|
||||
collect_json_files(&session_root, &mut files);
|
||||
let expected = format!("{session_id}.json");
|
||||
|
||||
files
|
||||
.into_iter()
|
||||
.find(|path| path.file_name().and_then(|name| name.to_str()) == Some(expected.as_str()))
|
||||
}
|
||||
|
||||
fn remove_file_if_exists(path: &Path) -> std::io::Result<()> {
|
||||
match std::fs::remove_file(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_dir_all_if_exists(path: &Path) -> std::io::Result<()> {
|
||||
match std::fs::remove_dir_all(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn delete_session_removes_session_diff_messages_and_parts() {
|
||||
let temp = tempdir().expect("tempdir");
|
||||
let storage = temp.path();
|
||||
let project_id = "project-123";
|
||||
let session_id = "ses_123";
|
||||
let session_dir = storage.join("session").join(project_id);
|
||||
let message_dir = storage.join("message").join(session_id);
|
||||
let session_diff = storage
|
||||
.join("session_diff")
|
||||
.join(format!("{session_id}.json"));
|
||||
let part_dir = storage.join("part").join("msg_1");
|
||||
let session_file = session_dir.join(format!("{session_id}.json"));
|
||||
|
||||
std::fs::create_dir_all(&session_dir).expect("create session dir");
|
||||
std::fs::create_dir_all(&message_dir).expect("create message dir");
|
||||
std::fs::create_dir_all(&part_dir).expect("create part dir");
|
||||
std::fs::create_dir_all(storage.join("project")).expect("create project dir");
|
||||
std::fs::create_dir_all(storage.join("session_diff")).expect("create session diff dir");
|
||||
|
||||
std::fs::write(
|
||||
&session_file,
|
||||
format!(
|
||||
r#"{{
|
||||
"id": "{session_id}",
|
||||
"projectID": "{project_id}",
|
||||
"directory": "/tmp/project",
|
||||
"time": {{ "created": 1, "updated": 2 }}
|
||||
}}"#
|
||||
),
|
||||
)
|
||||
.expect("write session file");
|
||||
std::fs::write(
|
||||
message_dir.join("msg_1.json"),
|
||||
format!(r#"{{"id":"msg_1","sessionID":"{session_id}","role":"user"}}"#),
|
||||
)
|
||||
.expect("write message file");
|
||||
std::fs::write(
|
||||
part_dir.join("prt_1.json"),
|
||||
r#"{"id":"prt_1","messageID":"msg_1"}"#,
|
||||
)
|
||||
.expect("write part file");
|
||||
std::fs::write(&session_diff, "[]").expect("write session diff");
|
||||
std::fs::write(
|
||||
storage.join("project").join(format!("{project_id}.json")),
|
||||
r#"{"id":"project-123"}"#,
|
||||
)
|
||||
.expect("write project file");
|
||||
|
||||
delete_session(storage, &message_dir, session_id).expect("delete session");
|
||||
|
||||
assert!(!session_file.exists());
|
||||
assert!(!message_dir.exists());
|
||||
assert!(!session_diff.exists());
|
||||
assert!(!part_dir.exists());
|
||||
assert!(storage
|
||||
.join("project")
|
||||
.join(format!("{project_id}.json"))
|
||||
.exists());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user