From a1c2837623d83c49c124c31c3e5a0caa2c5d408a Mon Sep 17 00:00:00 2001 From: Sergey Kurdin Date: Thu, 12 Jun 2025 11:39:10 -0400 Subject: [PATCH] backup and restore implementation --- BACKUP_RESTORE_IMPLEMENTATION_PLAN.md | 200 +++++++++ CLAUDE.md | 184 ++++++++ .../src/locales/lang/en/backuprestore.yaml | 27 ++ .../src/locales/lang/esES/backuprestore.yaml | 27 ++ .../src/locales/lang/fr/backuprestore.yaml | 27 ++ .../src/locales/lang/fr/settings.yaml | 2 +- .../src/locales/lang/it/backuprestore.yaml | 27 ++ .../src/locales/lang/it/settings.yaml | 2 +- .../src/locales/lang/ru/backuprestore.yaml | 27 ++ .../src/locales/lang/tr/backuprestore.yaml | 27 ++ .../src/locales/lang/uk/backuprestore.yaml | 27 ++ .../src/locales/lang/uk/settings.yaml | 27 +- .../src/locales/lang/zhCN/backuprestore.yaml | 27 ++ packages/pastebar-app-ui/src/pages/index.tsx | 2 + .../src/pages/settings/AppSettings.tsx | 17 + .../pages/settings/BackupRestoreSettings.tsx | 401 ++++++++++++++++++ pastebar_settings.yaml | 2 + src-tauri/Cargo.lock | 107 +++++ src-tauri/Cargo.toml | 1 + .../src/commands/backup_restore_commands.rs | 389 +++++++++++++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/main.rs | 7 + 22 files changed, 1555 insertions(+), 3 deletions(-) create mode 100644 BACKUP_RESTORE_IMPLEMENTATION_PLAN.md create mode 100644 CLAUDE.md create mode 100644 packages/pastebar-app-ui/src/locales/lang/en/backuprestore.yaml create mode 100644 packages/pastebar-app-ui/src/locales/lang/esES/backuprestore.yaml create mode 100644 packages/pastebar-app-ui/src/locales/lang/fr/backuprestore.yaml create mode 100644 packages/pastebar-app-ui/src/locales/lang/it/backuprestore.yaml create mode 100644 packages/pastebar-app-ui/src/locales/lang/ru/backuprestore.yaml create mode 100644 packages/pastebar-app-ui/src/locales/lang/tr/backuprestore.yaml create mode 100644 packages/pastebar-app-ui/src/locales/lang/uk/backuprestore.yaml create mode 100644 packages/pastebar-app-ui/src/locales/lang/zhCN/backuprestore.yaml create mode 100644 packages/pastebar-app-ui/src/pages/settings/BackupRestoreSettings.tsx create mode 100644 pastebar_settings.yaml create mode 100644 src-tauri/src/commands/backup_restore_commands.rs diff --git a/BACKUP_RESTORE_IMPLEMENTATION_PLAN.md b/BACKUP_RESTORE_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..257129eb --- /dev/null +++ b/BACKUP_RESTORE_IMPLEMENTATION_PLAN.md @@ -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: }` + +### 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 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. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..89f7f962 --- /dev/null +++ b/CLAUDE.md @@ -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` \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/en/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/en/backuprestore.yaml new file mode 100644 index 00000000..1d7a2b27 --- /dev/null +++ b/packages/pastebar-app-ui/src/locales/lang/en/backuprestore.yaml @@ -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 \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/esES/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/esES/backuprestore.yaml new file mode 100644 index 00000000..a9f82d88 --- /dev/null +++ b/packages/pastebar-app-ui/src/locales/lang/esES/backuprestore.yaml @@ -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 \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/fr/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/fr/backuprestore.yaml new file mode 100644 index 00000000..44f38726 --- /dev/null +++ b/packages/pastebar-app-ui/src/locales/lang/fr/backuprestore.yaml @@ -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 \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/fr/settings.yaml b/packages/pastebar-app-ui/src/locales/lang/fr/settings.yaml index e01c6fa7..199afb67 100644 --- a/packages/pastebar-app-ui/src/locales/lang/fr/settings.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/fr/settings.yaml @@ -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 \ No newline at end of file +password reset: réinitialisation mot de passe diff --git a/packages/pastebar-app-ui/src/locales/lang/it/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/it/backuprestore.yaml new file mode 100644 index 00000000..4d36a22b --- /dev/null +++ b/packages/pastebar-app-ui/src/locales/lang/it/backuprestore.yaml @@ -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 \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/it/settings.yaml b/packages/pastebar-app-ui/src/locales/lang/it/settings.yaml index 5b11033c..4ca4a35e 100644 --- a/packages/pastebar-app-ui/src/locales/lang/it/settings.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/it/settings.yaml @@ -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 \ No newline at end of file +password reset: reset della password diff --git a/packages/pastebar-app-ui/src/locales/lang/ru/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/ru/backuprestore.yaml new file mode 100644 index 00000000..7fcc4e84 --- /dev/null +++ b/packages/pastebar-app-ui/src/locales/lang/ru/backuprestore.yaml @@ -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: Размер \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/tr/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/tr/backuprestore.yaml new file mode 100644 index 00000000..e8e6de0b --- /dev/null +++ b/packages/pastebar-app-ui/src/locales/lang/tr/backuprestore.yaml @@ -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 \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/uk/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/uk/backuprestore.yaml new file mode 100644 index 00000000..7764bab8 --- /dev/null +++ b/packages/pastebar-app-ui/src/locales/lang/uk/backuprestore.yaml @@ -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: Розмір \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/uk/settings.yaml b/packages/pastebar-app-ui/src/locales/lang/uk/settings.yaml index 1b1abd21..6074d5a6 100644 --- a/packages/pastebar-app-ui/src/locales/lang/uk/settings.yaml +++ b/packages/pastebar-app-ui/src/locales/lang/uk/settings.yaml @@ -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: скидання пароля \ No newline at end of file +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 \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/locales/lang/zhCN/backuprestore.yaml b/packages/pastebar-app-ui/src/locales/lang/zhCN/backuprestore.yaml new file mode 100644 index 00000000..e723b9d5 --- /dev/null +++ b/packages/pastebar-app-ui/src/locales/lang/zhCN/backuprestore.yaml @@ -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: 大小 \ No newline at end of file diff --git a/packages/pastebar-app-ui/src/pages/index.tsx b/packages/pastebar-app-ui/src/pages/index.tsx index 118d4cff..b6dccf7b 100644 --- a/packages/pastebar-app-ui/src/pages/index.tsx +++ b/packages/pastebar-app-ui/src/pages/index.tsx @@ -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: }, { path: 'history', element: }, { path: 'preferences', element: }, + { path: 'backup-restore', element: }, { path: 'security', element: }, ], }, diff --git a/packages/pastebar-app-ui/src/pages/settings/AppSettings.tsx b/packages/pastebar-app-ui/src/pages/settings/AppSettings.tsx index dbb3c2a6..59d474f7 100644 --- a/packages/pastebar-app-ui/src/pages/settings/AppSettings.tsx +++ b/packages/pastebar-app-ui/src/pages/settings/AppSettings.tsx @@ -89,6 +89,23 @@ export default function AppSettingsPage() { )} + + {({ isActive }) => ( + + {t('Backup and Restore', { ns: 'backuprestore' })} + + )} + + ([]) + const [totalSize, setTotalSize] = useState('') + const [isLoadingBackups, setIsLoadingBackups] = useState(false) + + const loadBackups = async () => { + setIsLoadingBackups(true) + try { + const result = await invoke('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('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('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 ( + + + {({ height }) => ( + + + {/* Header */} + + + + {t('Backup and Restore', { ns: 'backuprestore' })} + + + + {/* Create Backup Section */} + + + + + {t('Create Backup', { ns: 'backuprestore' })} + + + + + setIncludeImages(checked as boolean)} + /> + + + + + + + + + + + {t('Create a backup of your data?', { ns: 'backuprestore' })} + + + This will create a backup file containing your database + {includeImages ? ' and images' : ''}. + + + + {t('Cancel', { ns: 'common' })} + + {t('Create Backup', { ns: 'backuprestore' })} + + + + + + + + {/* Restore Section */} + + + + + {t('Restore Data', { ns: 'backuprestore' })} + + + + + + + + {/* Available Backups */} + + + + {t('Available Backups', { ns: 'backuprestore' })} + + {totalSize && ( + + {t('Total backup space: {{size}}', { + ns: 'settings', + size: totalSize + })} + + )} + + + {isLoadingBackups ? ( + + + Loading backups... + + ) : backups.length === 0 ? ( + + + {t('No backups found', { ns: 'backuprestore' })} + + ) : ( + + {backups.map((backup) => ( + + + + + + {backup.filename} + + + {t('Created', { ns: 'common' })}: {backup.created_date} + + + {t('Size', { ns: 'common' })}: {backup.size_formatted} + + + + + + + + + + + + {t('Restore from {{filename}}? This will replace all current data.', { + ns: 'settings', + filename: backup.filename + })} + + + This action cannot be undone. All current data will be replaced with the backup data. + + + + {t('Cancel', { ns: 'common' })} + handleRestoreBackup(backup.full_path, backup.filename)} + className="bg-red-600 hover:bg-red-700" + > + {t('Restore', { ns: 'backuprestore' })} + + + + + + + + + + + + + {t('Delete this backup? This action cannot be undone.', { ns: 'backuprestore' })} + + + {backup.filename} ({backup.size_formatted}) will be permanently deleted. + + + + {t('Cancel', { ns: 'common' })} + handleDeleteBackup(backup.full_path, backup.filename)} + className="bg-red-600 hover:bg-red-700" + > + {t('Delete', { ns: 'backuprestore' })} + + + + + + + + ))} + + )} + + + + + + )} + + + ) +} \ No newline at end of file diff --git a/pastebar_settings.yaml b/pastebar_settings.yaml new file mode 100644 index 00000000..ab8df09e --- /dev/null +++ b/pastebar_settings.yaml @@ -0,0 +1,2 @@ +custom_db_path: null +data: {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 020ec3a9..26e18684 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6d5c2623..cb365d52 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" } diff --git a/src-tauri/src/commands/backup_restore_commands.rs b/src-tauri/src/commands/backup_restore_commands.rs new file mode 100644 index 00000000..84092781 --- /dev/null +++ b/src-tauri/src/commands/backup_restore_commands.rs @@ -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, + 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( + zip: &mut ZipWriter, + dir_path: &Path, + base_path: &Path, +) -> Result<(), Box> { + 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 { + 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 { + 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, 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 { + 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 { + 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 { + 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() + })) +} \ No newline at end of file diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 3444f702..05cbbfe0 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -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; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4d1f7efc..dfad2e4d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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,