backup and restore implementation
This commit is contained in:
parent
4f7d2aac29
commit
a1c2837623
200
BACKUP_RESTORE_IMPLEMENTATION_PLAN.md
Normal file
200
BACKUP_RESTORE_IMPLEMENTATION_PLAN.md
Normal 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
184
CLAUDE.md
Normal 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`
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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: Размер
|
@ -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
|
@ -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: Розмір
|
@ -165,3 +165,28 @@ Web Scraping and Parsing: Веб-скрейпінг та аналіз
|
|||||||
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Слова або речення, перелічені нижче, не будуть захоплюватися в історію буфера обміну, якщо вони будуть знайдені в скопійованому тексті. Без урахування регістру.
|
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Слова або речення, перелічені нижче, не будуть захоплюватися в історію буфера обміну, якщо вони будуть знайдені в скопійованому тексті. Без урахування регістру.
|
||||||
passcode reset: скидання коду доступу
|
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
|
@ -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: 大小
|
@ -3,6 +3,7 @@ import { Navigate, RouteObject } from 'react-router-dom'
|
|||||||
import ClipboardHistoryPage from './main/ClipboardHistoryPage'
|
import ClipboardHistoryPage from './main/ClipboardHistoryPage'
|
||||||
import PasteMenuPage from './main/PasteMenuPage'
|
import PasteMenuPage from './main/PasteMenuPage'
|
||||||
import AppSettingsPage from './settings/AppSettings'
|
import AppSettingsPage from './settings/AppSettings'
|
||||||
|
import BackupRestoreSettings from './settings/BackupRestoreSettings'
|
||||||
import ClipboardHistorySettings from './settings/ClipboardHistorySettings'
|
import ClipboardHistorySettings from './settings/ClipboardHistorySettings'
|
||||||
import ManageCollections from './settings/collections/ManageCollections'
|
import ManageCollections from './settings/collections/ManageCollections'
|
||||||
import SecuritySettings from './settings/SecuritySettings'
|
import SecuritySettings from './settings/SecuritySettings'
|
||||||
@ -32,6 +33,7 @@ export default [
|
|||||||
{ path: 'items', element: <ManageCollections /> },
|
{ path: 'items', element: <ManageCollections /> },
|
||||||
{ path: 'history', element: <ClipboardHistorySettings /> },
|
{ path: 'history', element: <ClipboardHistorySettings /> },
|
||||||
{ path: 'preferences', element: <UserPreferences /> },
|
{ path: 'preferences', element: <UserPreferences /> },
|
||||||
|
{ path: 'backup-restore', element: <BackupRestoreSettings /> },
|
||||||
{ path: 'security', element: <SecuritySettings /> },
|
{ path: 'security', element: <SecuritySettings /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -89,6 +89,23 @@ export default function AppSettingsPage() {
|
|||||||
)}
|
)}
|
||||||
</NavLink>
|
</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
|
<NavLink
|
||||||
to="/app-settings/security"
|
to="/app-settings/security"
|
||||||
replace
|
replace
|
||||||
|
@ -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
2
pastebar_settings.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
custom_db_path: null
|
||||||
|
data: {}
|
107
src-tauri/Cargo.lock
generated
107
src-tauri/Cargo.lock
generated
@ -428,6 +428,12 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "bcrypt"
|
||||||
version = "0.15.1"
|
version = "0.15.1"
|
||||||
@ -622,6 +628,26 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "cairo-rs"
|
name = "cairo-rs"
|
||||||
version = "0.15.12"
|
version = "0.15.12"
|
||||||
@ -670,6 +696,10 @@ name = "cc"
|
|||||||
version = "1.1.7"
|
version = "1.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc"
|
checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc"
|
||||||
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cesu8"
|
name = "cesu8"
|
||||||
@ -922,6 +952,12 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -2780,6 +2816,15 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jpeg-decoder"
|
name = "jpeg-decoder"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@ -3867,6 +3912,17 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "pastebar-app"
|
name = "pastebar-app"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@ -3939,6 +3995,7 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
"window-state",
|
"window-state",
|
||||||
"winreg 0.52.0",
|
"winreg 0.52.0",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3947,6 +4004,18 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
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]]
|
[[package]]
|
||||||
name = "peeking_take_while"
|
name = "peeking_take_while"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@ -7482,9 +7551,47 @@ version = "0.6.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
"bzip2",
|
||||||
|
"constant_time_eq",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"crossbeam-utils",
|
"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]]
|
[[package]]
|
||||||
|
@ -85,6 +85,7 @@ tl = { version = "0.7.7" }
|
|||||||
tld = "2.33.0"
|
tld = "2.33.0"
|
||||||
url = "2.4.1"
|
url = "2.4.1"
|
||||||
html-escape = "0.2.13"
|
html-escape = "0.2.13"
|
||||||
|
zip = "0.6"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
macos-accessibility-client = { git = "https://github.com/kurdin/macos-accessibility-client", branch = "master", version = "0.0.1" }
|
macos-accessibility-client = { git = "https://github.com/kurdin/macos-accessibility-client", branch = "master", version = "0.0.1" }
|
||||||
|
389
src-tauri/src/commands/backup_restore_commands.rs
Normal file
389
src-tauri/src/commands/backup_restore_commands.rs
Normal 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()
|
||||||
|
}))
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
pub(crate) mod backup_restore_commands;
|
||||||
pub(crate) mod clipboard_commands;
|
pub(crate) mod clipboard_commands;
|
||||||
pub(crate) mod collections_commands;
|
pub(crate) mod collections_commands;
|
||||||
pub(crate) mod download_update;
|
pub(crate) mod download_update;
|
||||||
|
@ -52,6 +52,7 @@ use crate::services::settings_service::get_all_settings;
|
|||||||
use crate::services::translations::translations::Translations;
|
use crate::services::translations::translations::Translations;
|
||||||
use crate::services::utils::ensure_url_or_email_prefix;
|
use crate::services::utils::ensure_url_or_email_prefix;
|
||||||
use crate::services::utils::remove_special_bbcode_tags;
|
use crate::services::utils::remove_special_bbcode_tags;
|
||||||
|
use commands::backup_restore_commands;
|
||||||
use commands::clipboard_commands;
|
use commands::clipboard_commands;
|
||||||
use commands::collections_commands;
|
use commands::collections_commands;
|
||||||
use commands::download_update;
|
use commands::download_update;
|
||||||
@ -1096,6 +1097,12 @@ async fn main() {
|
|||||||
app_ready,
|
app_ready,
|
||||||
get_app_settings,
|
get_app_settings,
|
||||||
update_setting,
|
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::delete_tab,
|
||||||
tabs_commands::create_tab,
|
tabs_commands::create_tab,
|
||||||
tabs_commands::update_tab,
|
tabs_commands::update_tab,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user