use std::path::{Path, PathBuf};
use lsp_types::FileChangeType;
use crate::context::AppContext;
use crate::edit;
use crate::protocol::{RawRequest, Response};
pub fn handle_delete_file(req: &RawRequest, ctx: &AppContext) -> Response {
let op_id = crate::backup::new_op_id();
let recursive = req
.params
.get("recursive")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if let Some(files) = req.params.get("files").and_then(|v| v.as_array()) {
let mut deleted = Vec::new();
let mut skipped = Vec::new();
for value in files {
let Some(file) = value.as_str() else {
skipped.push(serde_json::json!({"file": value, "reason": "not a string"}));
continue;
};
match delete_one_or_dir(req, ctx, file, recursive, &op_id) {
Ok(result) => deleted.push(result),
Err(resp) => skipped.push(serde_json::json!({
"file": file,
"reason": resp.data.get("message").and_then(|v| v.as_str()).unwrap_or("delete failed"),
})),
}
}
return Response::success(
&req.id,
serde_json::json!({
"complete": skipped.is_empty(),
"deleted": deleted,
"skipped_files": skipped,
}),
);
}
let file = match req.params.get("file").and_then(|v| v.as_str()) {
Some(f) => f,
None => {
return Response::error(
&req.id,
"invalid_request",
"delete_file: missing required param 'file' or 'files'",
);
}
};
match delete_one_or_dir(req, ctx, file, recursive, &op_id) {
Ok(result) => Response::success(&req.id, result),
Err(resp) => resp,
}
}
fn delete_one_or_dir(
req: &RawRequest,
ctx: &AppContext,
file: &str,
recursive: bool,
op_id: &str,
) -> Result<serde_json::Value, Response> {
let requested_path = Path::new(file);
if is_symlink(requested_path).map_err(|e| {
Response::error(
&req.id,
"io_error",
format!("delete_file: failed to inspect '{}': {}", file, e),
)
})? {
return Err(Response::error(
&req.id,
"invalid_request",
format!(
"delete_file: refusing to delete symlink '{}'; symlink undo is not supported",
file
),
));
}
let path = match ctx.validate_path(&req.id, requested_path) {
Ok(path) => path,
Err(resp) => return Err(resp),
};
if !path.exists() {
return Err(Response::error(
&req.id,
"file_not_found",
format!("delete_file: file not found: {}", file),
));
}
if is_symlink(&path).map_err(|e| {
Response::error(
&req.id,
"io_error",
format!("delete_file: failed to inspect '{}': {}", file, e),
)
})? {
return Err(Response::error(
&req.id,
"invalid_request",
format!(
"delete_file: refusing to delete symlink '{}'; symlink undo is not supported",
file
),
));
}
if path.is_dir() {
if !recursive {
return Err(Response::error(
&req.id,
"invalid_request",
format!(
"delete_file: '{}' is a directory. Pass recursive: true to delete it with all contents.",
file
),
));
}
return delete_directory(req, ctx, &path, file, op_id);
}
let backup_id = edit::auto_backup(
ctx,
req.session(),
&path,
"delete_file: pre-delete backup",
Some(op_id),
)
.map_err(|e| Response::error(&req.id, e.code(), e.to_string()))?;
if let Err(e) = std::fs::remove_file(&path) {
return Err(Response::error(
&req.id,
"io_error",
format!("delete_file: failed to delete: {}", e),
));
}
ctx.lsp_notify_watched_config_file(path.as_path(), FileChangeType::DELETED);
log::debug!("delete_file: {}", file);
let mut result = serde_json::json!({
"file": file,
"deleted": true,
});
if let Some(ref id) = backup_id {
result["backup_id"] = serde_json::json!(id);
}
Ok(result)
}
fn is_symlink(path: &Path) -> std::io::Result<bool> {
match std::fs::symlink_metadata(path) {
Ok(metadata) => Ok(metadata.file_type().is_symlink()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e),
}
}
fn delete_directory(
req: &RawRequest,
ctx: &AppContext,
path: &Path,
original: &str,
op_id: &str,
) -> Result<serde_json::Value, Response> {
let unsupported_paths = validate_directory_for_recursive_delete(path).map_err(|e| {
Response::error(
&req.id,
"io_error",
format!(
"delete_file: failed to validate directory '{}': {}",
original, e
),
)
})?;
if !unsupported_paths.is_empty() {
return Err(Response::error(
&req.id,
"unsupported_directory_contents",
unsupported_directory_contents_message(&unsupported_paths),
));
}
let mut files_to_backup: Vec<PathBuf> = Vec::new();
if let Err(e) = collect_files(path, &mut files_to_backup) {
return Err(Response::error(
&req.id,
"io_error",
format!(
"delete_file: failed to walk directory '{}': {}",
original, e
),
));
}
let mut backup_ids: Vec<String> = Vec::new();
for file_path in &files_to_backup {
match edit::auto_backup(
ctx,
req.session(),
file_path,
"delete_file: pre-delete backup (directory contents)",
Some(op_id),
) {
Ok(Some(id)) => backup_ids.push(id),
Ok(None) => {}
Err(e) => {
return Err(Response::error(
&req.id,
e.code(),
format!(
"delete_file: backup failed for '{}' inside '{}': {}",
file_path.display(),
original,
e
),
));
}
}
}
if let Err(e) = std::fs::remove_dir_all(path) {
return Err(Response::error(
&req.id,
"io_error",
format!(
"delete_file: failed to remove directory '{}': {}",
original, e
),
));
}
for file_path in &files_to_backup {
ctx.lsp_notify_watched_config_file(file_path.as_path(), FileChangeType::DELETED);
}
log::debug!(
"delete_file: recursively removed directory '{}' ({} file(s))",
original,
files_to_backup.len()
);
Ok(serde_json::json!({
"file": original,
"deleted": true,
"is_directory": true,
"files_deleted": files_to_backup.len(),
"backup_ids": backup_ids,
}))
}
fn validate_directory_for_recursive_delete(dir: &Path) -> std::io::Result<Vec<String>> {
let mut unsupported_paths = Vec::new();
if std::fs::symlink_metadata(dir)?.file_type().is_symlink() {
unsupported_paths.push(dir.display().to_string());
return Ok(unsupported_paths);
}
validate_directory_entries(dir, &mut unsupported_paths)?;
Ok(unsupported_paths)
}
fn validate_directory_entries(
dir: &Path,
unsupported_paths: &mut Vec<String>,
) -> std::io::Result<()> {
let mut entries = Vec::new();
for entry in std::fs::read_dir(dir)? {
entries.push(entry?);
}
if entries.is_empty() {
unsupported_paths.push(dir.display().to_string());
return Ok(());
}
for entry in entries {
let path = entry.path();
let file_type = entry.file_type()?;
if file_type.is_symlink() {
unsupported_paths.push(path.display().to_string());
} else if file_type.is_dir() {
validate_directory_entries(&path, unsupported_paths)?;
}
}
Ok(())
}
fn unsupported_directory_contents_message(paths: &[String]) -> String {
const MAX_PATHS: usize = 5;
let mut message = String::from(
"aft_delete with recursive: true does not yet support directory trees containing symlinks or empty directories. Restore would not recover these entries atomically.",
);
message.push_str(" Offending path(s): ");
message.push_str(
&paths
.iter()
.take(MAX_PATHS)
.map(String::as_str)
.collect::<Vec<_>>()
.join(", "),
);
if paths.len() > MAX_PATHS {
message.push_str(&format!(", ... and {} more", paths.len() - MAX_PATHS));
}
message
}
fn collect_files(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;
if file_type.is_file() || file_type.is_symlink() {
out.push(path);
} else if file_type.is_dir() {
collect_files(&path, out)?;
}
}
Ok(())
}