mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-03-31 17:12:09 +08:00
feat: add delete backup functionality with confirmation dialog
This commit is contained in:
@@ -168,3 +168,9 @@ pub fn rename_db_backup(
|
||||
) -> Result<String, String> {
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [deleteFilename, setDeleteFilename] = useState<string | null>(null);
|
||||
const [editingFilename, setEditingFilename] = useState<string | null>(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 */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">
|
||||
{t("settings.backupManager.title", {
|
||||
defaultValue: "Database Backups",
|
||||
})}
|
||||
</h4>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-sm font-medium">
|
||||
{t("settings.backupManager.title", {
|
||||
defaultValue: "Database Backups",
|
||||
})}
|
||||
</h4>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
disabled={isCreating || isRestoring}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await create();
|
||||
toast.success(
|
||||
t("settings.backupManager.createSuccess", {
|
||||
defaultValue: "Backup created successfully",
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
const detail =
|
||||
extractErrorMessage(error) ||
|
||||
t("settings.backupManager.createFailed", {
|
||||
defaultValue: "Backup failed",
|
||||
});
|
||||
toast.error(detail);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Download className="h-3 w-3 mr-1" />
|
||||
{isCreating
|
||||
? t("settings.backupManager.creating", {
|
||||
defaultValue: "Backing up...",
|
||||
})
|
||||
: t("settings.backupManager.createBackup", {
|
||||
defaultValue: "Backup Now",
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-sm text-muted-foreground py-2">Loading...</div>
|
||||
@@ -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",
|
||||
})}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:text-destructive"
|
||||
onClick={() => setDeleteFilename(backup.filename)}
|
||||
disabled={isRestoring || isDeleting}
|
||||
title={t("settings.backupManager.delete", {
|
||||
defaultValue: "Delete",
|
||||
})}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
disabled={isRestoring}
|
||||
disabled={isRestoring || isDeleting}
|
||||
onClick={() => setConfirmFilename(backup.filename)}
|
||||
>
|
||||
<RotateCcw className="h-3 w-3 mr-1" />
|
||||
@@ -329,7 +406,7 @@ export function BackupListSection({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
{/* Restore Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={!!confirmFilename}
|
||||
onOpenChange={(open) => !open && setConfirmFilename(null)}
|
||||
@@ -368,6 +445,50 @@ export function BackupListSection({
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={!!deleteFilename}
|
||||
onOpenChange={(open) => !open && setDeleteFilename(null)}
|
||||
>
|
||||
<DialogContent className="max-w-md" zIndex="alert">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("settings.backupManager.deleteConfirmTitle", {
|
||||
defaultValue: "Confirm Delete",
|
||||
})}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("settings.backupManager.deleteConfirmMessage", {
|
||||
defaultValue:
|
||||
"This backup will be permanently deleted. This action cannot be undone.",
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDeleteFilename(null)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{t("common.cancel", { defaultValue: "Cancel" })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting
|
||||
? t("settings.backupManager.deleting", {
|
||||
defaultValue: "Deleting...",
|
||||
})
|
||||
: t("settings.backupManager.delete", {
|
||||
defaultValue: "Delete",
|
||||
})}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -308,7 +308,17 @@
|
||||
"rename": "名前変更",
|
||||
"renameSuccess": "バックアップの名前を変更しました",
|
||||
"renameFailed": "名前変更に失敗しました",
|
||||
"namePlaceholder": "新しい名前を入力"
|
||||
"namePlaceholder": "新しい名前を入力",
|
||||
"createBackup": "今すぐバックアップ",
|
||||
"creating": "バックアップ中...",
|
||||
"createSuccess": "バックアップが作成されました",
|
||||
"createFailed": "バックアップに失敗しました",
|
||||
"delete": "削除",
|
||||
"deleting": "削除中...",
|
||||
"deleteSuccess": "バックアップを削除しました",
|
||||
"deleteFailed": "削除に失敗しました",
|
||||
"deleteConfirmTitle": "バックアップの削除を確認",
|
||||
"deleteConfirmMessage": "このバックアップは完全に削除されます。この操作は取り消せません。"
|
||||
},
|
||||
"webdavSync": {
|
||||
"title": "WebDAV クラウド同期",
|
||||
|
||||
@@ -308,7 +308,17 @@
|
||||
"rename": "重命名",
|
||||
"renameSuccess": "备份重命名成功",
|
||||
"renameFailed": "重命名失败",
|
||||
"namePlaceholder": "输入新名称"
|
||||
"namePlaceholder": "输入新名称",
|
||||
"createBackup": "立即备份",
|
||||
"creating": "备份中...",
|
||||
"createSuccess": "备份创建成功",
|
||||
"createFailed": "备份创建失败",
|
||||
"delete": "删除",
|
||||
"deleting": "删除中...",
|
||||
"deleteSuccess": "备份已删除",
|
||||
"deleteFailed": "删除失败",
|
||||
"deleteConfirmTitle": "确认删除备份",
|
||||
"deleteConfirmMessage": "此备份将被永久删除,此操作无法撤消。"
|
||||
},
|
||||
"webdavSync": {
|
||||
"title": "WebDAV 云同步",
|
||||
|
||||
@@ -238,4 +238,8 @@ export const backupsApi = {
|
||||
async renameDbBackup(oldFilename: string, newName: string): Promise<string> {
|
||||
return await invoke("rename_db_backup", { oldFilename, newName });
|
||||
},
|
||||
|
||||
async deleteDbBackup(filename: string): Promise<void> {
|
||||
await invoke("delete_db_backup", { filename });
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user