Enhance backup and restore features with improved UI, new translations, and error handling for better user experience

This commit is contained in:
Sergey Kurdin 2025-06-12 16:39:57 -04:00
parent 1aa8e5d28c
commit 488f746683
18 changed files with 765 additions and 369 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
local.pastebar.*
safelist.txt
pastebar-data-backup*
clipboard-images/**/*
clip-images/**/*

View File

@ -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

View File

@ -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' })}

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.: Восстановление резервной копии автоматически перезапустит приложение.

View File

@ -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: Невозможно сохранить файл

View 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.

View File

@ -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.: Відновлення резервної копії автоматично перезапустить додаток.

View File

@ -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.: 恢复备份将自动重启应用程序。

View File

@ -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,53 +294,92 @@ 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">
{({ 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 gap-2">
<CardTitle className="flex items-center justify-between">
<Flex className="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">
</Flex>
{/* Include Images option - right corner */}
<Flex className="items-center text-sm">
<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"
onChange={checked => setIncludeImages(checked as boolean)}
>
{t('Include images in backup', { ns: 'backuprestore' })}
</label>
</Checkbox>
</Flex>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button disabled={isCreatingBackup} className="w-full">
<Button
disabled={
isCreatingBackup || isRestoring || restoringFromFile
}
className="w-full"
>
{isCreatingBackup ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
@ -234,15 +396,25 @@ export default function BackupRestoreSettings() {
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t('Create a backup of your data?', { ns: 'backuprestore' })}
{t('Create a backup of your data?', {
ns: 'backuprestore',
})}
</AlertDialogTitle>
<AlertDialogDescription>
This will create a backup file containing your database
{includeImages ? ' and images' : ''}.
{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>
<AlertDialogCancel>
{t('Cancel', { ns: 'common' })}
</AlertDialogCancel>
<AlertDialogAction onClick={handleCreateBackup}>
{t('Create Backup', { ns: 'backuprestore' })}
</AlertDialogAction>
@ -251,24 +423,47 @@ export default function BackupRestoreSettings() {
</AlertDialog>
</CardContent>
</Card>
</Box>
<Box className="animate-in fade-in max-w-xl mt-4">
{/* Restore Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<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}
disabled={isRestoring || restoringFromFile}
className="w-full"
>
{restoringFromFile ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{t('Restoring...', { ns: 'backuprestore' })}
</>
) : (
<>
<FolderOpen className="w-4 h-4 mr-2" />
{t('Restore from File...', { ns: 'backuprestore' })}
</>
)}
</Button>
<Spacer h={4} />
@ -281,9 +476,9 @@ export default function BackupRestoreSettings() {
</Text>
{totalSize && (
<Badge variant="secondary">
{t('Total backup space: {{size}}', {
ns: 'settings',
size: totalSize
{t('Total backup space {{size}}', {
ns: 'backuprestore',
size: totalSize,
})}
</Badge>
)}
@ -292,7 +487,9 @@ export default function BackupRestoreSettings() {
{isLoadingBackups ? (
<Flex className="items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin" />
<Text className="ml-2">Loading backups...</Text>
<Text className="ml-2">
{t('Loading backups...', { ns: 'backuprestore' })}
</Text>
</Flex>
) : backups.length === 0 ? (
<Box className="text-center py-8 text-muted-foreground">
@ -301,81 +498,73 @@ export default function BackupRestoreSettings() {
</Box>
) : (
<Box className="space-y-3">
{backups.map((backup) => (
{backups.map(backup => {
const backupDate = parseBackupDate(backup.filename)
return (
<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>
<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>
<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">
{/* Buttons row */}
<Flex className="items-center justify-between">
{/* Delete button on the left */}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
disabled={isRestoring}
<ToolTip
side="bottom"
text={t('Delete', { 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: '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"
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' })}
{t(
'Delete this backup? This action cannot be undone.',
{ ns: 'backuprestore' }
)}
</AlertDialogTitle>
<AlertDialogDescription>
{backup.filename} ({backup.size_formatted}) will be permanently deleted.
{backup.filename} ({backup.size_formatted})
{t('will be permanently deleted.', {
ns: 'backuprestore',
})}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('Cancel', { ns: 'common' })}</AlertDialogCancel>
<AlertDialogCancel>
{t('Cancel', { ns: 'common' })}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDeleteBackup(backup.full_path, backup.filename)}
onClick={() =>
handleDeleteBackup(
backup.full_path,
backup.filename
)
}
className="bg-red-600 hover:bg-red-700"
>
{t('Delete', { ns: 'backuprestore' })}
@ -383,19 +572,117 @@ export default function BackupRestoreSettings() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Flex>
</Flex>
</Card>
))}
</Box>
{/* 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' })}
</>
)}
</Box>
</CardContent>
</Card>
</Box>
</SimpleBar>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t(
'Restore from {{filename}}? This will replace all current data.',
{
ns: 'backuprestore',
filename: backup.filename,
}
)}
</AutoSize>
</Box>
</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>
</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>
)
)
}}
</AutoSize>
)
}

View File

@ -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()

View File

@ -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;
}

View File

@ -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,88 +237,39 @@ 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
// 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!("Skipping pre-restore backup as requested");
});
}
// Open the backup zip file

View File

@ -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,