diff --git a/src-tauri/src/commands/import_export.rs b/src-tauri/src/commands/import_export.rs index ae7dab36..be2fc3d3 100644 --- a/src-tauri/src/commands/import_export.rs +++ b/src-tauri/src/commands/import_export.rs @@ -168,3 +168,9 @@ pub fn rename_db_backup( ) -> Result { Database::rename_backup(&oldFilename, &newName).map_err(|e| e.to_string()) } + +/// Delete a database backup file +#[tauri::command] +pub fn delete_db_backup(filename: String) -> Result<(), String> { + Database::delete_backup(&filename).map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/database/backup.rs b/src-tauri/src/database/backup.rs index b3d98df8..a1c7e80c 100644 --- a/src-tauri/src/database/backup.rs +++ b/src-tauri/src/database/backup.rs @@ -531,4 +531,29 @@ impl Database { log::info!("Renamed backup: {old_filename} -> {new_filename}"); Ok(new_filename) } + + /// Delete a backup file permanently. + pub fn delete_backup(filename: &str) -> Result<(), AppError> { + // Validate filename (path traversal + .db suffix) + if filename.contains("..") + || filename.contains('/') + || filename.contains('\\') + || !filename.ends_with(".db") + { + return Err(AppError::InvalidInput( + "Invalid backup filename".to_string(), + )); + } + + let backup_path = get_app_config_dir().join("backups").join(filename); + if !backup_path.exists() { + return Err(AppError::InvalidInput(format!( + "Backup file not found: {filename}" + ))); + } + + fs::remove_file(&backup_path).map_err(|e| AppError::io(&backup_path, e))?; + log::info!("Deleted backup: {filename}"); + Ok(()) + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 879fe81e..1ba9229d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -911,9 +911,11 @@ pub fn run() { commands::save_file_dialog, commands::open_file_dialog, commands::open_zip_file_dialog, + commands::create_db_backup, commands::list_db_backups, commands::restore_db_backup, commands::rename_db_backup, + commands::delete_db_backup, commands::sync_current_providers_live, // Deep link import commands::parse_deeplink, diff --git a/src/components/settings/BackupListSection.tsx b/src/components/settings/BackupListSection.tsx index b5c587ba..d07f8fec 100644 --- a/src/components/settings/BackupListSection.tsx +++ b/src/components/settings/BackupListSection.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import { Pencil, RotateCcw, Check, X } from "lucide-react"; +import { Pencil, RotateCcw, Check, X, Download, Trash2 } from "lucide-react"; import { Dialog, DialogContent, @@ -67,9 +67,20 @@ export function BackupListSection({ onSettingsChange, }: BackupListSectionProps) { const { t } = useTranslation(); - const { backups, isLoading, restore, isRestoring, rename, isRenaming } = - useBackupManager(); + const { + backups, + isLoading, + create, + isCreating, + restore, + isRestoring, + rename, + isRenaming, + remove, + isDeleting, + } = useBackupManager(); const [confirmFilename, setConfirmFilename] = useState(null); + const [deleteFilename, setDeleteFilename] = useState(null); const [editingFilename, setEditingFilename] = useState(null); const [editValue, setEditValue] = useState(""); @@ -110,6 +121,26 @@ export function BackupListSection({ setEditValue(""); }; + const handleDelete = async () => { + if (!deleteFilename) return; + try { + await remove(deleteFilename); + setDeleteFilename(null); + toast.success( + t("settings.backupManager.deleteSuccess", { + defaultValue: "Backup deleted", + }), + ); + } catch (error) { + const detail = + extractErrorMessage(error) || + t("settings.backupManager.deleteFailed", { + defaultValue: "Delete failed", + }); + toast.error(detail); + } + }; + const handleConfirmRename = async () => { if (!editingFilename || !editValue.trim()) return; try { @@ -221,11 +252,45 @@ export function BackupListSection({ {/* Backup list */}
-

- {t("settings.backupManager.title", { - defaultValue: "Database Backups", - })} -

+
+

+ {t("settings.backupManager.title", { + defaultValue: "Database Backups", + })} +

+ +
{isLoading ? (
Loading...
@@ -298,18 +363,30 @@ export function BackupListSection({ size="icon" className="h-7 w-7" onClick={() => handleStartRename(backup.filename)} - disabled={isRestoring || isRenaming} + disabled={isRestoring || isRenaming || isDeleting} title={t("settings.backupManager.rename", { defaultValue: "Rename", })} > +
- {/* Confirmation Dialog */} + {/* Restore Confirmation Dialog */} !open && setConfirmFilename(null)} @@ -368,6 +445,50 @@ export function BackupListSection({ + + {/* Delete Confirmation Dialog */} + !open && setDeleteFilename(null)} + > + + + + {t("settings.backupManager.deleteConfirmTitle", { + defaultValue: "Confirm Delete", + })} + + + {t("settings.backupManager.deleteConfirmMessage", { + defaultValue: + "This backup will be permanently deleted. This action cannot be undone.", + })} + + + + + + + + ); } diff --git a/src/hooks/useBackupManager.ts b/src/hooks/useBackupManager.ts index f6b2a8c6..82202523 100644 --- a/src/hooks/useBackupManager.ts +++ b/src/hooks/useBackupManager.ts @@ -13,6 +13,11 @@ export function useBackupManager() { queryFn: () => backupsApi.listDbBackups(), }); + const createMutation = useMutation({ + mutationFn: () => backupsApi.createDbBackup(), + onSuccess: () => refetch(), + }); + const restoreMutation = useMutation({ mutationFn: (filename: string) => backupsApi.restoreDbBackup(filename), onSuccess: async () => { @@ -34,12 +39,21 @@ export function useBackupManager() { onSuccess: () => refetch(), }); + const deleteMutation = useMutation({ + mutationFn: (filename: string) => backupsApi.deleteDbBackup(filename), + onSuccess: () => refetch(), + }); + return { backups, isLoading, + create: createMutation.mutateAsync, + isCreating: createMutation.isPending, restore: restoreMutation.mutateAsync, isRestoring: restoreMutation.isPending, rename: renameMutation.mutateAsync, isRenaming: renameMutation.isPending, + remove: deleteMutation.mutateAsync, + isDeleting: deleteMutation.isPending, }; } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 3677ad3d..8984d9f9 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -308,7 +308,17 @@ "rename": "Rename", "renameSuccess": "Backup renamed", "renameFailed": "Rename failed", - "namePlaceholder": "Enter new name" + "namePlaceholder": "Enter new name", + "createBackup": "Backup Now", + "creating": "Backing up...", + "createSuccess": "Backup created successfully", + "createFailed": "Backup failed", + "delete": "Delete", + "deleting": "Deleting...", + "deleteSuccess": "Backup deleted", + "deleteFailed": "Delete failed", + "deleteConfirmTitle": "Confirm Delete", + "deleteConfirmMessage": "This backup will be permanently deleted. This action cannot be undone." }, "webdavSync": { "title": "WebDAV Cloud Sync", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 2bf121ea..7da0b0e8 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -308,7 +308,17 @@ "rename": "名前変更", "renameSuccess": "バックアップの名前を変更しました", "renameFailed": "名前変更に失敗しました", - "namePlaceholder": "新しい名前を入力" + "namePlaceholder": "新しい名前を入力", + "createBackup": "今すぐバックアップ", + "creating": "バックアップ中...", + "createSuccess": "バックアップが作成されました", + "createFailed": "バックアップに失敗しました", + "delete": "削除", + "deleting": "削除中...", + "deleteSuccess": "バックアップを削除しました", + "deleteFailed": "削除に失敗しました", + "deleteConfirmTitle": "バックアップの削除を確認", + "deleteConfirmMessage": "このバックアップは完全に削除されます。この操作は取り消せません。" }, "webdavSync": { "title": "WebDAV クラウド同期", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 746f69e9..0f58f920 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -308,7 +308,17 @@ "rename": "重命名", "renameSuccess": "备份重命名成功", "renameFailed": "重命名失败", - "namePlaceholder": "输入新名称" + "namePlaceholder": "输入新名称", + "createBackup": "立即备份", + "creating": "备份中...", + "createSuccess": "备份创建成功", + "createFailed": "备份创建失败", + "delete": "删除", + "deleting": "删除中...", + "deleteSuccess": "备份已删除", + "deleteFailed": "删除失败", + "deleteConfirmTitle": "确认删除备份", + "deleteConfirmMessage": "此备份将被永久删除,此操作无法撤消。" }, "webdavSync": { "title": "WebDAV 云同步", diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index 6adb9bb8..e02e2a18 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -238,4 +238,8 @@ export const backupsApi = { async renameDbBackup(oldFilename: string, newName: string): Promise { return await invoke("rename_db_backup", { oldFilename, newName }); }, + + async deleteDbBackup(filename: string): Promise { + await invoke("delete_db_backup", { filename }); + }, };