Enhance backup and restore features with improved UI, new translations, and error handling for better user experience
This commit is contained in:
parent
1aa8e5d28c
commit
488f746683
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,6 +8,7 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
local.pastebar.*
|
||||
safelist.txt
|
||||
pastebar-data-backup*
|
||||
|
||||
clipboard-images/**/*
|
||||
clip-images/**/*
|
||||
|
@ -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
|
||||
|
@ -591,6 +591,13 @@ export function NavBar() {
|
||||
<Shortcut keys="ALT+U" />
|
||||
</MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarItem
|
||||
onClick={() => {
|
||||
navigate('/app-settings/backup-restore', { replace: true })
|
||||
}}
|
||||
>
|
||||
{t('Backup and Restore', { ns: 'backuprestore' })}
|
||||
</MenubarItem>
|
||||
<MenubarItem
|
||||
onClick={() => {
|
||||
navigate('/app-settings/security', { replace: true })
|
||||
@ -653,9 +660,7 @@ export function NavBar() {
|
||||
>
|
||||
<Text
|
||||
className={`mr-2 ${
|
||||
isImageCaptureDisabled
|
||||
? 'text-slate-900/50'
|
||||
: 'text-slate-800'
|
||||
isImageCaptureDisabled ? 'text-slate-900/50' : 'text-slate-800'
|
||||
}`}
|
||||
>
|
||||
{t('Enable Image Capture', { ns: 'history' })}
|
||||
|
@ -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
|
||||
The selected file is not a valid PasteBar backup: The selected file is not a valid PasteBar backup
|
||||
Created: Created
|
||||
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
|
||||
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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -25,3 +27,17 @@ 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
|
||||
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.
|
@ -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
|
||||
@ -25,3 +27,17 @@ 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
|
||||
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.
|
@ -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
|
||||
@ -25,3 +27,17 @@ 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
|
||||
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.
|
@ -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: Удалить
|
||||
@ -25,3 +27,17 @@ Invalid backup file: Недействительный файл резервно
|
||||
The selected file is not a valid PasteBar backup: Выбранный файл не является действительной резервной копией PasteBar
|
||||
Created: Создано
|
||||
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.: Восстановление резервной копии автоматически перезапустит приложение.
|
@ -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: Невозможно сохранить файл
|
||||
|
@ -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...
|
||||
@ -25,3 +27,17 @@ 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
|
||||
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.
|
@ -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: Видалити
|
||||
@ -25,3 +27,17 @@ Invalid backup file: Недійсний файл резервної копії
|
||||
The selected file is not a valid PasteBar backup: Вибраний файл не є дійсною резервною копією PasteBar
|
||||
Created: Створено
|
||||
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.: Відновлення резервної копії автоматично перезапустить додаток.
|
@ -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: 删除
|
||||
@ -25,3 +27,17 @@ Invalid backup file: 无效的备份文件
|
||||
The selected file is not a valid PasteBar backup: 选择的文件不是有效的PasteBar备份
|
||||
Created: 创建时间
|
||||
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.: 恢复备份将自动重启应用程序。
|
@ -1,23 +1,23 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
import { settingsStoreAtom, uiStoreAtom } from '~/store'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import {
|
||||
Archive,
|
||||
Download,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
HardDrive,
|
||||
Loader2,
|
||||
Package,
|
||||
RotateCcw,
|
||||
Trash2,
|
||||
Upload
|
||||
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<string | null>(null)
|
||||
const [backups, setBackups] = useState<BackupInfo[]>([])
|
||||
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: (
|
||||
<Box className="word-break">
|
||||
{t('Failed to load backup list', { ns: 'backuprestore' })}
|
||||
</Box>
|
||||
),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@ -98,8 +142,11 @@ export default function BackupRestoreSettings() {
|
||||
})
|
||||
|
||||
toast({
|
||||
id: 'backup-create-success',
|
||||
title: t('Backup created successfully', { ns: 'backuprestore' }),
|
||||
description: backupPath,
|
||||
duration: 3000,
|
||||
description: <Box className="word-break">{backupPath}</Box>,
|
||||
variant: 'success',
|
||||
})
|
||||
|
||||
// Reload backup list
|
||||
@ -107,8 +154,14 @@ export default function BackupRestoreSettings() {
|
||||
} 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: (
|
||||
<Box className="word-break">
|
||||
{t('Failed to create backup', { ns: 'backuprestore' })}: {String(error)}
|
||||
</Box>
|
||||
),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@ -118,42 +171,109 @@ export default function BackupRestoreSettings() {
|
||||
|
||||
const handleRestoreBackup = async (backupPath: string, filename: string) => {
|
||||
setIsRestoring(true)
|
||||
setRestoringBackupPath(backupPath)
|
||||
try {
|
||||
await invoke('restore_backup', { backupPath })
|
||||
await invoke('restore_backup', {
|
||||
backupPath,
|
||||
createPreRestoreBackup: backupOnRestore,
|
||||
})
|
||||
|
||||
toast({
|
||||
title: t('Restore completed. The application will restart.', { ns: 'backuprestore' }),
|
||||
description: `Restored from ${filename}`,
|
||||
id: 'backup-restore-success',
|
||||
title: t('Restore completed. The application will restart.', {
|
||||
ns: 'backuprestore',
|
||||
}),
|
||||
duration: 3000,
|
||||
description: (
|
||||
<Box className="word-break">
|
||||
{t('Restored from {{filename}}', { ns: 'backuprestore', filename })}
|
||||
</Box>
|
||||
),
|
||||
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: (
|
||||
<Box className="word-break">
|
||||
{t('Failed to restore backup', { ns: 'backuprestore' })}: {String(error)}
|
||||
</Box>
|
||||
),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
setIsRestoring(false)
|
||||
setRestoringBackupPath(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestoreFromFile = async () => {
|
||||
try {
|
||||
const selectedFile = await invoke<string | null>('select_backup_file')
|
||||
setRestoringFromFile(true)
|
||||
// Get current data directory path for defaultPath
|
||||
const dataPaths = await invoke<{ data_dir: string }>('get_data_paths')
|
||||
|
||||
if (selectedFile) {
|
||||
const filename = selectedFile.split(/[/\\]/).pop() || 'selected file'
|
||||
await handleRestoreBackup(selectedFile, filename)
|
||||
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: (
|
||||
<Box className="word-break">
|
||||
{t('The selected file is not a valid PasteBar backup', {
|
||||
ns: 'backuprestore',
|
||||
})}
|
||||
</Box>
|
||||
),
|
||||
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: (
|
||||
<Box className="word-break">
|
||||
{t('Failed to select backup file', { ns: 'backuprestore' })}: {String(error)}
|
||||
</Box>
|
||||
),
|
||||
variant: 'destructive',
|
||||
})
|
||||
setRestoringFromFile(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,8 +282,11 @@ export default function BackupRestoreSettings() {
|
||||
await invoke('delete_backup', { backupPath })
|
||||
|
||||
toast({
|
||||
id: 'backup-delete-success',
|
||||
title: t('Backup deleted successfully', { ns: 'backuprestore' }),
|
||||
description: filename,
|
||||
duration: 3000,
|
||||
description: <Box className="word-break">{filename}</Box>,
|
||||
variant: 'success',
|
||||
})
|
||||
|
||||
// Reload backup list
|
||||
@ -171,231 +294,395 @@ export default function BackupRestoreSettings() {
|
||||
} 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: <Box className="word-break">{`${error}`}</Box>,
|
||||
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: (
|
||||
<Box className="word-break">
|
||||
{t('Failed to open backup folder', { ns: 'backuprestore' })}: {String(error)}
|
||||
</Box>
|
||||
),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="w-full h-full">
|
||||
<AutoSize disableWidth>
|
||||
{({ height }) => (
|
||||
<SimpleBar style={{ height, width: '100%' }} autoHide>
|
||||
<Box className="flex flex-col gap-6 p-6">
|
||||
{/* Header */}
|
||||
<Box className="flex items-center gap-3">
|
||||
<Archive className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||
<Text className="text-2xl font-semibold">
|
||||
<AutoSize disableWidth>
|
||||
{({ height }) => {
|
||||
return (
|
||||
height && (
|
||||
<Box className="p-4 py-6 select-none min-w-[320px]">
|
||||
<Box className="text-xl my-2 mx-2 flex items-center justify-between">
|
||||
<Text className="light">
|
||||
{t('Backup and Restore', { ns: 'backuprestore' })}
|
||||
</Text>
|
||||
<Link to={returnRoute} replace>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-sm bg-slate-200 dark:bg-slate-700 dark:text-slate-200"
|
||||
size="sm"
|
||||
>
|
||||
{t('Back', { ns: 'common' })}
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
<Spacer h={3} />
|
||||
<SimpleBar style={{ maxHeight: height - 85 }} autoHide={true}>
|
||||
<Box className="animate-in fade-in max-w-xl mt-4">
|
||||
{/* Create Backup Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<Flex className="items-center gap-2">
|
||||
<Upload className="w-5 h-5" />
|
||||
{t('Create Backup', { ns: 'backuprestore' })}
|
||||
</Flex>
|
||||
|
||||
{/* Create Backup Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="w-5 h-5" />
|
||||
{t('Create Backup', { ns: 'backuprestore' })}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Flex className="items-center space-x-2">
|
||||
<Checkbox
|
||||
id="include-images"
|
||||
checked={includeImages}
|
||||
onCheckedChange={(checked) => setIncludeImages(checked as boolean)}
|
||||
/>
|
||||
<label
|
||||
htmlFor="include-images"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{t('Include images in backup', { ns: 'backuprestore' })}
|
||||
</label>
|
||||
</Flex>
|
||||
{/* Include Images option - right corner */}
|
||||
<Flex className="items-center text-sm">
|
||||
<Checkbox
|
||||
id="include-images"
|
||||
checked={includeImages}
|
||||
onChange={checked => setIncludeImages(checked as boolean)}
|
||||
>
|
||||
{t('Include images in backup', { ns: 'backuprestore' })}
|
||||
</Checkbox>
|
||||
</Flex>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
disabled={
|
||||
isCreatingBackup || isRestoring || restoringFromFile
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
{isCreatingBackup ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
{t('Creating backup...', { ns: 'backuprestore' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Package className="w-4 h-4 mr-2" />
|
||||
{t('Backup Now', { ns: 'backuprestore' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t('Create a backup of your data?', {
|
||||
ns: 'backuprestore',
|
||||
})}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t(
|
||||
'This will create a backup file containing your database',
|
||||
{ ns: 'backuprestore' }
|
||||
)}
|
||||
{includeImages
|
||||
? t('and images', { ns: 'backuprestore' })
|
||||
: ''}
|
||||
.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t('Cancel', { ns: 'common' })}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleCreateBackup}>
|
||||
{t('Create Backup', { ns: 'backuprestore' })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button disabled={isCreatingBackup} className="w-full">
|
||||
{isCreatingBackup ? (
|
||||
<Box className="animate-in fade-in max-w-xl mt-4">
|
||||
{/* Restore Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<Flex className="items-center gap-2">
|
||||
<Download className="w-5 h-5" />
|
||||
{t('Restore Data', { ns: 'backuprestore' })}
|
||||
</Flex>
|
||||
|
||||
<Flex className="items-center text-sm">
|
||||
<Checkbox
|
||||
id="backup-on-restore"
|
||||
checked={backupOnRestore}
|
||||
onChange={checked => setBackupOnRestore(checked as boolean)}
|
||||
>
|
||||
{t('Auto Backup on Restore', { ns: 'backuprestore' })}
|
||||
</Checkbox>
|
||||
</Flex>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRestoreFromFile}
|
||||
disabled={isRestoring || restoringFromFile}
|
||||
className="w-full"
|
||||
>
|
||||
{restoringFromFile ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
{t('Creating backup...', { ns: 'backuprestore' })}
|
||||
{t('Restoring...', { ns: 'backuprestore' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Package className="w-4 h-4 mr-2" />
|
||||
{t('Backup Now', { ns: 'backuprestore' })}
|
||||
<FolderOpen className="w-4 h-4 mr-2" />
|
||||
{t('Restore from File...', { ns: 'backuprestore' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t('Create a backup of your data?', { ns: 'backuprestore' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will create a backup file containing your database
|
||||
{includeImages ? ' and images' : ''}.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('Cancel', { ns: 'common' })}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleCreateBackup}>
|
||||
{t('Create Backup', { ns: 'backuprestore' })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Restore Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Download className="w-5 h-5" />
|
||||
{t('Restore Data', { ns: 'backuprestore' })}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRestoreFromFile}
|
||||
disabled={isRestoring}
|
||||
className="w-full"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4 mr-2" />
|
||||
{t('Restore from File...', { ns: 'backuprestore' })}
|
||||
</Button>
|
||||
<Spacer h={4} />
|
||||
|
||||
<Spacer h={4} />
|
||||
{/* Available Backups */}
|
||||
<Box>
|
||||
<Flex className="items-center justify-between mb-3">
|
||||
<Text className="font-medium">
|
||||
{t('Available Backups', { ns: 'backuprestore' })}
|
||||
</Text>
|
||||
{totalSize && (
|
||||
<Badge variant="secondary">
|
||||
{t('Total backup space {{size}}', {
|
||||
ns: 'backuprestore',
|
||||
size: totalSize,
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* Available Backups */}
|
||||
<Box>
|
||||
<Flex className="items-center justify-between mb-3">
|
||||
<Text className="font-medium">
|
||||
{t('Available Backups', { ns: 'backuprestore' })}
|
||||
{isLoadingBackups ? (
|
||||
<Flex className="items-center justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
<Text className="ml-2">
|
||||
{t('Loading backups...', { ns: 'backuprestore' })}
|
||||
</Text>
|
||||
</Flex>
|
||||
) : backups.length === 0 ? (
|
||||
<Box className="text-center py-8 text-muted-foreground">
|
||||
<HardDrive className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<Text>{t('No backups found', { ns: 'backuprestore' })}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Box className="space-y-3">
|
||||
{backups.map(backup => {
|
||||
const backupDate = parseBackupDate(backup.filename)
|
||||
return (
|
||||
<Card key={backup.filename} className="p-4">
|
||||
<Box className="space-y-3">
|
||||
{/* File name - bold on top */}
|
||||
<Text className="font-bold text-lg">
|
||||
{backup.filename}
|
||||
</Text>
|
||||
|
||||
{/* Date and size line */}
|
||||
<Flex className="items-center justify-between text-sm text-muted-foreground">
|
||||
<Flex className="items-center gap-2">
|
||||
{backupDate ? (
|
||||
<TimeAgo date={backupDate.getTime()} />
|
||||
) : (
|
||||
<span>{backup.created_date}</span>
|
||||
)}
|
||||
</Flex>
|
||||
<span>{backup.size_formatted}</span>
|
||||
</Flex>
|
||||
|
||||
{/* Buttons row */}
|
||||
<Flex className="items-center justify-between">
|
||||
{/* Delete button on the left */}
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<ToolTip
|
||||
side="bottom"
|
||||
text={t('Delete', { ns: 'backuprestore' })}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
disabled={isRestoring || restoringFromFile}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</ToolTip>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t(
|
||||
'Delete this backup? This action cannot be undone.',
|
||||
{ ns: 'backuprestore' }
|
||||
)}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{backup.filename} ({backup.size_formatted})
|
||||
{t('will be permanently deleted.', {
|
||||
ns: 'backuprestore',
|
||||
})}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t('Cancel', { ns: 'common' })}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() =>
|
||||
handleDeleteBackup(
|
||||
backup.full_path,
|
||||
backup.filename
|
||||
)
|
||||
}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
{t('Delete', { ns: 'backuprestore' })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Browse and Restore buttons on the right */}
|
||||
<Flex className="gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleBrowseBackupFolder(backup.full_path)
|
||||
}
|
||||
disabled={isRestoring || restoringFromFile}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 mr-1" />
|
||||
{t('Browse', { ns: 'backuprestore' })}
|
||||
</Button>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isRestoring || restoringFromFile}
|
||||
>
|
||||
{restoringBackupPath ===
|
||||
backup.full_path ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||
{t('Restoring...', {
|
||||
ns: 'backuprestore',
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RotateCcw className="w-4 h-4 mr-1" />
|
||||
{t('Restore', { ns: 'backuprestore' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t(
|
||||
'Restore from {{filename}}? This will replace all current data.',
|
||||
{
|
||||
ns: 'backuprestore',
|
||||
filename: backup.filename,
|
||||
}
|
||||
)}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t(
|
||||
'This action cannot be undone. All current data will be replaced with the backup data.',
|
||||
{ ns: 'backuprestore' }
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t('Cancel', { ns: 'common' })}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() =>
|
||||
handleRestoreBackup(
|
||||
backup.full_path,
|
||||
backup.filename
|
||||
)
|
||||
}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
{t('Restore', { ns: 'backuprestore' })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Restore note */}
|
||||
<Text className="text-xs text-muted-foreground mt-2">
|
||||
{t(
|
||||
'Restoring a backup will automatically restart the application.',
|
||||
{ ns: 'backuprestore' }
|
||||
)}
|
||||
</Text>
|
||||
{totalSize && (
|
||||
<Badge variant="secondary">
|
||||
{t('Total backup space: {{size}}', {
|
||||
ns: 'settings',
|
||||
size: totalSize
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{isLoadingBackups ? (
|
||||
<Flex className="items-center justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
<Text className="ml-2">Loading backups...</Text>
|
||||
</Flex>
|
||||
) : backups.length === 0 ? (
|
||||
<Box className="text-center py-8 text-muted-foreground">
|
||||
<HardDrive className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<Text>{t('No backups found', { ns: 'backuprestore' })}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Box className="space-y-3">
|
||||
{backups.map((backup) => (
|
||||
<Card key={backup.filename} className="p-4">
|
||||
<Flex className="items-center justify-between">
|
||||
<Box className="flex-1">
|
||||
<Flex className="items-center gap-2 mb-1">
|
||||
<Archive className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<Text className="font-medium">{backup.filename}</Text>
|
||||
</Flex>
|
||||
<TextNormal className="text-sm text-muted-foreground">
|
||||
{t('Created', { ns: 'common' })}: {backup.created_date}
|
||||
</TextNormal>
|
||||
<TextNormal className="text-sm text-muted-foreground">
|
||||
{t('Size', { ns: 'common' })}: {backup.size_formatted}
|
||||
</TextNormal>
|
||||
</Box>
|
||||
|
||||
<Flex className="gap-2">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isRestoring}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4 mr-1" />
|
||||
{t('Restore', { ns: 'backuprestore' })}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t('Restore from {{filename}}? This will replace all current data.', {
|
||||
ns: 'settings',
|
||||
filename: backup.filename
|
||||
})}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. All current data will be replaced with the backup data.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('Cancel', { ns: 'common' })}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleRestoreBackup(backup.full_path, backup.filename)}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
{t('Restore', { ns: 'backuprestore' })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t('Delete this backup? This action cannot be undone.', { ns: 'backuprestore' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{backup.filename} ({backup.size_formatted}) will be permanently deleted.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('Cancel', { ns: 'common' })}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleDeleteBackup(backup.full_path, backup.filename)}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
{t('Delete', { ns: 'backuprestore' })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Spacer h={6} />
|
||||
<Link to={returnRoute} replace>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-sm bg-slate-200 dark:bg-slate-700 dark:text-slate-200"
|
||||
size="sm"
|
||||
>
|
||||
{t('Back', { ns: 'common' })}
|
||||
</Button>
|
||||
</Link>
|
||||
<Spacer h={4} />
|
||||
</Box>
|
||||
</SimpleBar>
|
||||
</Box>
|
||||
</SimpleBar>
|
||||
)}
|
||||
</AutoSize>
|
||||
</Box>
|
||||
)
|
||||
)
|
||||
}}
|
||||
</AutoSize>
|
||||
)
|
||||
}
|
@ -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()
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
|
||||
// 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<String, String> {
|
||||
|
||||
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<BackupListResponse, String> {
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn select_backup_file() -> Result<Option<String>, 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<String, String> {
|
||||
pub async fn restore_backup(backup_path: String, create_pre_restore_backup: bool) -> Result<String, String> {
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user