移除Unknown错误码

This commit is contained in:
xintaofei
2026-03-07 17:55:06 +08:00
parent dbcac80712
commit b1ea16fae0
6 changed files with 134 additions and 271 deletions

View File

@@ -6,7 +6,6 @@ use crate::db::error::DbError;
#[derive(Debug, Clone, Copy, Serialize)] #[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum AppErrorCode { pub enum AppErrorCode {
Unknown,
InvalidInput, InvalidInput,
ConfigurationMissing, ConfigurationMissing,
ConfigurationInvalid, ConfigurationInvalid,
@@ -55,6 +54,10 @@ impl AppCommandError {
Self::new(AppErrorCode::InvalidInput, message) Self::new(AppErrorCode::InvalidInput, message)
} }
pub fn configuration_missing(message: impl Into<String>) -> Self {
Self::new(AppErrorCode::ConfigurationMissing, message)
}
pub fn configuration_invalid(message: impl Into<String>) -> Self { pub fn configuration_invalid(message: impl Into<String>) -> Self {
Self::new(AppErrorCode::ConfigurationInvalid, message) Self::new(AppErrorCode::ConfigurationInvalid, message)
} }
@@ -63,10 +66,34 @@ impl AppCommandError {
Self::new(AppErrorCode::NotFound, message) Self::new(AppErrorCode::NotFound, message)
} }
pub fn already_exists(message: impl Into<String>) -> Self {
Self::new(AppErrorCode::AlreadyExists, message)
}
pub fn permission_denied(message: impl Into<String>) -> Self {
Self::new(AppErrorCode::PermissionDenied, message)
}
pub fn dependency_missing(message: impl Into<String>) -> Self {
Self::new(AppErrorCode::DependencyMissing, message)
}
pub fn network(message: impl Into<String>) -> Self { pub fn network(message: impl Into<String>) -> Self {
Self::new(AppErrorCode::NetworkError, message) Self::new(AppErrorCode::NetworkError, message)
} }
pub fn authentication_failed(message: impl Into<String>) -> Self {
Self::new(AppErrorCode::AuthenticationFailed, message)
}
pub fn database_error(message: impl Into<String>) -> Self {
Self::new(AppErrorCode::DatabaseError, message)
}
pub fn io_error(message: impl Into<String>) -> Self {
Self::new(AppErrorCode::IoError, message)
}
pub fn task_execution_failed(message: impl Into<String>) -> Self { pub fn task_execution_failed(message: impl Into<String>) -> Self {
Self::new(AppErrorCode::TaskExecutionFailed, message) Self::new(AppErrorCode::TaskExecutionFailed, message)
} }
@@ -104,15 +131,3 @@ impl From<DbError> for AppCommandError {
Self::db(value) Self::db(value)
} }
} }
impl From<String> for AppCommandError {
fn from(value: String) -> Self {
Self::new(AppErrorCode::Unknown, "Operation failed").with_detail(value)
}
}
impl From<&str> for AppCommandError {
fn from(value: &str) -> Self {
Self::from(value.to_string())
}
}

View File

@@ -13,7 +13,7 @@ use tauri::Emitter;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::app_error::{AppCommandError, AppErrorCode}; use crate::app_error::AppCommandError;
use crate::db::error::DbError; use crate::db::error::DbError;
use crate::db::service::folder_service; use crate::db::service::folder_service;
use crate::db::AppDatabase; use crate::db::AppDatabase;
@@ -315,8 +315,7 @@ pub async fn set_folder_parent_branch(
.one(&db.conn) .one(&db.conn)
.await .await
.map_err(|e| { .map_err(|e| {
AppCommandError::new(AppErrorCode::DatabaseError, "Failed to query folder") AppCommandError::database_error("Failed to query folder").with_detail(e.to_string())
.with_detail(e.to_string())
})?; })?;
if let Some(folder_model) = row { if let Some(folder_model) = row {
@@ -354,8 +353,7 @@ pub async fn create_folder_directory(path: String) -> Result<(), AppCommandError
#[tauri::command] #[tauri::command]
pub async fn clone_repository(url: String, target_dir: String) -> Result<(), AppCommandError> { pub async fn clone_repository(url: String, target_dir: String) -> Result<(), AppCommandError> {
if url.trim().is_empty() || target_dir.trim().is_empty() { if url.trim().is_empty() || target_dir.trim().is_empty() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"Repository URL and target directory are required", "Repository URL and target directory are required",
)); ));
} }
@@ -366,8 +364,7 @@ pub async fn clone_repository(url: String, target_dir: String) -> Result<(), App
.await .await
.map_err(|e| { .map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound { if e.kind() == std::io::ErrorKind::NotFound {
AppCommandError::new( AppCommandError::dependency_missing(
AppErrorCode::DependencyMissing,
"Git is not installed. Please install Git first.", "Git is not installed. Please install Git first.",
) )
.with_detail("https://git-scm.com") .with_detail("https://git-scm.com")
@@ -387,16 +384,12 @@ fn classify_git_clone_error(stderr: &str) -> AppCommandError {
let normalized = stderr.to_lowercase(); let normalized = stderr.to_lowercase();
if normalized.contains("already exists and is not an empty directory") { if normalized.contains("already exists and is not an empty directory") {
return AppCommandError::new( return AppCommandError::already_exists("Target directory already exists and is not empty")
AppErrorCode::AlreadyExists, .with_detail(stderr.to_string());
"Target directory already exists and is not empty",
)
.with_detail(stderr.to_string());
} }
if normalized.contains("repository not found") { if normalized.contains("repository not found") {
return AppCommandError::new( return AppCommandError::not_found(
AppErrorCode::NotFound,
"Repository not found. Check URL and access permissions.", "Repository not found. Check URL and access permissions.",
) )
.with_detail(stderr.to_string()); .with_detail(stderr.to_string());
@@ -407,30 +400,23 @@ fn classify_git_clone_error(stderr: &str) -> AppCommandError {
|| normalized.contains("connection timed out") || normalized.contains("connection timed out")
|| normalized.contains("failed to connect") || normalized.contains("failed to connect")
{ {
return AppCommandError::new( return AppCommandError::network("Network is unavailable while cloning repository")
AppErrorCode::NetworkError, .with_detail(stderr.to_string());
"Network is unavailable while cloning repository",
)
.with_detail(stderr.to_string());
} }
if normalized.contains("authentication failed") if normalized.contains("authentication failed")
|| normalized.contains("could not read username") || normalized.contains("could not read username")
|| normalized.contains("permission denied (publickey)") || normalized.contains("permission denied (publickey)")
{ {
return AppCommandError::new( return AppCommandError::authentication_failed(
AppErrorCode::AuthenticationFailed,
"Authentication failed while cloning repository", "Authentication failed while cloning repository",
) )
.with_detail(stderr.to_string()); .with_detail(stderr.to_string());
} }
if normalized.contains("permission denied") { if normalized.contains("permission denied") {
return AppCommandError::new( return AppCommandError::permission_denied("Permission denied while cloning repository")
AppErrorCode::PermissionDenied, .with_detail(stderr.to_string());
"Permission denied while cloning repository",
)
.with_detail(stderr.to_string());
} }
AppCommandError::external_command("Git clone failed", stderr.to_string()) AppCommandError::external_command("Git clone failed", stderr.to_string())
@@ -610,18 +596,16 @@ pub async fn git_worktree_add(
.map_err(AppCommandError::io)?; .map_err(AppCommandError::io)?;
if check.status.success() { if check.status.success() {
return Err( return Err(
AppCommandError::new(AppErrorCode::AlreadyExists, "Branch already exists") AppCommandError::already_exists("Branch already exists").with_detail(branch_name)
.with_detail(branch_name),
); );
} }
// 校验目录是否已存在 // 校验目录是否已存在
if std::path::Path::new(&worktree_path).exists() { if std::path::Path::new(&worktree_path).exists() {
return Err(AppCommandError::new( return Err(
AppErrorCode::AlreadyExists, AppCommandError::already_exists("Worktree directory already exists")
"Worktree directory already exists", .with_detail(worktree_path),
) );
.with_detail(worktree_path));
} }
// 执行 git worktree add -b <branch> <path> // 执行 git worktree add -b <branch> <path>
@@ -786,8 +770,7 @@ pub async fn git_diff_with_branch(
) -> Result<String, AppCommandError> { ) -> Result<String, AppCommandError> {
let target_branch = branch.trim(); let target_branch = branch.trim();
if target_branch.is_empty() { if target_branch.is_empty() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"Branch name cannot be empty", "Branch name cannot be empty",
)); ));
} }
@@ -876,11 +859,9 @@ pub async fn git_show_file(
let bytes = &output.stdout; let bytes = &output.stdout;
if bytes.iter().take(2048).any(|b| *b == 0) { if bytes.iter().take(2048).any(|b| *b == 0) {
return Err(AppCommandError::new( return Err(
AppErrorCode::InvalidInput, AppCommandError::invalid_input("Binary files are not supported").with_detail(file_spec),
"Binary files are not supported", );
)
.with_detail(file_spec));
} }
Ok(String::from_utf8_lossy(bytes).to_string()) Ok(String::from_utf8_lossy(bytes).to_string())
@@ -946,10 +927,7 @@ pub async fn git_commit(
pub async fn git_rollback_file(path: String, file: String) -> Result<(), AppCommandError> { pub async fn git_rollback_file(path: String, file: String) -> Result<(), AppCommandError> {
let target = file.trim(); let target = file.trim();
if target.is_empty() { if target.is_empty() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input("File path cannot be empty"));
AppErrorCode::InvalidInput,
"File path cannot be empty",
));
} }
let literal_file = to_git_literal_pathspec(target); let literal_file = to_git_literal_pathspec(target);
@@ -1294,8 +1272,7 @@ fn should_refresh_git_status_for_paths(root_display: &str, changed_paths: &[Stri
fn canonicalize_watch_root(root: &Path) -> Result<(PathBuf, String), AppCommandError> { fn canonicalize_watch_root(root: &Path) -> Result<(PathBuf, String), AppCommandError> {
let canonical = std::fs::canonicalize(root).map_err(|e| { let canonical = std::fs::canonicalize(root).map_err(|e| {
AppCommandError::new(AppErrorCode::NotFound, "Unable to resolve workspace root") AppCommandError::not_found("Unable to resolve workspace root").with_detail(e.to_string())
.with_detail(e.to_string())
})?; })?;
let key = normalize_slash_path(&canonical); let key = normalize_slash_path(&canonical);
Ok((canonical, key)) Ok((canonical, key))
@@ -1532,26 +1509,17 @@ fn run_file_watch_event_loop(
fn resolve_tree_path(root: &Path, rel_path: &str) -> Result<PathBuf, AppCommandError> { fn resolve_tree_path(root: &Path, rel_path: &str) -> Result<PathBuf, AppCommandError> {
let rel = Path::new(rel_path); let rel = Path::new(rel_path);
if rel.is_absolute() { if rel.is_absolute() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input("Path must be relative"));
AppErrorCode::InvalidInput,
"Path must be relative",
));
} }
for component in rel.components() { for component in rel.components() {
match component { match component {
Component::Normal(_) | Component::CurDir => {} Component::Normal(_) | Component::CurDir => {}
Component::ParentDir => { Component::ParentDir => {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input("Path cannot contain '..'"));
AppErrorCode::InvalidInput,
"Path cannot contain '..'",
));
} }
Component::RootDir | Component::Prefix(_) => { Component::RootDir | Component::Prefix(_) => {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input("Invalid path component"));
AppErrorCode::InvalidInput,
"Invalid path component",
));
} }
} }
} }
@@ -1562,20 +1530,13 @@ fn resolve_tree_path(root: &Path, rel_path: &str) -> Result<PathBuf, AppCommandE
fn validate_new_name(new_name: &str) -> Result<&str, AppCommandError> { fn validate_new_name(new_name: &str) -> Result<&str, AppCommandError> {
let trimmed = new_name.trim(); let trimmed = new_name.trim();
if trimmed.is_empty() { if trimmed.is_empty() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input("New name cannot be empty"));
AppErrorCode::InvalidInput,
"New name cannot be empty",
));
} }
if trimmed == "." || trimmed == ".." { if trimmed == "." || trimmed == ".." {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input("Invalid file name"));
AppErrorCode::InvalidInput,
"Invalid file name",
));
} }
if trimmed.contains('/') || trimmed.contains('\\') { if trimmed.contains('/') || trimmed.contains('\\') {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"New name cannot contain path separators", "New name cannot contain path separators",
)); ));
} }
@@ -1589,10 +1550,7 @@ pub async fn start_file_tree_watch(
) -> Result<(), AppCommandError> { ) -> Result<(), AppCommandError> {
let root = PathBuf::from(&root_path); let root = PathBuf::from(&root_path);
if !root.exists() || !root.is_dir() { if !root.exists() || !root.is_dir() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("Folder does not exist"));
AppErrorCode::NotFound,
"Folder does not exist",
));
} }
let (root_canonical, key) = canonicalize_watch_root(&root)?; let (root_canonical, key) = canonicalize_watch_root(&root)?;
@@ -1636,8 +1594,7 @@ pub async fn start_file_tree_watch(
}, },
) )
.map_err(|e| { .map_err(|e| {
AppCommandError::new(AppErrorCode::IoError, "Failed to create file watcher") AppCommandError::io_error("Failed to create file watcher").with_detail(e.to_string())
.with_detail(e.to_string())
})?, })?,
); );
@@ -1646,8 +1603,7 @@ pub async fn start_file_tree_watch(
.ok_or_else(|| AppCommandError::task_execution_failed("Failed to create file watcher"))? .ok_or_else(|| AppCommandError::task_execution_failed("Failed to create file watcher"))?
.watch(&root_canonical, RecursiveMode::Recursive) .watch(&root_canonical, RecursiveMode::Recursive)
.map_err(|e| { .map_err(|e| {
AppCommandError::new(AppErrorCode::IoError, "Failed to start file watcher") AppCommandError::io_error("Failed to start file watcher").with_detail(e.to_string())
.with_detail(e.to_string())
})?; })?;
let should_cleanup_new_watcher = { let should_cleanup_new_watcher = {
@@ -1789,8 +1745,7 @@ fn ensure_path_in_workspace(root: &Path, target: &Path) -> Result<(), AppCommand
let canonical_root = std::fs::canonicalize(root).map_err(AppCommandError::io)?; let canonical_root = std::fs::canonicalize(root).map_err(AppCommandError::io)?;
let canonical_target = std::fs::canonicalize(target).map_err(AppCommandError::io)?; let canonical_target = std::fs::canonicalize(target).map_err(AppCommandError::io)?;
if !canonical_target.starts_with(&canonical_root) { if !canonical_target.starts_with(&canonical_root) {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"Path is outside workspace root", "Path is outside workspace root",
)); ));
} }
@@ -1807,8 +1762,7 @@ fn read_text_preview(target: &Path, limit: usize) -> Result<(String, bool), AppC
.map_err(AppCommandError::io)?; .map_err(AppCommandError::io)?;
if bytes.iter().take(2_048).any(|b| *b == 0) { if bytes.iter().take(2_048).any(|b| *b == 0) {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"Binary files are not supported in preview", "Binary files are not supported in preview",
)); ));
} }
@@ -1822,18 +1776,14 @@ fn read_text_preview(target: &Path, limit: usize) -> Result<(String, bool), AppC
fn atomic_write_text(path: &Path, bytes: &[u8]) -> Result<(), AppCommandError> { fn atomic_write_text(path: &Path, bytes: &[u8]) -> Result<(), AppCommandError> {
let parent = path.parent().ok_or_else(|| { let parent = path.parent().ok_or_else(|| {
AppCommandError::new( AppCommandError::invalid_input("Cannot determine parent directory for target file")
AppErrorCode::InvalidInput, .with_detail(path.display().to_string())
"Cannot determine parent directory for target file",
)
.with_detail(path.display().to_string())
})?; })?;
if !parent.exists() { if !parent.exists() {
return Err(AppCommandError::new( return Err(
AppErrorCode::NotFound, AppCommandError::not_found("Parent directory does not exist")
"Parent directory does not exist", .with_detail(parent.display().to_string()),
) );
.with_detail(parent.display().to_string()));
} }
let temp_path = parent.join(format!( let temp_path = parent.join(format!(
@@ -1902,11 +1852,10 @@ fn replace_file(temp_path: &Path, target_path: &Path) -> Result<(), AppCommandEr
}; };
if ok == 0 { if ok == 0 {
return Err(AppCommandError::new( return Err(
AppErrorCode::IoError, AppCommandError::io_error("Failed to atomically replace file")
"Failed to atomically replace file", .with_detail(std::io::Error::last_os_error().to_string()),
) );
.with_detail(std::io::Error::last_os_error().to_string()));
} }
Ok(()) Ok(())
@@ -1970,8 +1919,7 @@ pub async fn get_file_tree(
}) })
{ {
let entry = entry.map_err(|e| { let entry = entry.map_err(|e| {
AppCommandError::new(AppErrorCode::IoError, "Failed to walk file tree") AppCommandError::io_error("Failed to walk file tree").with_detail(e.to_string())
.with_detail(e.to_string())
})?; })?;
let entry_path = entry.path().to_path_buf(); let entry_path = entry.path().to_path_buf();
@@ -2088,24 +2036,15 @@ pub async fn read_file_preview(
) -> Result<FilePreviewContent, AppCommandError> { ) -> Result<FilePreviewContent, AppCommandError> {
let root = PathBuf::from(&root_path); let root = PathBuf::from(&root_path);
if !root.exists() || !root.is_dir() { if !root.exists() || !root.is_dir() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("Folder does not exist"));
AppErrorCode::NotFound,
"Folder does not exist",
));
} }
let target = resolve_tree_path(&root, &path)?; let target = resolve_tree_path(&root, &path)?;
if !target.exists() { if !target.exists() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("File does not exist"));
AppErrorCode::NotFound,
"File does not exist",
));
} }
if !target.is_file() { if !target.is_file() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input("Path is not a file"));
AppErrorCode::InvalidInput,
"Path is not a file",
));
} }
let limit = max_bytes let limit = max_bytes
.unwrap_or(FILE_PREVIEW_DEFAULT_MAX_BYTES) .unwrap_or(FILE_PREVIEW_DEFAULT_MAX_BYTES)
@@ -2132,24 +2071,15 @@ pub async fn read_file_for_edit(
) -> Result<FileEditContent, AppCommandError> { ) -> Result<FileEditContent, AppCommandError> {
let root = PathBuf::from(&root_path); let root = PathBuf::from(&root_path);
if !root.exists() || !root.is_dir() { if !root.exists() || !root.is_dir() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("Folder does not exist"));
AppErrorCode::NotFound,
"Folder does not exist",
));
} }
let target = resolve_tree_path(&root, &path)?; let target = resolve_tree_path(&root, &path)?;
if !target.exists() { if !target.exists() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("File does not exist"));
AppErrorCode::NotFound,
"File does not exist",
));
} }
if !target.is_file() { if !target.is_file() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input("Path is not a file"));
AppErrorCode::InvalidInput,
"Path is not a file",
));
} }
let limit = max_bytes let limit = max_bytes
@@ -2193,31 +2123,21 @@ pub async fn save_file_content(
) -> Result<FileSaveResult, AppCommandError> { ) -> Result<FileSaveResult, AppCommandError> {
let root = PathBuf::from(&root_path); let root = PathBuf::from(&root_path);
if !root.exists() || !root.is_dir() { if !root.exists() || !root.is_dir() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("Folder does not exist"));
AppErrorCode::NotFound,
"Folder does not exist",
));
} }
if content.len() > FILE_EDIT_MAX_BYTES { if content.len() > FILE_EDIT_MAX_BYTES {
return Err(AppCommandError::new( return Err(
AppErrorCode::InvalidInput, AppCommandError::invalid_input("File is too large to save in editor")
"File is too large to save in editor", .with_detail(format!("max_bytes={FILE_EDIT_MAX_BYTES}")),
) );
.with_detail(format!("max_bytes={FILE_EDIT_MAX_BYTES}")));
} }
let target = resolve_tree_path(&root, &path)?; let target = resolve_tree_path(&root, &path)?;
if !target.exists() { if !target.exists() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("File does not exist"));
AppErrorCode::NotFound,
"File does not exist",
));
} }
if !target.is_file() { if !target.is_file() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input("Path is not a file"));
AppErrorCode::InvalidInput,
"Path is not a file",
));
} }
let path_for_response = path.clone(); let path_for_response = path.clone();
@@ -2226,32 +2146,26 @@ pub async fn save_file_content(
let link_meta = std::fs::symlink_metadata(&target).map_err(AppCommandError::io)?; let link_meta = std::fs::symlink_metadata(&target).map_err(AppCommandError::io)?;
if link_meta.file_type().is_symlink() { if link_meta.file_type().is_symlink() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"Saving symlink targets is not supported", "Saving symlink targets is not supported",
)); ));
} }
let before_meta = std::fs::metadata(&target).map_err(AppCommandError::io)?; let before_meta = std::fs::metadata(&target).map_err(AppCommandError::io)?;
if before_meta.permissions().readonly() { if before_meta.permissions().readonly() {
return Err(AppCommandError::new( return Err(AppCommandError::permission_denied("File is read-only"));
AppErrorCode::PermissionDenied,
"File is read-only",
));
} }
let current_bytes = std::fs::read(&target).map_err(AppCommandError::io)?; let current_bytes = std::fs::read(&target).map_err(AppCommandError::io)?;
if current_bytes.iter().take(2_048).any(|b| *b == 0) { if current_bytes.iter().take(2_048).any(|b| *b == 0) {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"Binary files are not supported in editor", "Binary files are not supported in editor",
)); ));
} }
let current_etag = compute_etag(&current_bytes, &before_meta); let current_etag = compute_etag(&current_bytes, &before_meta);
if let Some(expected) = expected_etag { if let Some(expected) = expected_etag {
if expected != current_etag { if expected != current_etag {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"File has changed on disk. Reload the file before saving.", "File has changed on disk. Reload the file before saving.",
)); ));
} }
@@ -2308,31 +2222,21 @@ pub async fn save_file_copy(
) -> Result<FileSaveResult, AppCommandError> { ) -> Result<FileSaveResult, AppCommandError> {
let root = PathBuf::from(&root_path); let root = PathBuf::from(&root_path);
if !root.exists() || !root.is_dir() { if !root.exists() || !root.is_dir() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("Folder does not exist"));
AppErrorCode::NotFound,
"Folder does not exist",
));
} }
if content.len() > FILE_EDIT_MAX_BYTES { if content.len() > FILE_EDIT_MAX_BYTES {
return Err(AppCommandError::new( return Err(
AppErrorCode::InvalidInput, AppCommandError::invalid_input("File is too large to save in editor")
"File is too large to save in editor", .with_detail(format!("max_bytes={FILE_EDIT_MAX_BYTES}")),
) );
.with_detail(format!("max_bytes={FILE_EDIT_MAX_BYTES}")));
} }
let source = resolve_tree_path(&root, &path)?; let source = resolve_tree_path(&root, &path)?;
if !source.exists() { if !source.exists() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("File does not exist"));
AppErrorCode::NotFound,
"File does not exist",
));
} }
if !source.is_file() { if !source.is_file() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input("Path is not a file"));
AppErrorCode::InvalidInput,
"Path is not a file",
));
} }
run_file_io(move || { run_file_io(move || {
@@ -2340,8 +2244,7 @@ pub async fn save_file_copy(
let source_meta = std::fs::symlink_metadata(&source).map_err(AppCommandError::io)?; let source_meta = std::fs::symlink_metadata(&source).map_err(AppCommandError::io)?;
if source_meta.file_type().is_symlink() { if source_meta.file_type().is_symlink() {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"Saving symlink targets is not supported", "Saving symlink targets is not supported",
)); ));
} }
@@ -2349,10 +2252,7 @@ pub async fn save_file_copy(
let parent = source let parent = source
.parent() .parent()
.ok_or_else(|| { .ok_or_else(|| {
AppCommandError::new( AppCommandError::invalid_input("Cannot determine parent directory for source file")
AppErrorCode::InvalidInput,
"Cannot determine parent directory for source file",
)
})? })?
.to_path_buf(); .to_path_buf();
ensure_path_in_workspace(&root, &parent)?; ensure_path_in_workspace(&root, &parent)?;
@@ -2360,12 +2260,7 @@ pub async fn save_file_copy(
let source_name = source let source_name = source
.file_name() .file_name()
.map(|value| value.to_string_lossy().to_string()) .map(|value| value.to_string_lossy().to_string())
.ok_or_else(|| { .ok_or_else(|| AppCommandError::invalid_input("Cannot determine source file name"))?;
AppCommandError::new(
AppErrorCode::InvalidInput,
"Cannot determine source file name",
)
})?;
let mut created_path: Option<PathBuf> = None; let mut created_path: Option<PathBuf> = None;
for attempt in 1..=9_999 { for attempt in 1..=9_999 {
@@ -2379,8 +2274,7 @@ pub async fn save_file_copy(
} }
let created_path = created_path.ok_or_else(|| { let created_path = created_path.ok_or_else(|| {
AppCommandError::new( AppCommandError::already_exists(
AppErrorCode::AlreadyExists,
"Unable to create copy file: too many existing local copies", "Unable to create copy file: too many existing local copies",
) )
})?; })?;
@@ -2394,11 +2288,8 @@ pub async fn save_file_copy(
let rel_path = created_path let rel_path = created_path
.strip_prefix(&root) .strip_prefix(&root)
.map_err(|e| { .map_err(|e| {
AppCommandError::new( AppCommandError::invalid_input("Failed to compute relative path for copy")
AppErrorCode::InvalidInput, .with_detail(e.to_string())
"Failed to compute relative path for copy",
)
.with_detail(e.to_string())
})? })?
.to_string_lossy() .to_string_lossy()
.replace('\\', "/"); .replace('\\', "/");
@@ -2422,32 +2313,22 @@ pub async fn rename_file_tree_entry(
) -> Result<String, AppCommandError> { ) -> Result<String, AppCommandError> {
let root = PathBuf::from(&root_path); let root = PathBuf::from(&root_path);
if !root.exists() || !root.is_dir() { if !root.exists() || !root.is_dir() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("Folder does not exist"));
AppErrorCode::NotFound,
"Folder does not exist",
));
} }
let target = resolve_tree_path(&root, &path)?; let target = resolve_tree_path(&root, &path)?;
if !target.exists() { if !target.exists() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("Target file does not exist"));
AppErrorCode::NotFound,
"Target file does not exist",
));
} }
if target == root { if target == root {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"Cannot rename workspace root", "Cannot rename workspace root",
)); ));
} }
let parent = target.parent().ok_or_else(|| { let parent = target
AppCommandError::new( .parent()
AppErrorCode::InvalidInput, .ok_or_else(|| AppCommandError::invalid_input("Cannot rename path without parent"))?;
"Cannot rename path without parent",
)
})?;
let validated_name = validate_new_name(&new_name)?; let validated_name = validate_new_name(&new_name)?;
let next_path = parent.join(validated_name); let next_path = parent.join(validated_name);
@@ -2455,8 +2336,7 @@ pub async fn rename_file_tree_entry(
return Ok(path); return Ok(path);
} }
if next_path.exists() { if next_path.exists() {
return Err(AppCommandError::new( return Err(AppCommandError::already_exists(
AppErrorCode::AlreadyExists,
"A file with this name already exists", "A file with this name already exists",
)); ));
} }
@@ -2466,11 +2346,8 @@ pub async fn rename_file_tree_entry(
let rel = next_path let rel = next_path
.strip_prefix(&root) .strip_prefix(&root)
.map_err(|e| { .map_err(|e| {
AppCommandError::new( AppCommandError::invalid_input("Failed to compute relative path")
AppErrorCode::InvalidInput, .with_detail(e.to_string())
"Failed to compute relative path",
)
.with_detail(e.to_string())
})? })?
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
@@ -2484,22 +2361,15 @@ pub async fn delete_file_tree_entry(
) -> Result<(), AppCommandError> { ) -> Result<(), AppCommandError> {
let root = PathBuf::from(&root_path); let root = PathBuf::from(&root_path);
if !root.exists() || !root.is_dir() { if !root.exists() || !root.is_dir() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("Folder does not exist"));
AppErrorCode::NotFound,
"Folder does not exist",
));
} }
let target = resolve_tree_path(&root, &path)?; let target = resolve_tree_path(&root, &path)?;
if !target.exists() { if !target.exists() {
return Err(AppCommandError::new( return Err(AppCommandError::not_found("Target file does not exist"));
AppErrorCode::NotFound,
"Target file does not exist",
));
} }
if target == root { if target == root {
return Err(AppCommandError::new( return Err(AppCommandError::invalid_input(
AppErrorCode::InvalidInput,
"Cannot delete workspace root", "Cannot delete workspace root",
)); ));
} }

View File

@@ -1,7 +1,7 @@
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use tauri::State; use tauri::State;
use crate::app_error::{AppCommandError, AppErrorCode}; use crate::app_error::AppCommandError;
use crate::db::service::app_metadata_service; use crate::db::service::app_metadata_service;
use crate::db::AppDatabase; use crate::db::AppDatabase;
use crate::models::{SystemLanguageSettings, SystemProxySettings}; use crate::models::{SystemLanguageSettings, SystemProxySettings};
@@ -33,15 +33,11 @@ fn normalize_proxy_settings(
.map(str::trim) .map(str::trim)
.filter(|value| !value.is_empty()) .filter(|value| !value.is_empty())
.ok_or_else(|| { .ok_or_else(|| {
AppCommandError::new( AppCommandError::configuration_missing("Proxy URL is required when proxy is enabled")
AppErrorCode::ConfigurationMissing,
"Proxy URL is required when proxy is enabled",
)
})?; })?;
reqwest::Proxy::all(proxy_url).map_err(|e| { reqwest::Proxy::all(proxy_url).map_err(|e| {
AppCommandError::new(AppErrorCode::ConfigurationInvalid, "Invalid proxy URL") AppCommandError::configuration_invalid("Invalid proxy URL").with_detail(e.to_string())
.with_detail(e.to_string())
})?; })?;
Ok(SystemProxySettings { Ok(SystemProxySettings {
@@ -62,11 +58,8 @@ pub(crate) async fn load_system_proxy_settings(
}; };
let parsed = serde_json::from_str::<SystemProxySettings>(&raw).map_err(|e| { let parsed = serde_json::from_str::<SystemProxySettings>(&raw).map_err(|e| {
AppCommandError::new( AppCommandError::configuration_invalid("Failed to parse stored proxy settings")
AppErrorCode::ConfigurationInvalid, .with_detail(e.to_string())
"Failed to parse stored proxy settings",
)
.with_detail(e.to_string())
})?; })?;
normalize_proxy_settings(parsed) normalize_proxy_settings(parsed)
} }
@@ -83,11 +76,8 @@ pub(crate) async fn load_system_language_settings(
}; };
serde_json::from_str::<SystemLanguageSettings>(&raw).map_err(|e| { serde_json::from_str::<SystemLanguageSettings>(&raw).map_err(|e| {
AppCommandError::new( AppCommandError::configuration_invalid("Failed to parse stored language settings")
AppErrorCode::ConfigurationInvalid, .with_detail(e.to_string())
"Failed to parse stored language settings",
)
.with_detail(e.to_string())
}) })
} }
@@ -105,11 +95,8 @@ pub async fn update_system_proxy_settings(
) -> Result<SystemProxySettings, AppCommandError> { ) -> Result<SystemProxySettings, AppCommandError> {
let normalized = normalize_proxy_settings(settings)?; let normalized = normalize_proxy_settings(settings)?;
let serialized = serde_json::to_string(&normalized).map_err(|e| { let serialized = serde_json::to_string(&normalized).map_err(|e| {
AppCommandError::new( AppCommandError::invalid_input("Failed to serialize proxy settings")
AppErrorCode::InvalidInput, .with_detail(e.to_string())
"Failed to serialize proxy settings",
)
.with_detail(e.to_string())
})?; })?;
app_metadata_service::upsert_value(&db.conn, SYSTEM_PROXY_SETTINGS_KEY, &serialized) app_metadata_service::upsert_value(&db.conn, SYSTEM_PROXY_SETTINGS_KEY, &serialized)
@@ -133,11 +120,8 @@ pub async fn update_system_language_settings(
db: State<'_, AppDatabase>, db: State<'_, AppDatabase>,
) -> Result<SystemLanguageSettings, AppCommandError> { ) -> Result<SystemLanguageSettings, AppCommandError> {
let serialized = serde_json::to_string(&settings).map_err(|e| { let serialized = serde_json::to_string(&settings).map_err(|e| {
AppCommandError::new( AppCommandError::invalid_input("Failed to serialize language settings")
AppErrorCode::InvalidInput, .with_detail(e.to_string())
"Failed to serialize language settings",
)
.with_detail(e.to_string())
})?; })?;
app_metadata_service::upsert_value(&db.conn, SYSTEM_LANGUAGE_SETTINGS_KEY, &serialized) app_metadata_service::upsert_value(&db.conn, SYSTEM_LANGUAGE_SETTINGS_KEY, &serialized)

View File

@@ -3,7 +3,7 @@ use std::sync::Mutex;
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
use crate::app_error::{AppCommandError, AppErrorCode}; use crate::app_error::AppCommandError;
use crate::db::AppDatabase; use crate::db::AppDatabase;
use crate::models::FolderHistoryEntry; use crate::models::FolderHistoryEntry;
@@ -191,11 +191,10 @@ pub async fn focus_folder_window(app: AppHandle, folder_id: i32) -> Result<(), A
} }
} }
} }
Err(AppCommandError::new( Err(
AppErrorCode::NotFound, AppCommandError::not_found(format!("No open window for folder {folder_id}"))
format!("No open window for folder {folder_id}"), .with_detail(format!("folder_id={folder_id}")),
) )
.with_detail(format!("folder_id={folder_id}")))
} }
#[tauri::command] #[tauri::command]
@@ -259,11 +258,8 @@ pub async fn open_commit_window(
.await .await
.map_err(AppCommandError::from)? .map_err(AppCommandError::from)?
.ok_or_else(|| { .ok_or_else(|| {
AppCommandError::new( AppCommandError::not_found(format!("Folder {folder_id} not found"))
AppErrorCode::NotFound, .with_detail(format!("folder_id={folder_id}"))
format!("Folder {folder_id} not found"),
)
.with_detail(format!("folder_id={folder_id}"))
})?; })?;
let url = WebviewUrl::App(format!("commit?folderId={folder_id}").into()); let url = WebviewUrl::App(format!("commit?folderId={folder_id}").into());

View File

@@ -1,4 +1,4 @@
use crate::app_error::{AppCommandError, AppErrorCode}; use crate::app_error::AppCommandError;
use crate::models::SystemProxySettings; use crate::models::SystemProxySettings;
const PROXY_ENV_KEYS: [&str; 6] = [ const PROXY_ENV_KEYS: [&str; 6] = [
@@ -18,8 +18,7 @@ pub fn apply_system_proxy_settings(settings: &SystemProxySettings) -> Result<(),
.map(str::trim) .map(str::trim)
.filter(|value| !value.is_empty()) .filter(|value| !value.is_empty())
.ok_or_else(|| { .ok_or_else(|| {
AppCommandError::new( AppCommandError::configuration_missing(
AppErrorCode::ConfigurationMissing,
"Proxy URL is required when proxy is enabled", "Proxy URL is required when proxy is enabled",
) )
})?; })?;

View File

@@ -21,7 +21,6 @@ export type AgentType =
| "stakpak" | "stakpak"
export type AppErrorCode = export type AppErrorCode =
| "unknown"
| "invalid_input" | "invalid_input"
| "configuration_missing" | "configuration_missing"
| "configuration_invalid" | "configuration_invalid"