backup and restore implementation

This commit is contained in:
Sergey Kurdin 2025-06-12 11:39:10 -04:00
parent 4f7d2aac29
commit a1c2837623
22 changed files with 1555 additions and 3 deletions

View File

@ -0,0 +1,200 @@
## Backup and Restore Feature Implementation Plan
Based on my analysis of the PasteBar codebase, here's a comprehensive plan to implement the Backup and Restore functionality:
### 1. **Frontend Implementation**
#### A. Add Navigation Entry (in AppSettings.tsx)
- Add a new NavLink for "Backup and Restore" between User Preferences and Security sections
- Route path: `/app-settings/backup-restore`
#### B. Create BackupRestoreSettings.tsx Component
The component will include:
- **Backup Section:**
- Checkbox options: "Include images" (checked by default)
- "Backup Now" button
- Progress indicator during backup
- **Confirmation dialog before backup:** "Create a backup of your data?"
- After backup: Dialog to move file or keep in current location
- **Restore Section:**
- **"Restore from File" button** - Opens file picker to select backup from any location
- List of existing backup files (parsed from filesystem)
- Display: backup filename, date/time, file size
- **Total backup space indicator** at the top of the list
- "Restore" button for each backup
- **"Delete" button for each backup** (trash icon)
- **Confirmation dialog before restore:** "This will replace all current data. Are you sure?"
- **Confirmation dialog before delete:** "Delete this backup? This action cannot be undone."
#### C. Update Router Configuration (pages/index.tsx)
- Add route: `{ path: 'backup-restore', element: <BackupRestoreSettings /> }`
### 2. **Backend Implementation (Rust/Tauri)**
#### A. Create Backup/Restore Commands Module
`src-tauri/src/commands/backup_restore_commands.rs`:
- `create_backup(include_images: bool)` - Creates zip file with:
- Database file (pastebar-db.data)
- clip-images/ folder (if include_images is true)
- history-images/ folder (if include_images is true)
- Returns: backup file path
- `list_backups()` - Lists all backup files in data directory
- Returns: Vec<BackupInfo> with filename, date, size
- **Calculates total size of all backups**
- `restore_backup(backup_path: String)` - Restores from backup:
- Validates zip file (works with both local and external paths)
- Creates temporary backup of current data
- Extracts and replaces database and image folders
- Returns: success/error status
- `select_backup_file()` - Opens native file picker
- Filters for .zip files
- Returns selected file path
- Validates that it's a valid PasteBar backup
- `delete_backup(backup_path: String)` - Deletes a backup file
- Validates file exists and is a backup
- Deletes the file
- Returns: success/error status
- `get_data_paths()` - Gets current database and image folder paths
- Checks for custom data location setting
- Returns default or custom paths
#### B. Update main.rs
- Register new commands in the Tauri builder
### 3. **File Structure and Naming**
- Backup filename format: `pastebar-data-backup-YYYY-MM-DD-HH-mm.zip`
- Default location: Same as database location (custom or default)
- Zip structure:
```
pastebar-data-backup-2024-01-06-14-30.zip
├── pastebar-db.data
├── clip-images/
│ └── [image files]
└── history-images/
└── [image files]
```
### 4. **UI/UX Flow**
1. **Creating a Backup:**
- User navigates to Settings → Backup and Restore
- Selects backup options (include images or not)
- Clicks "Backup Now"
- **Confirmation dialog:** "Create a backup of your data?"
- Progress indicator shows during compression
- Dialog appears: "Backup created successfully. Move to another location?"
- Options: "Move...", "Keep in current location"
2. **Restoring from List:**
- User sees list of available backups with total space used
- Clicks "Restore" on desired backup
- **Confirmation dialog:** "This will replace all current data. Are you sure?"
- Progress indicator during restore
- App automatically restarts after successful restore
3. **Restoring from External File:**
- User clicks "Restore from File" button
- Native file picker opens (filtered for .zip files)
- User selects backup file from any location (external drive, cloud folder, etc.)
- File is validated as a PasteBar backup
- **Confirmation dialog:** "Restore from {{filename}}? This will replace all current data."
- Progress indicator during restore
- App automatically restarts after successful restore
4. **Deleting a Backup:**
- User clicks delete (trash) icon on a backup
- **Confirmation dialog:** "Delete this backup? This action cannot be undone."
- Backup is deleted and list refreshes
- Total backup space updates
### 5. **UI Layout**
```
Backup and Restore
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Create Backup
─────────────
☑ Include images in backup
[Backup Now]
Restore Data
────────────
[Restore from File...] ← Opens file picker
Available Backups (Total: 152.3 MB)
───────────────────────────────────
📦 pastebar-data-backup-2024-01-06-14-30.zip
Created: January 6, 2024 at 2:30 PM
Size: 25.4 MB
[Restore] [🗑️]
📦 pastebar-data-backup-2024-01-05-09-15.zip
Created: January 5, 2024 at 9:15 AM
Size: 23.1 MB
[Restore] [🗑️]
```
### 6. **Translations to Add**
Settings page titles and descriptions:
- "Backup and Restore"
- "Create Backup"
- "Include images in backup"
- "Backup Now"
- "Restore Data"
- "Restore from File..."
- "Select backup file"
- "Available Backups"
- "Total backup space: {{size}}"
- "No backups found"
- "Restore"
- "Delete"
- "Create a backup of your data?"
- "Backup created successfully"
- "Move to another location?"
- "This will replace all current data. Are you sure?"
- "Restore from {{filename}}? This will replace all current data."
- "Delete this backup? This action cannot be undone."
- "Restore completed. The application will restart."
- "Creating backup..."
- "Restoring backup..."
- "Backup deleted successfully"
- "Failed to delete backup"
- "Invalid backup file"
- "The selected file is not a valid PasteBar backup"
### 7. **Error Handling**
- Handle insufficient disk space
- Validate zip file integrity before restore
- Verify backup contains expected files (pastebar-db.data)
- Create automatic backup before restore operation
- Handle file permission errors
- Rollback on restore failure
- Prevent deletion of backup that's currently being restored
- Handle corrupted or incomplete backup files
- Validate external backup files are from PasteBar
### 8. **Implementation Order**
1. Create backend commands and file operations
2. Add frontend navigation and basic UI
3. Implement backup creation flow with confirmation
4. Implement backup listing with total space calculation
5. Implement restore from list functionality with confirmation
6. Implement "Restore from File" with file picker
7. Implement delete functionality with confirmation
8. Add all confirmation dialogs
9. Add translations for all languages
10. Test complete workflow including external file restore
This implementation provides maximum flexibility for users to manage their backups, whether stored locally or on external drives/cloud storage, while maintaining data safety through multiple confirmation dialogs and validation checks.

184
CLAUDE.md Normal file
View File

@ -0,0 +1,184 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
PasteBar is a cross-platform clipboard manager built with Tauri (Rust + TypeScript/React). It provides unlimited clipboard history, custom clip management, collections, and advanced features like programming language detection and web scraping.
**Technology Stack:**
- **Backend**: Rust with Tauri framework, Diesel ORM (SQLite), Reqwest, Serde, Tokio
- **Frontend**: TypeScript, React, React Query, Vite, TailwindCSS, Jotai, Zustand
- **Platforms**: macOS and Windows (including Apple Silicon M1, Intel, AMD, and ARM)
## Development Commands
### Prerequisites
First install the Diesel CLI:
```bash
cargo install diesel_cli --no-default-features --features sqlite
```
### Main Development Commands
```bash
# Development (starts both frontend and backend in dev mode)
npm start
# or
npm run dev
# Build for production
npm run build
# Build debug version
npm run app:build:debug
# Platform-specific builds
npm run app:build:osx:universal
npm run app:build:osx:x86_64
npm run app:build:windows:arm
# Database migrations
npm run diesel:migration:run
# Code formatting
npm run format
# Version management
npm run version:sync
```
### Frontend Development (packages/pastebar-app-ui/)
The frontend is a workspace package that builds separately:
```bash
cd packages/pastebar-app-ui
npm run dev # Development server on port 4422
npm run build # Build to dist-ui/
```
### Rust/Tauri Development (src-tauri/)
```bash
cd src-tauri
cargo run --no-default-features # Development mode
cargo build --release # Production build
```
## Architecture Overview
### High-Level Structure
**Tauri Architecture**: The app uses Tauri's hybrid architecture where:
- Rust backend handles core functionality (clipboard monitoring, database operations, system integration)
- TypeScript/React frontend provides the UI
- Communication happens via Tauri commands and events
**Core Components:**
1. **Clipboard Monitoring** (`src-tauri/src/clipboard/mod.rs`)
- Real-time clipboard monitoring using `clipboard-master`
- Automatic image capture and text processing
- Language detection for code snippets
- Configurable exclusion lists and masking
2. **Database Layer** (`src-tauri/src/db.rs` + Diesel)
- SQLite database with migrations in `migrations/`
- Custom data location support with path transformation
- Connection pooling with r2d2
3. **System Integration** (`src-tauri/src/main.rs`)
- System tray menu with dynamic content
- Global hotkeys and window management
- Platform-specific features (macOS accessibility, Windows compatibility)
4. **State Management** (Frontend)
- Jotai for atomic state management
- Zustand stores for settings, collections, clipboard history
- React Query for server state and caching
### Key Patterns
**Path Transformation System**:
- Images are stored with `{{base_folder}}` placeholders for relative paths
- `to_relative_image_path()` and `to_absolute_image_path()` handle conversion
- Enables custom database locations without breaking image references
**Event-Driven Communication**:
- Tauri events for real-time updates between backend and frontend
- Settings synchronization across multiple windows
- Menu rebuilding on state changes
**Multi-Window Architecture**:
- Main window (primary interface)
- History window (clipboard history view)
- QuickPaste window (contextual paste menu)
### Database Schema
Main entities:
- `items` - Custom clips and menu items
- `clipboard_history` - Automatic clipboard captures
- `collections` - Organization containers
- `tabs` - Sub-organization within collections
- `link_metadata` - Web scraping and link preview data
- `settings` - User preferences and configuration
### Frontend Structure
```
packages/pastebar-app-ui/src/
├── components/ # Reusable UI components
├── pages/ # Main application pages
├── store/ # State management (Jotai + Zustand)
├── hooks/ # Custom React hooks
├── lib/ # Utilities and helpers
├── locales/ # Internationalization
└── assets/ # Static assets
```
### Backend Structure
```
src-tauri/src/
├── commands/ # Tauri command handlers
├── services/ # Business logic layer
├── models/ # Database models
├── clipboard/ # Clipboard monitoring
├── menu.rs # System tray menu
├── db.rs # Database configuration
└── main.rs # Application entry point
```
## Important Development Notes
### Settings Management
- Settings are stored as generic key-value pairs in the database
- Frontend uses `settingsStore.ts` with automatic synchronization
- Use `updateSetting()` function and include `invoke('build_system_menu')` for settings that affect the system tray
### Custom Data Locations
- The app supports custom database locations via user settings
- All file operations must use `get_data_dir()`, `get_clip_images_dir()`, etc.
- Path transformation ensures image references work across location changes
### Image Handling
- Images are stored in both thumbnail and full resolution
- Use path transformation helpers when storing/retrieving image paths
- Images support relative paths with `{{base_folder}}` placeholders
### Internationalization
- Backend translations in `src-tauri/src/services/translations/translations.yaml`
- Frontend translations in `packages/pastebar-app-ui/src/locales/lang/`
- Use `t()` function in React components and `Translations::get()` in Rust
### Debug Logging
- Use `debug_output(|| { println!("message") })` in Rust for debug-only logging
- Debug messages only appear in debug builds, keeping release builds clean
### System Tray Menu
- Dynamic menu built from database items and settings
- Rebuild required when items or relevant settings change
- Use `invoke('build_system_menu')` after operations that affect menu content
### Database Migrations
- Use Diesel migrations for schema changes
- Place migration files in `migrations/` directory
- Run migrations with `npm run diesel:migration:run`

View File

@ -0,0 +1,27 @@
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
Available Backups: Available Backups
Total backup space: "{{size}}": Total backup space: {{size}}
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 completed. The application will restart.: Restore completed. The application will restart.
Creating backup...: Creating backup...
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
Size: Size

View File

@ -0,0 +1,27 @@
Backup and Restore: Copia de Seguridad y Restauración
Create Backup: Crear Copia de Seguridad
Include images in backup: Incluir imágenes en la copia de seguridad
Backup Now: Crear Copia Ahora
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}}
No backups found: No se encontraron copias de seguridad
Restore: Restaurar
Delete: Eliminar
Create a backup of your data?: ¿Crear una copia de seguridad de tus datos?
Backup created successfully: Copia de seguridad creada exitosamente
Move to another location?: ¿Mover a otra ubicación?
This will replace all current data. Are you sure?: Esto reemplazará todos los datos actuales. ¿Estás seguro?
Restore from "{{filename}}"? This will replace all current data.: ¿Restaurar desde {{filename}}? Esto reemplazará todos los datos actuales.
Delete this backup? This action cannot be undone.: ¿Eliminar esta copia de seguridad? Esta acción no se puede deshacer.
Restore completed. The application will restart.: Restauración completada. La aplicación se reiniciará.
Creating backup...: Creando copia de seguridad...
Restoring backup...: Restaurando copia de seguridad...
Backup deleted successfully: Copia de seguridad eliminada exitosamente
Failed to delete backup: Error al eliminar la copia de seguridad
Invalid backup file: Archivo de copia de seguridad inválido
The selected file is not a valid PasteBar backup: El archivo seleccionado no es una copia de seguridad válida de PasteBar
Created: Creado
Size: Tamaño

View File

@ -0,0 +1,27 @@
Backup and Restore: Sauvegarde et Restauration
Create Backup: Créer une Sauvegarde
Include images in backup: Inclure les images dans la sauvegarde
Backup Now: Sauvegarder Maintenant
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}}
No backups found: Aucune sauvegarde trouvée
Restore: Restaurer
Delete: Supprimer
Create a backup of your data?: Créer une sauvegarde de vos données?
Backup created successfully: Sauvegarde créée avec succès
Move to another location?: Déplacer vers un autre emplacement?
This will replace all current data. Are you sure?: Ceci remplacera toutes les données actuelles. Êtes-vous sûr?
Restore from "{{filename}}"? This will replace all current data.: Restaurer depuis {{filename}}? Ceci remplacera toutes les données actuelles.
Delete this backup? This action cannot be undone.: Supprimer cette sauvegarde? Cette action ne peut pas être annulée.
Restore completed. The application will restart.: Restauration terminée. L'application va redémarrer.
Creating backup...: Création de la sauvegarde...
Restoring backup...: Restauration de la sauvegarde...
Backup deleted successfully: Sauvegarde supprimée avec succès
Failed to delete backup: Échec de la suppression de la sauvegarde
Invalid backup file: Fichier de sauvegarde invalide
The selected file is not a valid PasteBar backup: Le fichier sélectionné n'est pas une sauvegarde PasteBar valide
Created: Créé
Size: Taille

View File

@ -181,4 +181,4 @@ User Preferences: Préférences utilisateur
Web Scraping and Parsing: Extraction et analyse web
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Les mots ou phrases énumérés ci-dessous ne seront pas capturés dans l'historique du presse-papiers s'ils se trouvent dans le texte copié. Insensible à la casse
passcode reset: réinitialisation code d'accès
password reset: réinitialisation mot de passe
password reset: réinitialisation mot de passe

View File

@ -0,0 +1,27 @@
Backup and Restore: Backup e Ripristino
Create Backup: Crea Backup
Include images in backup: Includi immagini nel backup
Backup Now: Crea Backup Ora
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}}
No backups found: Nessun backup trovato
Restore: Ripristina
Delete: Elimina
Create a backup of your data?: Creare un backup dei tuoi dati?
Backup created successfully: Backup creato con successo
Move to another location?: Spostare in un'altra posizione?
This will replace all current data. Are you sure?: Questo sostituirà tutti i dati attuali. Sei sicuro?
Restore from "{{filename}}"? This will replace all current data.: Ripristinare da {{filename}}? Questo sostituirà tutti i dati attuali.
Delete this backup? This action cannot be undone.: Eliminare questo backup? Questa azione non può essere annullata.
Restore completed. The application will restart.: Ripristino completato. L'applicazione si riavvierà.
Creating backup...: Creazione backup...
Restoring backup...: Ripristino backup...
Backup deleted successfully: Backup eliminato con successo
Failed to delete backup: Impossibile eliminare il backup
Invalid backup file: File di backup non valido
The selected file is not a valid PasteBar backup: Il file selezionato non è un backup PasteBar valido
Created: Creato
Size: Dimensione

View File

@ -180,4 +180,4 @@ User Preferences: Preferenze Utente
Web Scraping and Parsing: Web Scraping e Parsing
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Le parole o frasi elencate di seguito non verranno catturate nella cronologia degli appunti se trovate nel testo copiato. Non sensibile alle maiuscole/minuscole.
passcode reset: reset del codice di accesso
password reset: reset della password
password reset: reset della password

View File

@ -0,0 +1,27 @@
Backup and Restore: Резервное копирование и восстановление
Create Backup: Создать резервную копию
Include images in backup: Включить изображения в резервную копию
Backup Now: Создать резервную копию сейчас
Restore Data: Восстановить данные
Restore from File...: Восстановить из файла...
Select backup file: Выбрать файл резервной копии
Available Backups: Доступные резервные копии
Total backup space: "{{size}}": Общий размер резервных копий: {{size}}
No backups found: Резервные копии не найдены
Restore: Восстановить
Delete: Удалить
Create a backup of your data?: Создать резервную копию ваших данных?
Backup created successfully: Резервная копия успешно создана
Move to another location?: Переместить в другое место?
This will replace all current data. Are you sure?: Это заменит все текущие данные. Вы уверены?
Restore from "{{filename}}"? This will replace all current data.: Восстановить из {{filename}}? Это заменит все текущие данные.
Delete this backup? This action cannot be undone.: Удалить эту резервную копию? Это действие нельзя отменить.
Restore completed. The application will restart.: Восстановление завершено. Приложение перезапустится.
Creating backup...: Создание резервной копии...
Restoring backup...: Восстановление резервной копии...
Backup deleted successfully: Резервная копия успешно удалена
Failed to delete backup: Не удалось удалить резервную копию
Invalid backup file: Недействительный файл резервной копии
The selected file is not a valid PasteBar backup: Выбранный файл не является действительной резервной копией PasteBar
Created: Создано
Size: Размер

View File

@ -0,0 +1,27 @@
Backup and Restore: Yedekleme ve Geri Yükleme
Create Backup: Yedek Oluştur
Include images in backup: Yedekte görselleri dahil et
Backup Now: Şimdi Yedekle
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}}
No backups found: Yedek bulunamadı
Restore: Geri Yükle
Delete: Sil
Create a backup of your data?: Verilerinizin bir yedeğini oluşturmak istiyor musunuz?
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.
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...
Restoring backup...: Yedek geri yükleniyor...
Backup deleted successfully: Yedek başarıyla silindi
Failed to delete backup: Yedek silinemedi
Invalid backup file: Geçersiz yedek dosyası
The selected file is not a valid PasteBar backup: Seçilen dosya geçerli bir PasteBar yedeği değil
Created: Oluşturulma
Size: Boyut

View File

@ -0,0 +1,27 @@
Backup and Restore: Резервне копіювання та відновлення
Create Backup: Створити резервну копію
Include images in backup: Включити зображення в резервну копію
Backup Now: Створити резервну копію зараз
Restore Data: Відновити дані
Restore from File...: Відновити з файлу...
Select backup file: Вибрати файл резервної копії
Available Backups: Доступні резервні копії
Total backup space: "{{size}}": Загальний розмір резервних копій: {{size}}
No backups found: Резервні копії не знайдені
Restore: Відновити
Delete: Видалити
Create a backup of your data?: Створити резервну копію ваших даних?
Backup created successfully: Резервну копію успішно створено
Move to another location?: Перемістити в інше місце?
This will replace all current data. Are you sure?: Це замінить всі поточні дані. Ви впевнені?
Restore from "{{filename}}"? This will replace all current data.: Відновити з {{filename}}? Це замінить всі поточні дані.
Delete this backup? This action cannot be undone.: Видалити цю резервну копію? Цю дію неможливо скасувати.
Restore completed. The application will restart.: Відновлення завершено. Додаток перезапуститься.
Creating backup...: Створення резервної копії...
Restoring backup...: Відновлення резервної копії...
Backup deleted successfully: Резервну копію успішно видалено
Failed to delete backup: Не вдалося видалити резервну копію
Invalid backup file: Недійсний файл резервної копії
The selected file is not a valid PasteBar backup: Вибраний файл не є дійсною резервною копією PasteBar
Created: Створено
Size: Розмір

View File

@ -164,4 +164,29 @@ User Preferences: Користувацькі налаштування
Web Scraping and Parsing: Веб-скрейпінг та аналіз
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Слова або речення, перелічені нижче, не будуть захоплюватися в історію буфера обміну, якщо вони будуть знайдені в скопійованому тексті. Без урахування регістру.
passcode reset: скидання коду доступу
password reset: скидання пароля
password reset: скидання пароля
Backup and Restore: Резервне копіювання та відновлення
Create Backup: Створити резервну копію
Include images in backup: Включити зображення в резервну копію
Backup Now: Створити резервну копію зараз
Restore Data: Відновити дані
Restore from File...: Відновити з файлу...
Select backup file: Вибрати файл резервної копії
Available Backups: Доступні резервні копії
Total backup space: "{{size}}": Загальний розмір резервних копій: {{size}}
No backups found: Резервні копії не знайдені
Restore: Відновити
Delete: Видалити
Create a backup of your data?: Створити резервну копію ваших даних?
Backup created successfully: Резервну копію успішно створено
Move to another location?: Перемістити в інше місце?
This will replace all current data. Are you sure?: Це замінить всі поточні дані. Ви впевнені?
Restore from "{{filename}}"? This will replace all current data.: Відновити з {{filename}}? Це замінить всі поточні дані.
Delete this backup? This action cannot be undone.: Видалити цю резервну копію? Цю дію неможливо скасувати.
Restore completed. The application will restart.: Відновлення завершено. Додаток перезапуститься.
Creating backup...: Створення резервної копії...
Restoring backup...: Відновлення резервної копії...
Backup deleted successfully: Резервну копію успішно видалено
Failed to delete backup: Не вдалося видалити резервну копію
Invalid backup file: Недійсний файл резервної копії
The selected file is not a valid PasteBar backup: Вибраний файл не є дійсною резервною копією PasteBar

View File

@ -0,0 +1,27 @@
Backup and Restore: 备份和恢复
Create Backup: 创建备份
Include images in backup: 在备份中包含图片
Backup Now: 立即备份
Restore Data: 恢复数据
Restore from File...: 从文件恢复...
Select backup file: 选择备份文件
Available Backups: 可用备份
Total backup space: "{{size}}": 备份总空间: {{size}}
No backups found: 未找到备份
Restore: 恢复
Delete: 删除
Create a backup of your data?: 创建您数据的备份?
Backup created successfully: 备份创建成功
Move to another location?: 移动到其他位置?
This will replace all current data. Are you sure?: 这将替换所有当前数据。您确定吗?
Restore from "{{filename}}"? This will replace all current data.: 从{{filename}}恢复?这将替换所有当前数据。
Delete this backup? This action cannot be undone.: 删除此备份?此操作无法撤销。
Restore completed. The application will restart.: 恢复完成。应用程序将重新启动。
Creating backup...: 正在创建备份...
Restoring backup...: 正在恢复备份...
Backup deleted successfully: 备份删除成功
Failed to delete backup: 删除备份失败
Invalid backup file: 无效的备份文件
The selected file is not a valid PasteBar backup: 选择的文件不是有效的PasteBar备份
Created: 创建时间
Size: 大小

View File

@ -3,6 +3,7 @@ import { Navigate, RouteObject } from 'react-router-dom'
import ClipboardHistoryPage from './main/ClipboardHistoryPage'
import PasteMenuPage from './main/PasteMenuPage'
import AppSettingsPage from './settings/AppSettings'
import BackupRestoreSettings from './settings/BackupRestoreSettings'
import ClipboardHistorySettings from './settings/ClipboardHistorySettings'
import ManageCollections from './settings/collections/ManageCollections'
import SecuritySettings from './settings/SecuritySettings'
@ -32,6 +33,7 @@ export default [
{ path: 'items', element: <ManageCollections /> },
{ path: 'history', element: <ClipboardHistorySettings /> },
{ path: 'preferences', element: <UserPreferences /> },
{ path: 'backup-restore', element: <BackupRestoreSettings /> },
{ path: 'security', element: <SecuritySettings /> },
],
},

View File

@ -89,6 +89,23 @@ export default function AppSettingsPage() {
)}
</NavLink>
<NavLink
to="/app-settings/backup-restore"
replace
id="app-settings-backup-restore_tour"
>
{({ isActive }) => (
<Text
className={`pr-5 text-right py-3 text-lg justify-end items-center animate fade-in transition-fonts duration-100 dark:!text-slate-400 ${
isActive &&
'!font-bold text-[19px] dark:!text-slate-300 !_text-slate-600'
}`}
>
{t('Backup and Restore', { ns: 'backuprestore' })}
</Text>
)}
</NavLink>
<NavLink
to="/app-settings/security"
replace

View File

@ -0,0 +1,401 @@
import { useEffect, useState } from 'react'
import { invoke } from '@tauri-apps/api'
import { useTranslation } from 'react-i18next'
import {
Archive,
Download,
FolderOpen,
HardDrive,
Loader2,
Package,
RotateCcw,
Trash2,
Upload
} from 'lucide-react'
import { useToast } from '~/components/ui/use-toast'
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,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '~/components/ui/alert-dialog'
import {
Badge,
Box,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
Checkbox,
Flex,
Text,
TextNormal,
} from '~/components/ui'
interface BackupInfo {
filename: string
full_path: string
created_date: string
size: number
size_formatted: string
}
interface BackupListResponse {
backups: BackupInfo[]
total_size: number
total_size_formatted: string
}
export default function BackupRestoreSettings() {
const { t } = useTranslation()
const { toast } = useToast()
const [includeImages, setIncludeImages] = useState(true)
const [isCreatingBackup, setIsCreatingBackup] = useState(false)
const [isRestoring, setIsRestoring] = useState(false)
const [backups, setBackups] = useState<BackupInfo[]>([])
const [totalSize, setTotalSize] = useState('')
const [isLoadingBackups, setIsLoadingBackups] = useState(false)
const loadBackups = async () => {
setIsLoadingBackups(true)
try {
const result = await invoke<BackupListResponse>('list_backups')
setBackups(result.backups)
setTotalSize(result.total_size_formatted)
} catch (error) {
console.error('Failed to load backups:', error)
toast({
title: t('Error', { ns: 'common' }),
description: 'Failed to load backup list',
variant: 'destructive',
})
} finally {
setIsLoadingBackups(false)
}
}
useEffect(() => {
loadBackups()
}, [])
const handleCreateBackup = async () => {
setIsCreatingBackup(true)
try {
const backupPath = await invoke<string>('create_backup', {
includeImages,
})
toast({
title: t('Backup created successfully', { ns: 'backuprestore' }),
description: backupPath,
})
// Reload backup list
await loadBackups()
} catch (error) {
console.error('Failed to create backup:', error)
toast({
title: t('Error', { ns: 'common' }),
description: `Failed to create backup: ${error}`,
variant: 'destructive',
})
} finally {
setIsCreatingBackup(false)
}
}
const handleRestoreBackup = async (backupPath: string, filename: string) => {
setIsRestoring(true)
try {
await invoke('restore_backup', { backupPath })
toast({
title: t('Restore completed. The application will restart.', { ns: 'backuprestore' }),
description: `Restored from ${filename}`,
})
// 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}`,
variant: 'destructive',
})
} finally {
setIsRestoring(false)
}
}
const handleRestoreFromFile = async () => {
try {
const selectedFile = await invoke<string | null>('select_backup_file')
if (selectedFile) {
const filename = selectedFile.split(/[/\\]/).pop() || 'selected file'
await handleRestoreBackup(selectedFile, filename)
}
} catch (error) {
console.error('Failed to select backup file:', error)
toast({
title: t('Error', { ns: 'common' }),
description: `Failed to select backup file: ${error}`,
variant: 'destructive',
})
}
}
const handleDeleteBackup = async (backupPath: string, filename: string) => {
try {
await invoke('delete_backup', { backupPath })
toast({
title: t('Backup deleted successfully', { ns: 'backuprestore' }),
description: filename,
})
// Reload backup list
await loadBackups()
} catch (error) {
console.error('Failed to delete backup:', error)
toast({
title: t('Failed to delete backup', { ns: 'backuprestore' }),
description: `${error}`,
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">
{t('Backup and Restore', { ns: 'backuprestore' })}
</Text>
</Box>
{/* 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>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button disabled={isCreatingBackup} 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>
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} />
{/* 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: 'settings',
size: totalSize
})}
</Badge>
)}
</Flex>
{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>
</Box>
</SimpleBar>
)}
</AutoSize>
</Box>
)
}

2
pastebar_settings.yaml Normal file
View File

@ -0,0 +1,2 @@
custom_db_path: null
data: {}

107
src-tauri/Cargo.lock generated
View File

@ -428,6 +428,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "bcrypt"
version = "0.15.1"
@ -622,6 +628,26 @@ dependencies = [
"serde",
]
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "cairo-rs"
version = "0.15.12"
@ -670,6 +696,10 @@ name = "cc"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc"
dependencies = [
"jobserver",
"libc",
]
[[package]]
name = "cesu8"
@ -922,6 +952,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "convert_case"
version = "0.4.0"
@ -2780,6 +2816,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.1"
@ -3867,6 +3912,17 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "password-hash"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "pastebar-app"
version = "0.0.1"
@ -3939,6 +3995,7 @@ dependencies = [
"winapi",
"window-state",
"winreg 0.52.0",
"zip",
]
[[package]]
@ -3947,6 +4004,18 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pbkdf2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
dependencies = [
"digest",
"hmac",
"password-hash",
"sha2",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@ -7482,9 +7551,47 @@ version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"flate2",
"hmac",
"pbkdf2",
"sha1",
"time",
"zstd",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "5.0.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.15+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
dependencies = [
"cc",
"pkg-config",
]
[[package]]

View File

@ -85,6 +85,7 @@ tl = { version = "0.7.7" }
tld = "2.33.0"
url = "2.4.1"
html-escape = "0.2.13"
zip = "0.6"
[target.'cfg(target_os = "macos")'.dependencies]
macos-accessibility-client = { git = "https://github.com/kurdin/macos-accessibility-client", branch = "master", version = "0.0.1" }

View File

@ -0,0 +1,389 @@
use std::fs;
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::services::utils::debug_output;
#[derive(Debug, Serialize, Deserialize)]
pub struct BackupInfo {
pub filename: String,
pub full_path: String,
pub created_date: String,
pub size: u64,
pub size_formatted: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BackupListResponse {
pub backups: Vec<BackupInfo>,
pub total_size: u64,
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();
format!("pastebar-data-backup-{}.zip", now.format("%Y-%m-%d-%H-%M"))
}
fn format_file_size(size: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
let mut size_f = size as f64;
let mut unit_index = 0;
while size_f >= 1024.0 && unit_index < UNITS.len() - 1 {
size_f /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", size, UNITS[unit_index])
} else {
format!("{:.1} {}", size_f, UNITS[unit_index])
}
}
fn add_directory_to_zip<W: Write + Seek>(
zip: &mut ZipWriter<W>,
dir_path: &Path,
base_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
if !dir_path.exists() {
debug_output(|| {
println!("Directory does not exist: {}", dir_path.display());
});
return Ok(());
}
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o755);
for entry in fs::read_dir(dir_path)? {
let entry = entry?;
let path = entry.path();
let relative_path = path.strip_prefix(base_path)?;
if path.is_dir() {
// Add directory entry
let dir_name = format!("{}/", relative_path.display());
zip.start_file(dir_name, options)?;
// Recursively add directory contents
add_directory_to_zip(zip, &path, base_path)?;
} else {
// Add file
let mut file = fs::File::open(&path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
zip.start_file(relative_path.to_string_lossy(), options)?;
zip.write_all(&buffer)?;
}
}
Ok(())
}
#[tauri::command]
pub async fn create_backup(include_images: bool) -> Result<String, String> {
debug_output(|| {
println!("Creating backup with include_images: {}", include_images);
});
let data_dir = get_data_dir();
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);
if !db_path.exists() {
return Err("Database file not found".to_string());
}
// Create zip file
let file = fs::File::create(&backup_path)
.map_err(|e| format!("Failed to create backup file: {}", e))?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o644);
// Add database file
let mut db_file = fs::File::open(&db_path)
.map_err(|e| format!("Failed to open database file: {}", e))?;
let mut db_buffer = Vec::new();
db_file.read_to_end(&mut db_buffer)
.map_err(|e| format!("Failed to read database file: {}", e))?;
zip.start_file(db_filename, options)
.map_err(|e| format!("Failed to start database file in zip: {}", e))?;
zip.write_all(&db_buffer)
.map_err(|e| format!("Failed to write database to zip: {}", e))?;
// 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");
if clip_images_dir.exists() {
add_directory_to_zip(&mut zip, &clip_images_dir, &data_dir)
.map_err(|e| format!("Failed to add clip-images directory: {}", e))?;
}
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))?;
}
}
zip.finish()
.map_err(|e| format!("Failed to finalize zip file: {}", e))?;
debug_output(|| {
println!("Backup created successfully: {}", backup_path.display());
});
Ok(backup_path.to_string_lossy().to_string())
}
#[tauri::command]
pub async fn list_backups() -> Result<BackupListResponse, String> {
let data_dir = get_data_dir();
let mut backups = Vec::new();
let mut total_size = 0u64;
if let Ok(entries) = fs::read_dir(&data_dir) {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if let Some(filename) = path.file_name() {
let filename_str = filename.to_string_lossy();
if filename_str.starts_with("pastebar-data-backup-") && filename_str.ends_with(".zip") {
if let Ok(metadata) = entry.metadata() {
let size = metadata.len();
total_size += size;
// Parse date from filename
let created_date = if let Some(date_part) = filename_str
.strip_prefix("pastebar-data-backup-")
.and_then(|s| s.strip_suffix(".zip"))
{
// Format: YYYY-MM-DD-HH-MM
if let Ok(parsed_date) = DateTime::parse_from_str(
&format!("{} +0000", date_part.replace('-', " ").replacen(' ', "-", 2).replacen(' ', "-", 1).replacen(' ', ":", 1)),
"%Y-%m-%d-%H-%M %z"
) {
parsed_date.format("%B %d, %Y at %I:%M %p").to_string()
} else {
"Unknown date".to_string()
}
} else {
"Unknown date".to_string()
};
backups.push(BackupInfo {
filename: filename_str.to_string(),
full_path: path.to_string_lossy().to_string(),
created_date,
size,
size_formatted: format_file_size(size),
});
}
}
}
}
}
}
// Sort by filename (which includes date) in descending order
backups.sort_by(|a, b| b.filename.cmp(&a.filename));
Ok(BackupListResponse {
backups,
total_size,
total_size_formatted: format_file_size(total_size),
})
}
#[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> {
debug_output(|| {
println!("Restoring backup from: {}", backup_path);
});
// Validate backup file
validate_backup_file(&backup_path)?;
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 {
debug_output(|| {
println!("Warning: Could not create pre-restore backup: {}", e);
});
}
// Open the backup zip file
let file = fs::File::open(&backup_path)
.map_err(|e| format!("Failed to open backup file: {}", e))?;
let mut archive = ZipArchive::new(file)
.map_err(|e| format!("Failed to read backup file: {}", e))?;
// Extract files
for i in 0..archive.len() {
let mut file = archive.by_index(i)
.map_err(|e| format!("Failed to read file from backup: {}", e))?;
let outpath = data_dir.join(file.name());
if file.name().ends_with('/') {
// Directory
fs::create_dir_all(&outpath)
.map_err(|e| format!("Failed to create directory: {}", e))?;
} else {
// File
if let Some(parent) = outpath.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create parent directory: {}", e))?;
}
let mut outfile = fs::File::create(&outpath)
.map_err(|e| format!("Failed to create file: {}", e))?;
std::io::copy(&mut file, &mut outfile)
.map_err(|e| format!("Failed to extract file: {}", e))?;
}
}
debug_output(|| {
println!("Backup restored successfully from: {}", backup_path);
});
Ok("Backup restored successfully".to_string())
}
#[tauri::command]
pub async fn delete_backup(backup_path: String) -> Result<String, String> {
let path = Path::new(&backup_path);
if !path.exists() {
return Err("Backup file does not exist".to_string());
}
// Validate it's actually a backup file
if let Some(filename) = path.file_name() {
let filename_str = filename.to_string_lossy();
if !filename_str.starts_with("pastebar-data-backup-") || !filename_str.ends_with(".zip") {
return Err("File is not a valid backup file".to_string());
}
} else {
return Err("Invalid file path".to_string());
}
fs::remove_file(path)
.map_err(|e| format!("Failed to delete backup file: {}", e))?;
debug_output(|| {
println!("Backup deleted successfully: {}", backup_path);
});
Ok("Backup deleted successfully".to_string())
}
#[tauri::command]
pub async fn get_data_paths() -> Result<serde_json::Value, String> {
let data_dir = get_data_dir();
Ok(serde_json::json!({
"data_dir": data_dir.to_string_lossy(),
"database_file": if cfg!(debug_assertions) { "local.pastebar-db.data" } else { "pastebar-db.data" },
"clip_images_dir": data_dir.join("clip-images").to_string_lossy(),
"history_images_dir": data_dir.join("history-images").to_string_lossy()
}))
}

View File

@ -1,3 +1,4 @@
pub(crate) mod backup_restore_commands;
pub(crate) mod clipboard_commands;
pub(crate) mod collections_commands;
pub(crate) mod download_update;

View File

@ -52,6 +52,7 @@ use crate::services::settings_service::get_all_settings;
use crate::services::translations::translations::Translations;
use crate::services::utils::ensure_url_or_email_prefix;
use crate::services::utils::remove_special_bbcode_tags;
use commands::backup_restore_commands;
use commands::clipboard_commands;
use commands::collections_commands;
use commands::download_update;
@ -1096,6 +1097,12 @@ async fn main() {
app_ready,
get_app_settings,
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,
tabs_commands::delete_tab,
tabs_commands::create_tab,
tabs_commands::update_tab,