Add Backup and Restore functionality with improved UI, new translations, and enhanced error handling for better user experience

This commit is contained in:
Sergey Kurdin 2025-06-12 17:14:00 -04:00
parent 488f746683
commit 07d0bb6b4e
4 changed files with 307 additions and 282 deletions

View File

@ -0,0 +1,5 @@
---
'pastebar-app-ui': patch
---
Added Backup and Restore database and images

View File

@ -8,7 +8,7 @@ Restore Data: Restaurar Datos
Restore from File...: Restaurar desde Archivo... Restore from File...: Restaurar desde Archivo...
Select backup file: Seleccionar archivo de copia de seguridad Select backup file: Seleccionar archivo de copia de seguridad
Available Backups: Copias de Seguridad Disponibles Available Backups: Copias de Seguridad Disponibles
Total backup space {{size}}: 'Espacio total de copia s: {{size}}' Total backup space {{size}}: 'Espacio total de respaldo: {{size}}'
No backups found: No se encontraron copias de seguridad No backups found: No se encontraron copias de seguridad
Restore: Restaurar Restore: Restaurar
Delete: Eliminar Delete: Eliminar

View File

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api'
import { open } from '@tauri-apps/api/dialog' import { open } from '@tauri-apps/api/dialog'
import { type as getOsType } from '@tauri-apps/api/os'
import { settingsStoreAtom, uiStoreAtom } from '~/store' import { settingsStoreAtom, uiStoreAtom } from '~/store'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { import {
@ -137,10 +138,16 @@ export default function BackupRestoreSettings() {
const handleCreateBackup = async () => { const handleCreateBackup = async () => {
setIsCreatingBackup(true) setIsCreatingBackup(true)
try { try {
const backupPath = await invoke<string>('create_backup', { let backupPath = await invoke<string>('create_backup', {
includeImages, includeImages,
}) })
// Normalize path for Windows display
const osType = await getOsType()
if (osType === 'Windows_NT' && backupPath.startsWith('\\\\?\\')) {
backupPath = backupPath.substring(4)
}
toast({ toast({
id: 'backup-create-success', id: 'backup-create-success',
title: t('Backup created successfully', { ns: 'backuprestore' }), title: t('Backup created successfully', { ns: 'backuprestore' }),

View File

@ -1,13 +1,13 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::io::Write;
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zip::{ZipWriter, ZipArchive}; use std::fs;
use zip::write::FileOptions; use std::io::Write;
use std::io::{Read, Seek}; use std::io::{Read, Seek};
use std::path::{Path, PathBuf};
use zip::write::FileOptions;
use zip::{ZipArchive, ZipWriter};
use crate::db::{get_data_dir, get_db_path, get_clip_images_dir, get_clipboard_images_dir}; use crate::db::{get_clip_images_dir, get_clipboard_images_dir, get_data_dir, get_db_path};
use crate::services::utils::debug_output; use crate::services::utils::debug_output;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -26,7 +26,6 @@ pub struct BackupListResponse {
pub total_size_formatted: String, pub total_size_formatted: String,
} }
fn get_backup_filename() -> String { fn get_backup_filename() -> String {
let now = Local::now(); let now = Local::now();
format!("pastebar-data-backup-{}.zip", now.format("%Y-%m-%d-%H-%M")) format!("pastebar-data-backup-{}.zip", now.format("%Y-%m-%d-%H-%M"))
@ -120,8 +119,8 @@ pub async fn create_backup(include_images: bool) -> Result<String, String> {
} }
// Create zip file // Create zip file
let file = fs::File::create(&backup_path) let file =
.map_err(|e| format!("Failed to create backup file: {}", e))?; fs::File::create(&backup_path).map_err(|e| format!("Failed to create backup file: {}", e))?;
let mut zip = ZipWriter::new(file); let mut zip = ZipWriter::new(file);
let options = FileOptions::default() let options = FileOptions::default()
@ -129,20 +128,24 @@ pub async fn create_backup(include_images: bool) -> Result<String, String> {
.unix_permissions(0o644); .unix_permissions(0o644);
// Add database file // Add database file
let mut db_file = fs::File::open(&db_path) let mut db_file =
.map_err(|e| format!("Failed to open database file: {}", e))?; fs::File::open(&db_path).map_err(|e| format!("Failed to open database file: {}", e))?;
let mut db_buffer = Vec::new(); let mut db_buffer = Vec::new();
db_file.read_to_end(&mut db_buffer) db_file
.read_to_end(&mut db_buffer)
.map_err(|e| format!("Failed to read database file: {}", e))?; .map_err(|e| format!("Failed to read database file: {}", e))?;
// Get just the filename for the zip entry // Get just the filename for the zip entry
let db_filename = db_path.file_name() let db_filename = db_path
.file_name()
.and_then(|name| name.to_str()) .and_then(|name| name.to_str())
.unwrap_or("pastebar-db.data"); .unwrap_or("pastebar-db.data");
zip.start_file(db_filename, options) zip
.start_file(db_filename, options)
.map_err(|e| format!("Failed to start database file in zip: {}", e))?; .map_err(|e| format!("Failed to start database file in zip: {}", e))?;
zip.write_all(&db_buffer) zip
.write_all(&db_buffer)
.map_err(|e| format!("Failed to write database to zip: {}", e))?; .map_err(|e| format!("Failed to write database to zip: {}", e))?;
// Add image directories if requested // Add image directories if requested
@ -168,7 +171,8 @@ pub async fn create_backup(include_images: bool) -> Result<String, String> {
} }
} }
zip.finish() zip
.finish()
.map_err(|e| format!("Failed to finalize zip file: {}", e))?; .map_err(|e| format!("Failed to finalize zip file: {}", e))?;
debug_output(|| { debug_output(|| {
@ -202,8 +206,15 @@ pub async fn list_backups() -> Result<BackupListResponse, String> {
{ {
// Format: YYYY-MM-DD-HH-MM // Format: YYYY-MM-DD-HH-MM
if let Ok(parsed_date) = DateTime::parse_from_str( if let Ok(parsed_date) = DateTime::parse_from_str(
&format!("{} +0000", date_part.replace('-', " ").replacen(' ', "-", 2).replacen(' ', "-", 1).replacen(' ', ":", 1)), &format!(
"%Y-%m-%d-%H-%M %z" "{} +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() parsed_date.format("%B %d, %Y at %I:%M %p").to_string()
} else { } else {
@ -237,9 +248,11 @@ pub async fn list_backups() -> Result<BackupListResponse, String> {
}) })
} }
#[tauri::command] #[tauri::command]
pub async fn restore_backup(backup_path: String, create_pre_restore_backup: bool) -> Result<String, String> { pub async fn restore_backup(
backup_path: String,
create_pre_restore_backup: bool,
) -> Result<String, String> {
debug_output(|| { debug_output(|| {
println!("Restoring backup from: {}", backup_path); println!("Restoring backup from: {}", backup_path);
}); });
@ -273,23 +286,24 @@ pub async fn restore_backup(backup_path: String, create_pre_restore_backup: bool
} }
// Open the backup zip file // Open the backup zip file
let file = fs::File::open(&backup_path) let file =
.map_err(|e| format!("Failed to open backup file: {}", e))?; fs::File::open(&backup_path).map_err(|e| format!("Failed to open backup file: {}", e))?;
let mut archive = ZipArchive::new(file) let mut archive =
.map_err(|e| format!("Failed to read backup file: {}", e))?; ZipArchive::new(file).map_err(|e| format!("Failed to read backup file: {}", e))?;
// Extract files // Extract files
for i in 0..archive.len() { for i in 0..archive.len() {
let mut file = archive.by_index(i) let mut file = archive
.by_index(i)
.map_err(|e| format!("Failed to read file from backup: {}", e))?; .map_err(|e| format!("Failed to read file from backup: {}", e))?;
let outpath = data_dir.join(file.name()); let sanitized_name = file.name().replace("..", "");
let outpath = data_dir.join(sanitized_name);
if file.name().ends_with('/') { if file.name().ends_with('/') {
// Directory // Directory
fs::create_dir_all(&outpath) fs::create_dir_all(&outpath).map_err(|e| format!("Failed to create directory: {}", e))?;
.map_err(|e| format!("Failed to create directory: {}", e))?;
} else { } else {
// File // File
if let Some(parent) = outpath.parent() { if let Some(parent) = outpath.parent() {
@ -297,8 +311,8 @@ pub async fn restore_backup(backup_path: String, create_pre_restore_backup: bool
.map_err(|e| format!("Failed to create parent directory: {}", e))?; .map_err(|e| format!("Failed to create parent directory: {}", e))?;
} }
let mut outfile = fs::File::create(&outpath) let mut outfile =
.map_err(|e| format!("Failed to create file: {}", e))?; fs::File::create(&outpath).map_err(|e| format!("Failed to create file: {}", e))?;
std::io::copy(&mut file, &mut outfile) std::io::copy(&mut file, &mut outfile)
.map_err(|e| format!("Failed to extract file: {}", e))?; .map_err(|e| format!("Failed to extract file: {}", e))?;
@ -330,8 +344,7 @@ pub async fn delete_backup(backup_path: String) -> Result<String, String> {
return Err("Invalid file path".to_string()); return Err("Invalid file path".to_string());
} }
fs::remove_file(path) fs::remove_file(path).map_err(|e| format!("Failed to delete backup file: {}", e))?;
.map_err(|e| format!("Failed to delete backup file: {}", e))?;
debug_output(|| { debug_output(|| {
println!("Backup deleted successfully: {}", backup_path); println!("Backup deleted successfully: {}", backup_path);