From 488f746683cc662e0a8db860be22d990cd98b2c0 Mon Sep 17 00:00:00 2001 From: Sergey Kurdin Date: Thu, 12 Jun 2025 16:39:57 -0400 Subject: [PATCH] Enhance backup and restore features with improved UI, new translations, and error handling for better user experience --- .gitignore | 1 + .../src/components/ui/use-toast.ts | 1 + .../pastebar-app-ui/src/layout/NavBar.tsx | 11 +- .../src/locales/lang/en/backuprestore.yaml | 61 +- .../src/locales/lang/en/common.yaml | 2 + .../src/locales/lang/esES/backuprestore.yaml | 20 +- .../src/locales/lang/fr/backuprestore.yaml | 20 +- .../src/locales/lang/it/backuprestore.yaml | 20 +- .../src/locales/lang/ru/backuprestore.yaml | 20 +- .../src/locales/lang/ru/common.yaml | 1 + .../src/locales/lang/tr/backuprestore.yaml | 22 +- .../src/locales/lang/uk/backuprestore.yaml | 20 +- .../src/locales/lang/zhCN/backuprestore.yaml | 20 +- .../pages/settings/BackupRestoreSettings.tsx | 765 ++++++++++++------ .../src/pages/settings/UserPreferences.tsx | 3 +- .../pastebar-app-ui/src/styles/globals.css | 6 +- .../src/commands/backup_restore_commands.rs | 140 ++-- src-tauri/src/main.rs | 1 - 18 files changed, 765 insertions(+), 369 deletions(-) diff --git a/.gitignore b/.gitignore index 60cc568b..50edf5f2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ pnpm-debug.log* lerna-debug.log* local.pastebar.* safelist.txt +pastebar-data-backup* clipboard-images/**/* clip-images/**/* diff --git a/packages/pastebar-app-ui/src/components/ui/use-toast.ts b/packages/pastebar-app-ui/src/components/ui/use-toast.ts index 607d1870..ce8fa843 100644 --- a/packages/pastebar-app-ui/src/components/ui/use-toast.ts +++ b/packages/pastebar-app-ui/src/components/ui/use-toast.ts @@ -11,6 +11,7 @@ export type ToasterToast = { description?: React.ReactNode open?: boolean className?: string + variant?: 'default' | 'success' | 'warning' | 'danger' | 'destructive' | 'info' | null onOpenChange?: (open: boolean) => void duration?: number onDismiss?: () => void diff --git a/packages/pastebar-app-ui/src/layout/NavBar.tsx b/packages/pastebar-app-ui/src/layout/NavBar.tsx index 9cef6d5b..cd2d2a77 100644 --- a/packages/pastebar-app-ui/src/layout/NavBar.tsx +++ b/packages/pastebar-app-ui/src/layout/NavBar.tsx @@ -591,6 +591,13 @@ export function NavBar() { + { + navigate('/app-settings/backup-restore', { replace: true }) + }} + > + {t('Backup and Restore', { ns: 'backuprestore' })} + { navigate('/app-settings/security', { replace: true }) @@ -653,9 +660,7 @@ export function NavBar() { > {t('Enable Image Capture', { ns: 'history' })} diff --git a/packages/pastebar-app-ui/src/locales/lang/en/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/en/backuprestore.yaml index 1d7a2b27..dbcdb0c8 100644 --- a/packages/pastebar-app-ui/src/locales/lang/en/backuprestore.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/en/backuprestore.yaml @@ -1,27 +1,46 @@ -Backup and Restore: Backup and Restore -Create Backup: Create Backup -Include images in backup: Include images in backup -Backup Now: Backup Now -Restore Data: Restore Data -Restore from File...: Restore from File... -Select backup file: Select backup file +Auto Backup on Restore: Auto Backup on Restore Available Backups: Available Backups -Total backup space: "{{size}}": Total backup space: {{size}} +Backup Files: Backup Files +Backup Now: Backup Now +Backup and Restore: Backup and Restore +Backup created successfully: Backup created successfully +Backup deleted successfully: Backup deleted successfully +Backup on Restore: Backup on Restore +Browse: Browse +Create Backup: Create Backup +Create a backup of your data?: Create a backup of your data? +Created: Created +Creating backup...: Creating backup... +Delete: Delete +Delete this backup? This action cannot be undone.: Delete this backup? This action cannot be undone. +Failed to create backup: Failed to create backup +Failed to delete backup: Failed to delete backup +Failed to load backup list: Failed to load backup list +Failed to open backup folder: Failed to open backup folder +Failed to restore backup: Failed to restore backup +Failed to select backup file: Failed to select backup file +Include images in backup: Include images in backup +Invalid backup file: Invalid backup file +Loading backups...: Loading backups... +Move to another location?: Move to another location? No backups found: No backups found Restore: Restore -Delete: Delete -Create a backup of your data?: Create a backup of your data? -Backup created successfully: Backup created successfully -Move to another location?: Move to another location? -This will replace all current data. Are you sure?: This will replace all current data. Are you sure? -Restore from "{{filename}}"? This will replace all current data.: Restore from {{filename}}? This will replace all current data. -Delete this backup? This action cannot be undone.: Delete this backup? This action cannot be undone. +Restore Data: Restore Data Restore completed. The application will restart.: Restore completed. The application will restart. -Creating backup...: Creating backup... +Restore from "{{filename}}"? This will replace all current data.: Restore from {{filename}}? This will replace all current data. +Restore from File...: Restore from File... +Restore from {{filename}}? This will replace all current data.: Restore from {{filename}}? This will replace all current data. +Restored from {{filename}}: Restored from {{filename}} +Restoring a backup will automatically restart the application.: Restoring a backup will automatically restart the application. Restoring backup...: Restoring backup... -Backup deleted successfully: Backup deleted successfully -Failed to delete backup: Failed to delete backup -Invalid backup file: Invalid backup file +Restoring...: Restoring... +Select backup file: Select backup file +Size: Size The selected file is not a valid PasteBar backup: The selected file is not a valid PasteBar backup -Created: Created -Size: Size \ No newline at end of file +This action cannot be undone. All current data will be replaced with the backup data.: This action cannot be undone. All current data will be replaced with the backup data. +This will create a backup file containing your database: This will create a backup file containing your database +This will replace all current data. Are you sure?: This will replace all current data. Are you sure? +Total backup space {{size}}: 'Total backup space: {{size}}' +and images: and images +selected file: selected file +will be permanently deleted.: will be permanently deleted. diff --git a/packages/pastebar-app-ui/src/locales/lang/en/common.yaml b/packages/pastebar-app-ui/src/locales/lang/en/common.yaml index b2559294..141c0558 100644 --- a/packages/pastebar-app-ui/src/locales/lang/en/common.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/en/common.yaml @@ -101,6 +101,7 @@ Enter Passcode: Enter Passcode Enter Password: Enter Password Enter Recovery Password: Enter Recovery Password Enter passcode or password to unlock: Enter passcode or password to unlock +Error: Error Errors: Error loading link: Error loading link Cant save to file: Cant save to file @@ -237,6 +238,7 @@ Set: Set Set Password: Set Password Show Large View: Show Large View Show all: Show all +Size: Size Source: Source Split History Window: Split History Window Star: Star diff --git a/packages/pastebar-app-ui/src/locales/lang/esES/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/esES/backuprestore.yaml index a9f82d88..fc893f66 100644 --- a/packages/pastebar-app-ui/src/locales/lang/esES/backuprestore.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/esES/backuprestore.yaml @@ -1,4 +1,6 @@ Backup and Restore: Copia de Seguridad y Restauración +Auto Backup on Restore: Copia Automática al Restaurar +Browse: Explorar Create Backup: Crear Copia de Seguridad Include images in backup: Incluir imágenes en la copia de seguridad Backup Now: Crear Copia Ahora @@ -6,7 +8,7 @@ Restore Data: Restaurar Datos Restore from File...: Restaurar desde Archivo... Select backup file: Seleccionar archivo de copia de seguridad Available Backups: Copias de Seguridad Disponibles -Total backup space: "{{size}}": Espacio total de copias: {{size}} +Total backup space {{size}}: 'Espacio total de copia s: {{size}}' No backups found: No se encontraron copias de seguridad Restore: Restaurar Delete: Eliminar @@ -24,4 +26,18 @@ Failed to delete backup: Error al eliminar la copia de seguridad Invalid backup file: Archivo de copia de seguridad inválido The selected file is not a valid PasteBar backup: El archivo seleccionado no es una copia de seguridad válida de PasteBar Created: Creado -Size: Tamaño \ No newline at end of file +Size: Tamaño +Failed to load backup list: Error al cargar la lista de copias de seguridad +Failed to create backup: Error al crear la copia de seguridad +Restored from {{filename}}: Restaurado desde {{filename}} +Failed to restore backup: Error al restaurar la copia de seguridad +Backup Files: Archivos de Copia de Seguridad +selected file: archivo seleccionado +Failed to select backup file: Error al seleccionar el archivo de copia de seguridad +Failed to open backup folder: Error al abrir la carpeta de copia de seguridad +This will create a backup file containing your database: Esto creará un archivo de copia de seguridad que contiene tu base de datos +and images: e imágenes +Loading backups...: Cargando copias de seguridad... +will be permanently deleted.: será eliminado permanentemente. +This action cannot be undone. All current data will be replaced with the backup data.: Esta acción no se puede deshacer. Todos los datos actuales serán reemplazados con los datos de la copia de seguridad. +Restoring a backup will automatically restart the application.: Restaurar una copia de seguridad reiniciará automáticamente la aplicación. \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/fr/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/fr/backuprestore.yaml index 44f38726..b10fc741 100644 --- a/packages/pastebar-app-ui/src/locales/lang/fr/backuprestore.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/fr/backuprestore.yaml @@ -1,4 +1,6 @@ Backup and Restore: Sauvegarde et Restauration +Auto Backup on Restore: Sauvegarde Automatique à la Restauration +Browse: Parcourir Create Backup: Créer une Sauvegarde Include images in backup: Inclure les images dans la sauvegarde Backup Now: Sauvegarder Maintenant @@ -6,7 +8,7 @@ Restore Data: Restaurer les Données Restore from File...: Restaurer depuis un Fichier... Select backup file: Sélectionner le fichier de sauvegarde Available Backups: Sauvegardes Disponibles -Total backup space: "{{size}}": Espace total des sauvegardes: {{size}} +Total backup space {{size}}: 'Espace total des sauvegardes: {{size}}' No backups found: Aucune sauvegarde trouvée Restore: Restaurer Delete: Supprimer @@ -24,4 +26,18 @@ Failed to delete backup: Échec de la suppression de la sauvegarde Invalid backup file: Fichier de sauvegarde invalide The selected file is not a valid PasteBar backup: Le fichier sélectionné n'est pas une sauvegarde PasteBar valide Created: Créé -Size: Taille \ No newline at end of file +Size: Taille +Failed to load backup list: Échec du chargement de la liste des sauvegardes +Failed to create backup: Échec de la création de la sauvegarde +Restored from {{filename}}: Restauré depuis {{filename}} +Failed to restore backup: Échec de la restauration de la sauvegarde +Backup Files: Fichiers de Sauvegarde +selected file: fichier sélectionné +Failed to select backup file: Échec de la sélection du fichier de sauvegarde +Failed to open backup folder: Échec de l'ouverture du dossier de sauvegarde +This will create a backup file containing your database: Ceci créera un fichier de sauvegarde contenant votre base de données +and images: et les images +Loading backups...: Chargement des sauvegardes... +will be permanently deleted.: sera définitivement supprimé. +This action cannot be undone. All current data will be replaced with the backup data.: Cette action ne peut pas être annulée. Toutes les données actuelles seront remplacées par les données de sauvegarde. +Restoring a backup will automatically restart the application.: La restauration d'une sauvegarde redémarrera automatiquement l'application. \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/it/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/it/backuprestore.yaml index 4d36a22b..de25259a 100644 --- a/packages/pastebar-app-ui/src/locales/lang/it/backuprestore.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/it/backuprestore.yaml @@ -1,4 +1,6 @@ +Auto Backup on Restore: Backup Automatico al Ripristino Backup and Restore: Backup e Ripristino +Browse: Sfoglia Create Backup: Crea Backup Include images in backup: Includi immagini nel backup Backup Now: Crea Backup Ora @@ -6,7 +8,7 @@ Restore Data: Ripristina Dati Restore from File...: Ripristina da File... Select backup file: Seleziona file di backup Available Backups: Backup Disponibili -Total backup space: "{{size}}": Spazio totale backup: {{size}} +Total backup space {{size}}: 'Spazio totale di backup: {{size}}' No backups found: Nessun backup trovato Restore: Ripristina Delete: Elimina @@ -24,4 +26,18 @@ Failed to delete backup: Impossibile eliminare il backup Invalid backup file: File di backup non valido The selected file is not a valid PasteBar backup: Il file selezionato non è un backup PasteBar valido Created: Creato -Size: Dimensione \ No newline at end of file +Size: Dimensione +Failed to load backup list: Impossibile caricare l'elenco dei backup +Failed to create backup: Impossibile creare il backup +Restored from {{filename}}: Ripristinato da {{filename}} +Failed to restore backup: Impossibile ripristinare il backup +Backup Files: File di Backup +selected file: file selezionato +Failed to select backup file: Impossibile selezionare il file di backup +Failed to open backup folder: Impossibile aprire la cartella di backup +This will create a backup file containing your database: Questo creerà un file di backup contenente il tuo database +and images: e le immagini +Loading backups...: Caricamento backup... +will be permanently deleted.: sarà eliminato permanentemente. +This action cannot be undone. All current data will be replaced with the backup data.: Questa azione non può essere annullata. Tutti i dati attuali saranno sostituiti con i dati del backup. +Restoring a backup will automatically restart the application.: Il ripristino di un backup riavvierà automaticamente l'applicazione. \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/ru/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/ru/backuprestore.yaml index 7fcc4e84..e90b6bd6 100644 --- a/packages/pastebar-app-ui/src/locales/lang/ru/backuprestore.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/ru/backuprestore.yaml @@ -1,4 +1,6 @@ +Auto Backup on Restore: Автобэкап при восстановлении Backup and Restore: Резервное копирование и восстановление +Browse: Обзор Create Backup: Создать резервную копию Include images in backup: Включить изображения в резервную копию Backup Now: Создать резервную копию сейчас @@ -6,7 +8,7 @@ Restore Data: Восстановить данные Restore from File...: Восстановить из файла... Select backup file: Выбрать файл резервной копии Available Backups: Доступные резервные копии -Total backup space: "{{size}}": Общий размер резервных копий: {{size}} +Total backup space {{size}}: 'Общий размер резервных копий: {{size}}' No backups found: Резервные копии не найдены Restore: Восстановить Delete: Удалить @@ -24,4 +26,18 @@ Failed to delete backup: Не удалось удалить резервную Invalid backup file: Недействительный файл резервной копии The selected file is not a valid PasteBar backup: Выбранный файл не является действительной резервной копией PasteBar Created: Создано -Size: Размер \ No newline at end of file +Size: Размер +Failed to load backup list: Не удалось загрузить список резервных копий +Failed to create backup: Не удалось создать резервную копию +Restored from {{filename}}: Восстановлено из {{filename}} +Failed to restore backup: Не удалось восстановить резервную копию +Backup Files: Файлы резервных копий +selected file: выбранный файл +Failed to select backup file: Не удалось выбрать файл резервной копии +Failed to open backup folder: Не удалось открыть папку резервных копий +This will create a backup file containing your database: Это создаст файл резервной копии, содержащий вашу базу данных +and images: и изображения +Loading backups...: Загрузка резервных копий... +will be permanently deleted.: будет удалена навсегда. +This action cannot be undone. All current data will be replaced with the backup data.: Это действие нельзя отменить. Все текущие данные будут заменены данными из резервной копии. +Restoring a backup will automatically restart the application.: Восстановление резервной копии автоматически перезапустит приложение. \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/ru/common.yaml b/packages/pastebar-app-ui/src/locales/lang/ru/common.yaml index 8af236ed..50f3371e 100644 --- a/packages/pastebar-app-ui/src/locales/lang/ru/common.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/ru/common.yaml @@ -96,6 +96,7 @@ Enter Passcode: Введите код доступа Enter Password: Введите пароль Enter Recovery Password: Введите пароль восстановления Enter passcode or password to unlock: Введите код доступа или пароль для разблокировки +Error: Error Errors: Error loading link: Ошибка загрузки ссылки Cant save file: Невозможно сохранить файл diff --git a/packages/pastebar-app-ui/src/locales/lang/tr/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/tr/backuprestore.yaml index e8e6de0b..72ba0618 100644 --- a/packages/pastebar-app-ui/src/locales/lang/tr/backuprestore.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/tr/backuprestore.yaml @@ -1,4 +1,6 @@ +Auto Backup on Restore: Geri Yüklemede Otomatik Yedek Backup and Restore: Yedekleme ve Geri Yükleme +Browse: Gözat Create Backup: Yedek Oluştur Include images in backup: Yedekte görselleri dahil et Backup Now: Şimdi Yedekle @@ -6,7 +8,7 @@ Restore Data: Verileri Geri Yükle Restore from File...: Dosyadan Geri Yükle... Select backup file: Yedek dosyası seç Available Backups: Mevcut Yedekler -Total backup space: "{{size}}": Toplam yedek alanı: {{size}} +Total backup space {{size}}: 'Toplam yedek alanı: {{size}}' No backups found: Yedek bulunamadı Restore: Geri Yükle Delete: Sil @@ -14,7 +16,7 @@ Create a backup of your data?: Verilerinizin bir yedeğini oluşturmak istiyor m Backup created successfully: Yedek başarıyla oluşturuldu Move to another location?: Başka bir konuma taşı? This will replace all current data. Are you sure?: Bu, mevcut tüm verileri değiştirecek. Emin misiniz? -Restore from "{{filename}}"? This will replace all current data.: {{filename}} dosyasından geri yüklensin mi? Bu, mevcut tüm verileri değiştirecek. +Restore from {{filename}}? This will replace all current data.: '{{filename}} dosyasından geri yüklensin mi? Bu, mevcut tüm verileri değiştirecek.' Delete this backup? This action cannot be undone.: Bu yedeği sil? Bu eylem geri alınamaz. Restore completed. The application will restart.: Geri yükleme tamamlandı. Uygulama yeniden başlatılacak. Creating backup...: Yedek oluşturuluyor... @@ -24,4 +26,18 @@ Failed to delete backup: Yedek silinemedi Invalid backup file: Geçersiz yedek dosyası The selected file is not a valid PasteBar backup: Seçilen dosya geçerli bir PasteBar yedeği değil Created: Oluşturulma -Size: Boyut \ No newline at end of file +Size: Boyut +Failed to load backup list: Yedek listesi yüklenemedi +Failed to create backup: Yedek oluşturulamadı +'Restored from {{filename}}': '{{filename}} dosyasından geri yüklendi' +Failed to restore backup: Yedek geri yüklenemedi +Backup Files: Yedek Dosyaları +selected file: seçilen dosya +Failed to select backup file: Yedek dosyası seçilemedi +Failed to open backup folder: Yedek klasörü açılamadı +This will create a backup file containing your database: Bu, veritabanınızı içeren bir yedek dosyası oluşturacak +and images: ve görseller +Loading backups...: Yedekler yükleniyor... +will be permanently deleted.: kalıcı olarak silinecek. +This action cannot be undone. All current data will be replaced with the backup data.: Bu eylem geri alınamaz. Mevcut tüm veriler yedek verileriyle değiştirilecek. +Restoring a backup will automatically restart the application.: Bir yedeği geri yüklemek uygulamayı otomatik olarak yeniden başlatacak. \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/uk/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/uk/backuprestore.yaml index 7764bab8..c7a74276 100644 --- a/packages/pastebar-app-ui/src/locales/lang/uk/backuprestore.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/uk/backuprestore.yaml @@ -1,4 +1,6 @@ +Auto Backup on Restore: Авто Бекап при Відновленні Backup and Restore: Резервне копіювання та відновлення +Browse: Переглянути Create Backup: Створити резервну копію Include images in backup: Включити зображення в резервну копію Backup Now: Створити резервну копію зараз @@ -6,7 +8,7 @@ Restore Data: Відновити дані Restore from File...: Відновити з файлу... Select backup file: Вибрати файл резервної копії Available Backups: Доступні резервні копії -Total backup space: "{{size}}": Загальний розмір резервних копій: {{size}} +Total backup space {{size}}: 'Загальний розмір резервних копій: {{size}}' No backups found: Резервні копії не знайдені Restore: Відновити Delete: Видалити @@ -24,4 +26,18 @@ Failed to delete backup: Не вдалося видалити резервну Invalid backup file: Недійсний файл резервної копії The selected file is not a valid PasteBar backup: Вибраний файл не є дійсною резервною копією PasteBar Created: Створено -Size: Розмір \ No newline at end of file +Size: Розмір +Failed to load backup list: Не вдалося завантажити список резервних копій +Failed to create backup: Не вдалося створити резервну копію +Restored from {{filename}}: Відновлено з {{filename}} +Failed to restore backup: Не вдалося відновити резервну копію +Backup Files: Файли резервних копій +selected file: вибраний файл +Failed to select backup file: Не вдалося вибрати файл резервної копії +Failed to open backup folder: Не вдалося відкрити папку резервних копій +This will create a backup file containing your database: Це створить файл резервної копії, що містить вашу базу даних +and images: та зображення +Loading backups...: Завантаження резервних копій... +will be permanently deleted.: буде видалено назавжди. +This action cannot be undone. All current data will be replaced with the backup data.: Цю дію неможливо скасувати. Всі поточні дані будуть замінені даними з резервної копії. +Restoring a backup will automatically restart the application.: Відновлення резервної копії автоматично перезапустить додаток. \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/zhCN/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/zhCN/backuprestore.yaml index e723b9d5..eda48995 100644 --- a/packages/pastebar-app-ui/src/locales/lang/zhCN/backuprestore.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/zhCN/backuprestore.yaml @@ -1,4 +1,6 @@ +Auto Backup on Restore: 恢复时自动备份 Backup and Restore: 备份和恢复 +Browse: 浏览 Create Backup: 创建备份 Include images in backup: 在备份中包含图片 Backup Now: 立即备份 @@ -6,7 +8,7 @@ Restore Data: 恢复数据 Restore from File...: 从文件恢复... Select backup file: 选择备份文件 Available Backups: 可用备份 -Total backup space: "{{size}}": 备份总空间: {{size}} +Total backup space {{size}}: '备份总空间: {{size}}' No backups found: 未找到备份 Restore: 恢复 Delete: 删除 @@ -24,4 +26,18 @@ Failed to delete backup: 删除备份失败 Invalid backup file: 无效的备份文件 The selected file is not a valid PasteBar backup: 选择的文件不是有效的PasteBar备份 Created: 创建时间 -Size: 大小 \ No newline at end of file +Size: 大小 +Failed to load backup list: 加载备份列表失败 +Failed to create backup: 创建备份失败 +Restored from {{filename}}: 从{{filename}}恢复 +Failed to restore backup: 恢复备份失败 +Backup Files: 备份文件 +selected file: 选择的文件 +Failed to select backup file: 选择备份文件失败 +Failed to open backup folder: 打开备份文件夹失败 +This will create a backup file containing your database: 这将创建一个包含您数据库的备份文件 +and images: 和图片 +Loading backups...: 正在加载备份... +will be permanently deleted.: 将被永久删除。 +This action cannot be undone. All current data will be replaced with the backup data.: 此操作无法撤销。所有当前数据将被备份数据替换。 +Restoring a backup will automatically restart the application.: 恢复备份将自动重启应用程序。 \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/pages/settings/BackupRestoreSettings.tsx b/packages/pastebar-app-ui/src/pages/settings/BackupRestoreSettings.tsx index fa19ab67..59ab70b5 100644 --- a/packages/pastebar-app-ui/src/pages/settings/BackupRestoreSettings.tsx +++ b/packages/pastebar-app-ui/src/pages/settings/BackupRestoreSettings.tsx @@ -1,23 +1,23 @@ import { useEffect, useState } from 'react' import { invoke } from '@tauri-apps/api' -import { useTranslation } from 'react-i18next' -import { - Archive, - Download, - FolderOpen, - HardDrive, - Loader2, - Package, - RotateCcw, - Trash2, - Upload +import { open } from '@tauri-apps/api/dialog' +import { settingsStoreAtom, uiStoreAtom } from '~/store' +import { useAtomValue } from 'jotai' +import { + Download, + ExternalLink, + FolderOpen, + HardDrive, + Loader2, + Package, + RotateCcw, + Trash2, + Upload, } from 'lucide-react' -import { useToast } from '~/components/ui/use-toast' +import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' import AutoSize from 'react-virtualized-auto-sizer' -import Spacer from '~/components/atoms/spacer' -import { Icons } from '~/components/icons' -import SimpleBar from '~/components/libs/simplebar-react' import { AlertDialog, AlertDialogAction, @@ -29,6 +29,12 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '~/components/ui/alert-dialog' +import { useToast } from '~/components/ui/use-toast' +import Spacer from '~/components/atoms/spacer' +import { TimeAgo } from '~/components/atoms/time-ago/TimeAgo' +import ToolTip from '~/components/atoms/tooltip' +import { Icons } from '~/components/icons' +import SimpleBar from '~/components/libs/simplebar-react' import { Badge, Box, @@ -40,7 +46,6 @@ import { Checkbox, Flex, Text, - TextNormal, } from '~/components/ui' interface BackupInfo { @@ -60,14 +65,47 @@ interface BackupListResponse { export default function BackupRestoreSettings() { const { t } = useTranslation() const { toast } = useToast() + const { returnRoute } = useAtomValue(uiStoreAtom) + + const { relaunchApp } = useAtomValue(settingsStoreAtom) const [includeImages, setIncludeImages] = useState(true) + const [backupOnRestore, setBackupOnRestore] = useState(() => { + const saved = localStorage.getItem('backupOnRestore') + return saved !== null ? JSON.parse(saved) : true + }) + + // Save backup on restore preference to localStorage + useEffect(() => { + localStorage.setItem('backupOnRestore', JSON.stringify(backupOnRestore)) + }, [backupOnRestore]) const [isCreatingBackup, setIsCreatingBackup] = useState(false) const [isRestoring, setIsRestoring] = useState(false) + const [restoringFromFile, setRestoringFromFile] = useState(false) + const [restoringBackupPath, setRestoringBackupPath] = useState(null) const [backups, setBackups] = useState([]) const [totalSize, setTotalSize] = useState('') const [isLoadingBackups, setIsLoadingBackups] = useState(false) + // Parse date from backup filename + const parseBackupDate = (filename: string): Date | null => { + // Extract date from filename format: pastebar-data-backup-YYYY-MM-DD-HH-MM.zip + const match = filename.match( + /pastebar-data-backup-(\d{4})-(\d{2})-(\d{2})-(\d{2})-(\d{2})\.zip/ + ) + if (match) { + const [, year, month, day, hour, minute] = match + return new Date( + parseInt(year), + parseInt(month) - 1, + parseInt(day), + parseInt(hour), + parseInt(minute) + ) + } + return null + } + const loadBackups = async () => { setIsLoadingBackups(true) try { @@ -77,8 +115,14 @@ export default function BackupRestoreSettings() { } catch (error) { console.error('Failed to load backups:', error) toast({ + id: 'backup-list-error', title: t('Error', { ns: 'common' }), - description: 'Failed to load backup list', + duration: 3000, + description: ( + + {t('Failed to load backup list', { ns: 'backuprestore' })} + + ), variant: 'destructive', }) } finally { @@ -96,19 +140,28 @@ export default function BackupRestoreSettings() { const backupPath = await invoke('create_backup', { includeImages, }) - + toast({ + id: 'backup-create-success', title: t('Backup created successfully', { ns: 'backuprestore' }), - description: backupPath, + duration: 3000, + description: {backupPath}, + variant: 'success', }) - + // Reload backup list await loadBackups() } catch (error) { console.error('Failed to create backup:', error) toast({ + id: 'backup-create-error', title: t('Error', { ns: 'common' }), - description: `Failed to create backup: ${error}`, + duration: 3000, + description: ( + + {t('Failed to create backup', { ns: 'backuprestore' })}: {String(error)} + + ), variant: 'destructive', }) } finally { @@ -118,284 +171,518 @@ export default function BackupRestoreSettings() { const handleRestoreBackup = async (backupPath: string, filename: string) => { setIsRestoring(true) + setRestoringBackupPath(backupPath) try { - await invoke('restore_backup', { backupPath }) - - toast({ - title: t('Restore completed. The application will restart.', { ns: 'backuprestore' }), - description: `Restored from ${filename}`, + await invoke('restore_backup', { + backupPath, + createPreRestoreBackup: backupOnRestore, }) - + + toast({ + id: 'backup-restore-success', + title: t('Restore completed. The application will restart.', { + ns: 'backuprestore', + }), + duration: 3000, + description: ( + + {t('Restored from {{filename}}', { ns: 'backuprestore', filename })} + + ), + variant: 'success', + }) + + setTimeout(() => { + relaunchApp() + setIsRestoring(false) + }, 3000) + // Application should restart after restore } catch (error) { console.error('Failed to restore backup:', error) toast({ title: t('Error', { ns: 'common' }), - description: `Failed to restore backup: ${error}`, + id: 'backup-restore-error', + duration: 3000, + description: ( + + {t('Failed to restore backup', { ns: 'backuprestore' })}: {String(error)} + + ), variant: 'destructive', }) } finally { setIsRestoring(false) + setRestoringBackupPath(null) } } const handleRestoreFromFile = async () => { try { - const selectedFile = await invoke('select_backup_file') - - if (selectedFile) { - const filename = selectedFile.split(/[/\\]/).pop() || 'selected file' - await handleRestoreBackup(selectedFile, filename) + setRestoringFromFile(true) + // Get current data directory path for defaultPath + const dataPaths = await invoke<{ data_dir: string }>('get_data_paths') + + const selected = await open({ + multiple: false, + defaultPath: dataPaths.data_dir, + filters: [ + { + name: t('Backup Files', { ns: 'backuprestore' }), + extensions: ['zip'], + }, + ], + }) + + if (selected && typeof selected === 'string') { + // Validate it's a valid backup file + const filename = + selected.split(/[/\\]/).pop() || t('selected file', { ns: 'backuprestore' }) + if (!filename.includes('pastebar-data-backup-') || !filename.endsWith('.zip')) { + toast({ + id: 'backup-invalid-file', + title: t('Invalid backup file', { ns: 'backuprestore' }), + duration: 3000, + description: ( + + {t('The selected file is not a valid PasteBar backup', { + ns: 'backuprestore', + })} + + ), + variant: 'destructive', + }) + setRestoringFromFile(false) + return + } + + await handleRestoreBackup(selected, filename) } + // If selected is null, user cancelled the dialog - no action needed + setRestoringFromFile(false) } catch (error) { console.error('Failed to select backup file:', error) toast({ title: t('Error', { ns: 'common' }), - description: `Failed to select backup file: ${error}`, + id: 'backup-select-error', + duration: 3000, + description: ( + + {t('Failed to select backup file', { ns: 'backuprestore' })}: {String(error)} + + ), variant: 'destructive', }) + setRestoringFromFile(false) } } const handleDeleteBackup = async (backupPath: string, filename: string) => { try { await invoke('delete_backup', { backupPath }) - + toast({ + id: 'backup-delete-success', title: t('Backup deleted successfully', { ns: 'backuprestore' }), - description: filename, + duration: 3000, + description: {filename}, + variant: 'success', }) - + // Reload backup list await loadBackups() } catch (error) { console.error('Failed to delete backup:', error) toast({ + id: 'backup-delete-error', title: t('Failed to delete backup', { ns: 'backuprestore' }), - description: `${error}`, + duration: 3000, + description: {`${error}`}, + variant: 'destructive', + }) + } + } + + const handleBrowseBackupFolder = async (backupPath: string) => { + try { + // Extract directory from the backup file path + const backupDir = backupPath.substring( + 0, + backupPath.lastIndexOf('/') || backupPath.lastIndexOf('\\') + ) + await invoke('open_path_or_app', { path: backupDir }) + } catch (error) { + console.error('Failed to open backup folder:', error) + toast({ + id: 'backup-open-error', + title: t('Error', { ns: 'common' }), + duration: 3000, + description: ( + + {t('Failed to open backup folder', { ns: 'backuprestore' })}: {String(error)} + + ), variant: 'destructive', }) } } return ( - - - {({ height }) => ( - - - {/* Header */} - - - + + {({ height }) => { + return ( + height && ( + + + {t('Backup and Restore', { ns: 'backuprestore' })} + + + + + + + {/* Create Backup Section */} + + + + + + {t('Create Backup', { ns: 'backuprestore' })} + - {/* Create Backup Section */} - - - - - {t('Create Backup', { ns: 'backuprestore' })} - - - - - setIncludeImages(checked as boolean)} - /> - - - - - - + + + + + {t('Create a backup of your data?', { + ns: 'backuprestore', + })} + + + {t( + 'This will create a backup file containing your database', + { ns: 'backuprestore' } + )} + {includeImages + ? t('and images', { ns: 'backuprestore' }) + : ''} + . + + + + + {t('Cancel', { ns: 'common' })} + + + {t('Create Backup', { ns: 'backuprestore' })} + + + + + + + + + + {/* Restore Section */} + + + + + + {t('Restore Data', { ns: 'backuprestore' })} + + + + setBackupOnRestore(checked as boolean)} + > + {t('Auto Backup on Restore', { ns: 'backuprestore' })} + + + + + + - - - - - {t('Create a backup of your data?', { ns: 'backuprestore' })} - - - This will create a backup file containing your database - {includeImages ? ' and images' : ''}. - - - - {t('Cancel', { ns: 'common' })} - - {t('Create Backup', { ns: 'backuprestore' })} - - - - - - - {/* Restore Section */} - - - - - {t('Restore Data', { ns: 'backuprestore' })} - - - - + - + {/* Available Backups */} + + + + {t('Available Backups', { ns: 'backuprestore' })} + + {totalSize && ( + + {t('Total backup space {{size}}', { + ns: 'backuprestore', + size: totalSize, + })} + + )} + - {/* Available Backups */} - - - - {t('Available Backups', { ns: 'backuprestore' })} + {isLoadingBackups ? ( + + + + {t('Loading backups...', { ns: 'backuprestore' })} + + + ) : backups.length === 0 ? ( + + + {t('No backups found', { ns: 'backuprestore' })} + + ) : ( + + {backups.map(backup => { + const backupDate = parseBackupDate(backup.filename) + return ( + + + {/* File name - bold on top */} + + {backup.filename} + + + {/* Date and size line */} + + + {backupDate ? ( + + ) : ( + {backup.created_date} + )} + + {backup.size_formatted} + + + {/* Buttons row */} + + {/* Delete button on the left */} + + + + + + + + + + {t( + 'Delete this backup? This action cannot be undone.', + { ns: 'backuprestore' } + )} + + + {backup.filename} ({backup.size_formatted}) + {t('will be permanently deleted.', { + ns: 'backuprestore', + })} + + + + + {t('Cancel', { ns: 'common' })} + + + handleDeleteBackup( + backup.full_path, + backup.filename + ) + } + className="bg-red-600 hover:bg-red-700" + > + {t('Delete', { ns: 'backuprestore' })} + + + + + + {/* Browse and Restore buttons on the right */} + + + + + + + + + + + {t( + 'Restore from {{filename}}? This will replace all current data.', + { + ns: 'backuprestore', + filename: backup.filename, + } + )} + + + {t( + 'This action cannot be undone. All current data will be replaced with the backup data.', + { ns: 'backuprestore' } + )} + + + + + {t('Cancel', { ns: 'common' })} + + + handleRestoreBackup( + backup.full_path, + backup.filename + ) + } + className="bg-red-600 hover:bg-red-700" + > + {t('Restore', { ns: 'backuprestore' })} + + + + + + + + + ) + })} + + )} + + + {/* Restore note */} + + {t( + 'Restoring a backup will automatically restart the application.', + { ns: 'backuprestore' } + )} - {totalSize && ( - - {t('Total backup space: {{size}}', { - ns: 'settings', - size: totalSize - })} - - )} - + + - {isLoadingBackups ? ( - - - Loading backups... - - ) : backups.length === 0 ? ( - - - {t('No backups found', { ns: 'backuprestore' })} - - ) : ( - - {backups.map((backup) => ( - - - - - - {backup.filename} - - - {t('Created', { ns: 'common' })}: {backup.created_date} - - - {t('Size', { ns: 'common' })}: {backup.size_formatted} - - - - - - - - - - - - {t('Restore from {{filename}}? This will replace all current data.', { - ns: 'settings', - filename: backup.filename - })} - - - This action cannot be undone. All current data will be replaced with the backup data. - - - - {t('Cancel', { ns: 'common' })} - handleRestoreBackup(backup.full_path, backup.filename)} - className="bg-red-600 hover:bg-red-700" - > - {t('Restore', { ns: 'backuprestore' })} - - - - - - - - - - - - - {t('Delete this backup? This action cannot be undone.', { ns: 'backuprestore' })} - - - {backup.filename} ({backup.size_formatted}) will be permanently deleted. - - - - {t('Cancel', { ns: 'common' })} - handleDeleteBackup(backup.full_path, backup.filename)} - className="bg-red-600 hover:bg-red-700" - > - {t('Delete', { ns: 'backuprestore' })} - - - - - - - - ))} - - )} - - - + + + + + + + - - )} - - + ) + ) + }} + ) -} \ No newline at end of file +} diff --git a/packages/pastebar-app-ui/src/pages/settings/UserPreferences.tsx b/packages/pastebar-app-ui/src/pages/settings/UserPreferences.tsx index e483a0f9..3bc2e550 100644 --- a/packages/pastebar-app-ui/src/pages/settings/UserPreferences.tsx +++ b/packages/pastebar-app-ui/src/pages/settings/UserPreferences.tsx @@ -10,7 +10,6 @@ import { themeStoreAtom, uiStoreAtom, } from '~/store' -import CustomDatabaseLocationSettings from './CustomDatabaseLocationSettings' import { useAtomValue } from 'jotai' import { ChevronDown, ChevronUp, MessageSquare, MessageSquareDashed } from 'lucide-react' import { useTheme } from 'next-themes' @@ -40,6 +39,8 @@ import { import md from '~/store/example.md?raw' +import CustomDatabaseLocationSettings from './CustomDatabaseLocationSettings' + export default function UserPreferences() { const { t } = useTranslation() diff --git a/packages/pastebar-app-ui/src/styles/globals.css b/packages/pastebar-app-ui/src/styles/globals.css index ee39f12c..0571918c 100644 --- a/packages/pastebar-app-ui/src/styles/globals.css +++ b/packages/pastebar-app-ui/src/styles/globals.css @@ -521,7 +521,7 @@ div.note-content.release-notes { --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 100% 50%; + --destructive: 0 50% 50%; --destructive-foreground: 210 40% 98%; --ring: 215 20.2% 65.1%; @@ -904,3 +904,7 @@ pre.code-editor-pre .token-line { overflow: hidden; border-radius: 13px; } + +.word-break { + word-break: break-word; +} diff --git a/src-tauri/src/commands/backup_restore_commands.rs b/src-tauri/src/commands/backup_restore_commands.rs index 84092781..43429472 100644 --- a/src-tauri/src/commands/backup_restore_commands.rs +++ b/src-tauri/src/commands/backup_restore_commands.rs @@ -3,12 +3,11 @@ use std::path::{Path, PathBuf}; use std::io::Write; use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; -use tauri::api::dialog::FileDialogBuilder; use zip::{ZipWriter, ZipArchive}; use zip::write::FileOptions; use std::io::{Read, Seek}; -use crate::db::APP_CONSTANTS; +use crate::db::{get_data_dir, get_db_path, get_clip_images_dir, get_clipboard_images_dir}; use crate::services::utils::debug_output; #[derive(Debug, Serialize, Deserialize)] @@ -27,10 +26,6 @@ pub struct BackupListResponse { pub total_size_formatted: String, } -fn get_data_dir() -> PathBuf { - // Get current data directory - could be custom or default - APP_CONSTANTS.get().unwrap().app_data_dir.clone() -} fn get_backup_filename() -> String { let now = Local::now(); @@ -106,16 +101,22 @@ pub async fn create_backup(include_images: bool) -> Result { let backup_filename = get_backup_filename(); let backup_path = data_dir.join(&backup_filename); - // Database file path - let db_filename = if cfg!(debug_assertions) { - "local.pastebar-db.data" - } else { - "pastebar-db.data" - }; - let db_path = data_dir.join(db_filename); + debug_output(|| { + println!("Data directory: {}", data_dir.display()); + println!("Backup will be created at: {}", backup_path.display()); + }); + + // Database file path - use the actual database path which handles debug/release naming + let db_path_str = get_db_path(); + let db_path = PathBuf::from(&db_path_str); + + debug_output(|| { + println!("Looking for database file at: {}", db_path_str); + println!("Database file exists: {}", db_path.exists()); + }); if !db_path.exists() { - return Err("Database file not found".to_string()); + return Err(format!("Database file not found at: {}", db_path_str)); } // Create zip file @@ -134,6 +135,11 @@ pub async fn create_backup(include_images: bool) -> Result { db_file.read_to_end(&mut db_buffer) .map_err(|e| format!("Failed to read database file: {}", e))?; + // Get just the filename for the zip entry + let db_filename = db_path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or("pastebar-db.data"); + zip.start_file(db_filename, options) .map_err(|e| format!("Failed to start database file in zip: {}", e))?; zip.write_all(&db_buffer) @@ -141,8 +147,15 @@ pub async fn create_backup(include_images: bool) -> Result { // Add image directories if requested if include_images { - let clip_images_dir = data_dir.join("clip-images"); - let history_images_dir = data_dir.join("history-images"); + let clip_images_dir = get_clip_images_dir(); + let history_images_dir = get_clipboard_images_dir(); + + debug_output(|| { + println!("Clip images directory: {}", clip_images_dir.display()); + println!("Clip images exists: {}", clip_images_dir.exists()); + println!("History images directory: {}", history_images_dir.display()); + println!("History images exists: {}", history_images_dir.exists()); + }); if clip_images_dir.exists() { add_directory_to_zip(&mut zip, &clip_images_dir, &data_dir) @@ -151,7 +164,7 @@ pub async fn create_backup(include_images: bool) -> Result { if history_images_dir.exists() { add_directory_to_zip(&mut zip, &history_images_dir, &data_dir) - .map_err(|e| format!("Failed to add history-images directory: {}", e))?; + .map_err(|e| format!("Failed to add clipboard-images directory: {}", e))?; } } @@ -224,87 +237,38 @@ pub async fn list_backups() -> Result { }) } -#[tauri::command] -pub fn select_backup_file() -> Result, String> { - use std::sync::{Arc, Mutex}; - use std::sync::mpsc; - - let (tx, rx) = mpsc::channel(); - - FileDialogBuilder::new() - .set_title("Select PasteBar Backup File") - .add_filter("Backup Files", &["zip"]) - .pick_file(move |file_path| { - let _ = tx.send(file_path); - }); - - // Wait for the dialog result - if let Ok(selected_path) = rx.recv() { - if let Some(path) = selected_path { - let path_str = path.to_string_lossy().to_string(); - // Validate it's a valid backup file - if let Err(e) = validate_backup_file(&path_str) { - return Err(format!("Invalid backup file: {}", e)); - } - Ok(Some(path_str)) - } else { - Ok(None) - } - } else { - Err("Failed to get file dialog result".to_string()) - } -} - -fn validate_backup_file(file_path: &str) -> Result<(), String> { - let path = Path::new(file_path); - - if !path.exists() { - return Err("File does not exist".to_string()); - } - - if !path.extension().map_or(false, |ext| ext == "zip") { - return Err("File is not a zip file".to_string()); - } - - // Open zip and check for required files - let file = fs::File::open(path) - .map_err(|e| format!("Cannot open file: {}", e))?; - - let mut archive = ZipArchive::new(file) - .map_err(|e| format!("Invalid zip file: {}", e))?; - - // Check for database file - let db_files = ["pastebar-db.data", "local.pastebar-db.data"]; - let has_db = db_files.iter().any(|&db_file| { - archive.by_name(db_file).is_ok() - }); - - if !has_db { - return Err("Backup does not contain a valid PasteBar database".to_string()); - } - - Ok(()) -} #[tauri::command] -pub async fn restore_backup(backup_path: String) -> Result { +pub async fn restore_backup(backup_path: String, create_pre_restore_backup: bool) -> Result { debug_output(|| { println!("Restoring backup from: {}", backup_path); }); - // Validate backup file - validate_backup_file(&backup_path)?; + // Basic validation - check if file exists and is a zip + let backup_file = Path::new(&backup_path); + if !backup_file.exists() { + return Err("Backup file does not exist".to_string()); + } + if !backup_file.extension().map_or(false, |ext| ext == "zip") { + return Err("Backup file must be a zip file".to_string()); + } let data_dir = get_data_dir(); - // Create backup of current data before restore - let _pre_restore_backup = format!("pre-restore-backup-{}.zip", Local::now().format("%Y-%m-%d-%H-%M-%S")); - let _pre_restore_path = data_dir.join(&_pre_restore_backup); - - // Create backup of current state - if let Err(e) = create_backup(true).await { + // Optionally create backup of current data before restore + if create_pre_restore_backup { + if let Err(e) = create_backup(true).await { + debug_output(|| { + println!("Warning: Could not create pre-restore backup: {}", e); + }); + } else { + debug_output(|| { + println!("Created pre-restore backup"); + }); + } + } else { debug_output(|| { - println!("Warning: Could not create pre-restore backup: {}", e); + println!("Skipping pre-restore backup as requested"); }); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 84cffbad..b9bb88bb 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1105,7 +1105,6 @@ async fn main() { update_setting, backup_restore_commands::create_backup, backup_restore_commands::list_backups, - backup_restore_commands::select_backup_file, backup_restore_commands::restore_backup, backup_restore_commands::delete_backup, backup_restore_commands::get_data_paths,