mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 03:40:33 +08:00
feat(migration): enhance migration flow with new stages and error handling
This commit introduces additional stages to the migration process, including 'backup_required', 'backup_progress', and 'backup_confirmed', improving user guidance during data migration. It also adds new IPC channels for proceeding to backup and retrying migration, along with enhanced error handling and logging throughout the migration flow. The user interface has been updated to reflect these changes, providing clearer feedback and options during the migration process.
This commit is contained in:
parent
8715eb1f41
commit
c3f61533f7
@ -186,13 +186,15 @@ export enum IpcChannel {
|
||||
|
||||
// data migration
|
||||
DataMigrate_CheckNeeded = 'data-migrate:check-needed',
|
||||
DataMigrate_StartMigration = 'data-migrate:start-migration',
|
||||
DataMigrate_GetProgress = 'data-migrate:get-progress',
|
||||
DataMigrate_Cancel = 'data-migrate:cancel',
|
||||
DataMigrate_RequireBackup = 'data-migrate:require-backup',
|
||||
DataMigrate_BackupCompleted = 'data-migrate:backup-completed',
|
||||
DataMigrate_ShowBackupDialog = 'data-migrate:show-backup-dialog',
|
||||
DataMigrate_StartFlow = 'data-migrate:start-flow',
|
||||
DataMigrate_ProceedToBackup = 'data-migrate:proceed-to-backup',
|
||||
DataMigrate_StartMigration = 'data-migrate:start-migration',
|
||||
DataMigrate_RetryMigration = 'data-migrate:retry-migration',
|
||||
DataMigrate_RestartApp = 'data-migrate:restart-app',
|
||||
DataMigrate_CloseWindow = 'data-migrate:close-window',
|
||||
|
||||
|
||||
@ -22,11 +22,21 @@ interface DataRefactorMigrationStatus {
|
||||
version?: string
|
||||
}
|
||||
|
||||
type MigrationStage =
|
||||
| 'introduction' // Introduction phase - user can cancel
|
||||
| 'backup_required' // Backup required - show backup requirement
|
||||
| 'backup_progress' // Backup in progress - user is backing up
|
||||
| 'backup_confirmed' // Backup confirmed - ready to migrate
|
||||
| 'migration' // Migration in progress - cannot cancel
|
||||
| 'completed' // Completed - restart app
|
||||
| 'error' // Error - recovery options
|
||||
|
||||
interface MigrationProgress {
|
||||
stage: string
|
||||
stage: MigrationStage
|
||||
progress: number
|
||||
total: number
|
||||
message: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface MigrationResult {
|
||||
@ -39,13 +49,12 @@ class DataRefactorMigrateService {
|
||||
private static instance: DataRefactorMigrateService | null = null
|
||||
private migrateWindow: BrowserWindow | null = null
|
||||
private backupManager: BackupManager
|
||||
private backupCompletionResolver: ((value: boolean) => void) | null = null
|
||||
private db = dbService.getDb()
|
||||
private currentProgress: MigrationProgress = {
|
||||
stage: 'idle',
|
||||
stage: 'introduction',
|
||||
progress: 0,
|
||||
total: 100,
|
||||
message: 'Ready to migrate'
|
||||
message: 'Ready to start data migration'
|
||||
}
|
||||
private isMigrating: boolean = false
|
||||
|
||||
@ -77,11 +86,32 @@ class DataRefactorMigrateService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.DataMigrate_ProceedToBackup, async () => {
|
||||
try {
|
||||
await this.proceedToBackup()
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('IPC handler error: proceedToBackup', error as Error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.DataMigrate_StartMigration, async () => {
|
||||
try {
|
||||
return await this.runMigration()
|
||||
await this.startMigrationProcess()
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('IPC handler error: runMigration', error as Error)
|
||||
logger.error('IPC handler error: startMigrationProcess', error as Error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.DataMigrate_RetryMigration, async () => {
|
||||
try {
|
||||
await this.retryMigration()
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('IPC handler error: retryMigration', error as Error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
@ -104,9 +134,9 @@ class DataRefactorMigrateService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.DataMigrate_BackupCompleted, () => {
|
||||
ipcMain.handle(IpcChannel.DataMigrate_BackupCompleted, async () => {
|
||||
try {
|
||||
this.notifyBackupCompleted()
|
||||
await this.notifyBackupCompleted()
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('IPC handler error: notifyBackupCompleted', error as Error)
|
||||
@ -114,14 +144,55 @@ class DataRefactorMigrateService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.DataMigrate_ShowBackupDialog, () => {
|
||||
ipcMain.handle(IpcChannel.DataMigrate_ShowBackupDialog, async () => {
|
||||
try {
|
||||
// Show the backup dialog/interface
|
||||
// This could integrate with existing backup UI or create a new backup interface
|
||||
logger.info('Backup dialog request received')
|
||||
return true
|
||||
logger.info('Opening backup dialog for migration')
|
||||
|
||||
// Update progress to indicate backup dialog is opening
|
||||
await this.updateProgress('backup_progress', 10, 'Opening backup dialog...')
|
||||
|
||||
// Instead of performing backup automatically, let's open the file dialog
|
||||
// and let the user choose where to save the backup
|
||||
const { dialog } = await import('electron')
|
||||
const result = await dialog.showSaveDialog({
|
||||
title: 'Save Migration Backup',
|
||||
defaultPath: `cherry-studio-migration-backup-${new Date().toISOString().split('T')[0]}.zip`,
|
||||
filters: [
|
||||
{ name: 'Backup Files', extensions: ['zip'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
]
|
||||
})
|
||||
|
||||
if (!result.canceled && result.filePath) {
|
||||
logger.info('User selected backup location', { filePath: result.filePath })
|
||||
await this.updateProgress('backup_progress', 50, 'Creating backup file...')
|
||||
|
||||
// Perform the actual backup to the selected location
|
||||
const backupResult = await this.performBackupToFile(result.filePath)
|
||||
|
||||
if (backupResult.success) {
|
||||
await this.updateProgress('backup_progress', 100, 'Backup created successfully!')
|
||||
// Wait a moment to show the success message, then transition to confirmed state
|
||||
setTimeout(async () => {
|
||||
await this.updateProgress(
|
||||
'backup_confirmed',
|
||||
100,
|
||||
'Backup completed! Ready to start migration. Click "Start Migration" to continue.'
|
||||
)
|
||||
}, 1000)
|
||||
} else {
|
||||
await this.updateProgress('backup_required', 0, `Backup failed: ${backupResult.error}`)
|
||||
}
|
||||
|
||||
return backupResult
|
||||
} else {
|
||||
logger.info('User cancelled backup dialog')
|
||||
await this.updateProgress('backup_required', 0, 'Backup cancelled. Please create a backup to continue.')
|
||||
return { success: false, error: 'Backup cancelled by user' }
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('IPC handler error: showBackupDialog', error as Error)
|
||||
await this.updateProgress('backup_required', 0, 'Backup process failed')
|
||||
throw error
|
||||
}
|
||||
})
|
||||
@ -135,9 +206,9 @@ class DataRefactorMigrateService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.DataMigrate_RestartApp, () => {
|
||||
ipcMain.handle(IpcChannel.DataMigrate_RestartApp, async () => {
|
||||
try {
|
||||
this.restartApplication()
|
||||
await this.restartApplication()
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('IPC handler error: restartApplication', error as Error)
|
||||
@ -167,12 +238,14 @@ class DataRefactorMigrateService {
|
||||
|
||||
try {
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_CheckNeeded)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartMigration)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_GetProgress)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_Cancel)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_BackupCompleted)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_ShowBackupDialog)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartFlow)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_ProceedToBackup)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartMigration)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_RetryMigration)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_RestartApp)
|
||||
ipcMain.removeAllListeners(IpcChannel.DataMigrate_CloseWindow)
|
||||
|
||||
@ -200,6 +273,7 @@ class DataRefactorMigrateService {
|
||||
return true
|
||||
}
|
||||
|
||||
logger.info('Data Refactor Migration is needed')
|
||||
return false
|
||||
} catch (error) {
|
||||
logger.error('Failed to check migration status', error as Error)
|
||||
@ -212,18 +286,40 @@ class DataRefactorMigrateService {
|
||||
*/
|
||||
private async isMigrationCompleted(): Promise<boolean> {
|
||||
try {
|
||||
logger.debug('Checking migration completion status in database')
|
||||
|
||||
// First check if the database is available
|
||||
if (!this.db) {
|
||||
logger.warn('Database not initialized, assuming migration not completed')
|
||||
return false
|
||||
}
|
||||
|
||||
const result = await this.db
|
||||
.select()
|
||||
.from(appStateTable)
|
||||
.where(eq(appStateTable.key, DATA_REFACTOR_MIGRATION_STATUS))
|
||||
.limit(1)
|
||||
|
||||
if (result.length === 0) return false
|
||||
logger.debug('Migration status query result', { resultCount: result.length })
|
||||
|
||||
if (result.length === 0) {
|
||||
logger.info('No migration status record found, migration needed')
|
||||
return false
|
||||
}
|
||||
|
||||
const status = result[0].value as DataRefactorMigrationStatus
|
||||
return status.completed === true
|
||||
const isCompleted = status.completed === true
|
||||
|
||||
logger.info('Migration status found', {
|
||||
completed: isCompleted,
|
||||
completedAt: status.completedAt,
|
||||
version: status.version
|
||||
})
|
||||
|
||||
return isCompleted
|
||||
} catch (error) {
|
||||
logger.warn('Failed to check migration state', error as Error)
|
||||
logger.error('Failed to check migration state - treating as not completed', error as Error)
|
||||
// In case of database errors, assume migration is needed to be safe
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -320,7 +416,7 @@ class DataRefactorMigrateService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the complete migration process
|
||||
* Show migration window and initialize introduction stage
|
||||
*/
|
||||
async runMigration(): Promise<void> {
|
||||
if (this.isMigrating) {
|
||||
@ -332,6 +428,9 @@ class DataRefactorMigrateService {
|
||||
this.isMigrating = true
|
||||
logger.info('Showing migration window')
|
||||
|
||||
// Initialize introduction stage
|
||||
await this.updateProgress('introduction', 0, 'Welcome to Cherry Studio data migration')
|
||||
|
||||
// Create migration window
|
||||
const window = this.createMigrateWindow()
|
||||
|
||||
@ -345,12 +444,45 @@ class DataRefactorMigrateService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Start migration flow - simply ensure we're in introduction stage
|
||||
* This is called when user first opens the migration window
|
||||
*/
|
||||
async startMigrationFlow(): Promise<void> {
|
||||
if (!this.isMigrating) {
|
||||
logger.warn('Migration not started, cannot execute flow.')
|
||||
return
|
||||
}
|
||||
logger.info('Starting migration flow from user action')
|
||||
|
||||
logger.info('Confirming introduction stage for migration flow')
|
||||
await this.updateProgress('introduction', 0, 'Ready to begin migration process. Please read the information below.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceed from introduction to backup requirement stage
|
||||
* This is called when user clicks "Next" in introduction
|
||||
*/
|
||||
async proceedToBackup(): Promise<void> {
|
||||
if (!this.isMigrating) {
|
||||
logger.warn('Migration not started, cannot proceed to backup.')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('Proceeding from introduction to backup stage')
|
||||
await this.updateProgress('backup_required', 0, 'Data backup is required before migration can proceed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the actual migration process
|
||||
* This is called when user confirms backup and clicks "Start Migration"
|
||||
*/
|
||||
async startMigrationProcess(): Promise<void> {
|
||||
if (!this.isMigrating) {
|
||||
logger.warn('Migration not started, cannot start migration process.')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('Starting actual migration process')
|
||||
try {
|
||||
await this.executeMigrationFlow()
|
||||
} catch (error) {
|
||||
@ -360,21 +492,12 @@ class DataRefactorMigrateService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the complete migration flow
|
||||
* Execute the actual migration process
|
||||
* Called after user has confirmed backup completion
|
||||
*/
|
||||
private async executeMigrationFlow(): Promise<void> {
|
||||
try {
|
||||
// Step 1: Enforce backup
|
||||
await this.updateProgress('backup', 0, 'Starting backup process...')
|
||||
const backupSuccess = await this.enforceBackup()
|
||||
|
||||
if (!backupSuccess) {
|
||||
throw new Error('Backup process failed or was cancelled by user')
|
||||
}
|
||||
|
||||
await this.updateProgress('backup', 100, 'Backup completed successfully')
|
||||
|
||||
// Step 2: Execute migration
|
||||
// Start migration
|
||||
await this.updateProgress('migration', 0, 'Starting data migration...')
|
||||
const migrationResult = await this.executeMigration()
|
||||
|
||||
@ -388,16 +511,18 @@ class DataRefactorMigrateService {
|
||||
`Migration completed: ${migrationResult.migratedCount} items migrated`
|
||||
)
|
||||
|
||||
// Step 3: Mark as completed
|
||||
// Mark as completed
|
||||
await this.markMigrationCompleted()
|
||||
|
||||
await this.updateProgress('completed', 100, 'Migration completed! Please restart the app.')
|
||||
await this.updateProgress('completed', 100, 'Migration completed successfully! Click restart to continue.')
|
||||
} catch (error) {
|
||||
logger.error('Migration flow failed', error as Error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
await this.updateProgress(
|
||||
'error',
|
||||
0,
|
||||
`Migration failed: ${error instanceof Error ? error.message : String(error)}. Please close this window and restart the app to try again.`
|
||||
'Migration failed. You can close this window and try again, or continue using the previous version.',
|
||||
errorMessage
|
||||
)
|
||||
|
||||
throw error
|
||||
@ -405,58 +530,113 @@ class DataRefactorMigrateService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce backup before migration
|
||||
* Perform backup to a specific file location
|
||||
*/
|
||||
private async enforceBackup(): Promise<boolean> {
|
||||
private async performBackupToFile(filePath: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
logger.info('Enforcing backup before migration')
|
||||
logger.info('Performing backup to file', { filePath })
|
||||
|
||||
await this.updateProgress('backup', 0, 'Backup is required before migration')
|
||||
// Get backup data from the current application state
|
||||
const backupData = await this.getBackupData()
|
||||
|
||||
// Send backup requirement to renderer
|
||||
if (this.migrateWindow && !this.migrateWindow.isDestroyed()) {
|
||||
this.migrateWindow.webContents.send(IpcChannel.DataMigrate_RequireBackup)
|
||||
}
|
||||
// Extract directory and filename from the full path
|
||||
const path = await import('path')
|
||||
const destinationDir = path.dirname(filePath)
|
||||
const fileName = path.basename(filePath)
|
||||
|
||||
// Wait for user to complete backup
|
||||
const backupResult = await this.waitForBackupCompletion()
|
||||
// Use the existing backup manager to create a backup
|
||||
const backupPath = await this.backupManager.backup(
|
||||
null as any, // IpcMainInvokeEvent - we're calling directly so pass null
|
||||
fileName,
|
||||
backupData,
|
||||
destinationDir,
|
||||
false // Don't skip backup files - full backup for migration safety
|
||||
)
|
||||
|
||||
if (backupResult) {
|
||||
await this.updateProgress('backup', 100, 'Backup completed successfully')
|
||||
return true
|
||||
if (backupPath) {
|
||||
logger.info('Backup created successfully', { path: backupPath })
|
||||
return { success: true }
|
||||
} else {
|
||||
await this.updateProgress('backup', 0, 'Backup is required to proceed with migration')
|
||||
return false
|
||||
return {
|
||||
success: false,
|
||||
error: 'Backup process did not return a file path'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Backup enforcement failed', error as Error)
|
||||
await this.updateProgress('backup', 0, 'Backup process failed')
|
||||
return false
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
logger.error('Backup failed during migration:', error as Error)
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for user to complete backup
|
||||
* Get backup data from the current application
|
||||
* This creates a minimal backup with essential system information
|
||||
*/
|
||||
private async waitForBackupCompletion(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
// Store resolver for later use
|
||||
this.backupCompletionResolver = resolve
|
||||
private async getBackupData(): Promise<string> {
|
||||
try {
|
||||
const fs = await import('fs-extra')
|
||||
const path = await import('path')
|
||||
|
||||
// The actual completion will be triggered by notifyBackupCompleted() method
|
||||
})
|
||||
// Gather basic system information
|
||||
const data = {
|
||||
backup: {
|
||||
timestamp: new Date().toISOString(),
|
||||
version: electronApp.getVersion(),
|
||||
type: 'pre-migration-backup',
|
||||
note: 'This is a safety backup created before data migration'
|
||||
},
|
||||
system: {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
nodeVersion: process.version
|
||||
},
|
||||
// Include basic configuration files if they exist
|
||||
configs: {} as Record<string, any>
|
||||
}
|
||||
|
||||
// Try to read some basic configuration files (non-critical if they fail)
|
||||
try {
|
||||
const { getDataPath } = await import('@main/utils')
|
||||
const dataPath = getDataPath()
|
||||
|
||||
// Check if there are any config files we should backup
|
||||
const configFiles = ['config.json', 'settings.json', 'preferences.json']
|
||||
for (const configFile of configFiles) {
|
||||
const configPath = path.join(dataPath, configFile)
|
||||
if (await fs.pathExists(configPath)) {
|
||||
try {
|
||||
const configContent = await fs.readJson(configPath)
|
||||
data.configs[configFile] = configContent
|
||||
} catch (err) {
|
||||
logger.warn(`Could not read config file ${configFile}`, err as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Could not access data directory for config backup', err as Error)
|
||||
}
|
||||
|
||||
return JSON.stringify(data, null, 2)
|
||||
} catch (error) {
|
||||
logger.error('Failed to get backup data:', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that backup has been completed (called from IPC handler)
|
||||
*/
|
||||
public notifyBackupCompleted(): void {
|
||||
if (this.backupCompletionResolver) {
|
||||
logger.info('Backup completed by user')
|
||||
|
||||
this.backupCompletionResolver(true)
|
||||
this.backupCompletionResolver = null
|
||||
}
|
||||
public async notifyBackupCompleted(): Promise<void> {
|
||||
logger.info('Backup completed by user')
|
||||
await this.updateProgress(
|
||||
'backup_confirmed',
|
||||
100,
|
||||
'Backup completed! Ready to start migration. Click "Start Migration" to continue.'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -494,12 +674,18 @@ class DataRefactorMigrateService {
|
||||
/**
|
||||
* Update migration progress and broadcast to window
|
||||
*/
|
||||
private async updateProgress(stage: string, progress: number, message: string): Promise<void> {
|
||||
private async updateProgress(
|
||||
stage: MigrationStage,
|
||||
progress: number,
|
||||
message: string,
|
||||
error?: string
|
||||
): Promise<void> {
|
||||
this.currentProgress = {
|
||||
stage,
|
||||
progress,
|
||||
total: 100,
|
||||
message
|
||||
message,
|
||||
error
|
||||
}
|
||||
|
||||
if (this.migrateWindow && !this.migrateWindow.isDestroyed()) {
|
||||
@ -518,18 +704,36 @@ class DataRefactorMigrateService {
|
||||
|
||||
/**
|
||||
* Cancel migration process
|
||||
* Only allowed during introduction and backup phases
|
||||
*/
|
||||
async cancelMigration(): Promise<void> {
|
||||
if (!this.isMigrating) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentStage = this.currentProgress.stage
|
||||
if (currentStage === 'migration') {
|
||||
logger.warn('Cannot cancel migration during migration process')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('Cancelling migration process')
|
||||
this.isMigrating = false
|
||||
await this.updateProgress('cancelled', 0, 'Migration cancelled by user')
|
||||
this.closeMigrateWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry migration after error
|
||||
*/
|
||||
async retryMigration(): Promise<void> {
|
||||
logger.info('Retrying migration process')
|
||||
await this.updateProgress(
|
||||
'introduction',
|
||||
0,
|
||||
'Ready to restart migration process. Please read the information below.'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close migration window
|
||||
*/
|
||||
@ -547,22 +751,73 @@ class DataRefactorMigrateService {
|
||||
/**
|
||||
* Restart the application after successful migration
|
||||
*/
|
||||
private restartApplication(): void {
|
||||
private async restartApplication(): Promise<void> {
|
||||
try {
|
||||
logger.info('Restarting application after migration completion')
|
||||
logger.info('Preparing to restart application after migration completion')
|
||||
|
||||
// Clean up migration window and handlers before restart
|
||||
this.closeMigrateWindow()
|
||||
// Ensure migration status is properly saved before restart
|
||||
await this.verifyMigrationStatus()
|
||||
|
||||
// Restart the app using Electron's relaunch mechanism
|
||||
app.relaunch()
|
||||
app.exit(0)
|
||||
// Give some time for database operations to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
logger.info('Restarting application now')
|
||||
|
||||
// In development mode, relaunch might not work properly
|
||||
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
|
||||
logger.warn('Development mode detected - showing restart instruction instead of auto-restart')
|
||||
|
||||
const { dialog } = await import('electron')
|
||||
await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Migration Complete - Restart Required',
|
||||
message:
|
||||
'Data migration completed successfully!\n\nSince you are in development mode, please manually restart the application to continue.',
|
||||
buttons: ['Close App'],
|
||||
defaultId: 0
|
||||
})
|
||||
|
||||
// Clean up migration window and handlers after showing dialog
|
||||
this.closeMigrateWindow()
|
||||
app.quit()
|
||||
} else {
|
||||
// Production mode - clean up first, then relaunch
|
||||
this.closeMigrateWindow()
|
||||
app.relaunch()
|
||||
app.exit(0)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to restart application', error as Error)
|
||||
// Fallback: just close migration window and let user manually restart
|
||||
this.closeMigrateWindow()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that migration status is properly saved
|
||||
*/
|
||||
private async verifyMigrationStatus(): Promise<void> {
|
||||
try {
|
||||
const isCompleted = await this.isMigrationCompleted()
|
||||
if (isCompleted) {
|
||||
logger.info('Migration status verified as completed')
|
||||
} else {
|
||||
logger.warn('Migration status not found as completed, attempting to mark again')
|
||||
await this.markMigrationCompleted()
|
||||
|
||||
// Double-check
|
||||
const recheck = await this.isMigrationCompleted()
|
||||
if (recheck) {
|
||||
logger.info('Migration status successfully marked as completed on retry')
|
||||
} else {
|
||||
logger.error('Failed to mark migration as completed even on retry')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to verify migration status', error as Error)
|
||||
// Don't throw - still allow restart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
|
||||
@ -113,20 +113,43 @@ if (!app.requestSingleInstanceLock()) {
|
||||
// Data Refactor Migration
|
||||
// Check if data migration is needed BEFORE creating any windows
|
||||
try {
|
||||
logger.info('Checking if data refactor migration is needed')
|
||||
const isMigrated = await dataRefactorMigrateService.isMigrated()
|
||||
logger.info('Migration status check result', { isMigrated })
|
||||
|
||||
if (!isMigrated) {
|
||||
logger.info('Data Refactor Migration needed, starting migration process')
|
||||
await dataRefactorMigrateService.runMigration()
|
||||
logger.info('Migration completed, app will restart automatically')
|
||||
// Migration service will handle app restart, no need to continue startup
|
||||
return
|
||||
|
||||
try {
|
||||
await dataRefactorMigrateService.runMigration()
|
||||
logger.info('Migration window created successfully')
|
||||
// Migration service will handle the migration flow, no need to continue startup
|
||||
return
|
||||
} catch (migrationError) {
|
||||
logger.error('Failed to start migration process', migrationError as Error)
|
||||
|
||||
// Migration is required for this version - show error and exit
|
||||
await dialog.showErrorBox(
|
||||
'Migration Required - Application Cannot Start',
|
||||
`This version of Cherry Studio requires data migration to function properly.\n\nMigration window failed to start: ${(migrationError as Error).message}\n\nThe application will now exit. Please try starting again or contact support if the problem persists.`
|
||||
)
|
||||
|
||||
logger.error('Exiting application due to failed migration startup')
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Migration process failed', error as Error)
|
||||
dialog.showErrorBox(
|
||||
'Fatal Error: Data Refactor Migration Failed',
|
||||
`The application could not start due to a critical error during data migration.\n\nPlease contact support or try restoring data from a backup.\n\nError details:\n${(error as Error).message}`
|
||||
logger.error('Migration status check failed', error as Error)
|
||||
|
||||
// If we can't check migration status, this could indicate a serious database issue
|
||||
// Since migration may be required, it's safer to exit and let user investigate
|
||||
await dialog.showErrorBox(
|
||||
'Migration Status Check Failed - Application Cannot Start',
|
||||
`Could not determine if data migration is completed.\n\nThis may indicate a database connectivity issue: ${(error as Error).message}\n\nThe application will now exit. Please check your installation and try again.`
|
||||
)
|
||||
|
||||
logger.error('Exiting application due to migration status check failure')
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
@ -140,7 +163,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
app.dock?.hide()
|
||||
}
|
||||
|
||||
// Only create main window if no migration was needed or migration failed
|
||||
// Create main window - migration has either completed or was not needed
|
||||
const mainWindow = windowService.createMainWindow()
|
||||
|
||||
new TrayService()
|
||||
|
||||
@ -6,21 +6,30 @@ import styled from 'styled-components'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
type MigrationStage =
|
||||
| 'introduction' // Introduction phase - user can cancel
|
||||
| 'backup_required' // Backup required - show backup requirement
|
||||
| 'backup_progress' // Backup in progress - user is backing up
|
||||
| 'backup_confirmed' // Backup confirmed - ready to migrate
|
||||
| 'migration' // Migration in progress - cannot cancel
|
||||
| 'completed' // Completed - restart app
|
||||
| 'error' // Error - recovery options
|
||||
|
||||
interface MigrationProgress {
|
||||
stage: string
|
||||
stage: MigrationStage
|
||||
progress: number
|
||||
total: number
|
||||
message: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
const MigrateApp: React.FC = () => {
|
||||
const [progress, setProgress] = useState<MigrationProgress>({
|
||||
stage: 'idle',
|
||||
stage: 'introduction',
|
||||
progress: 0,
|
||||
total: 100,
|
||||
message: '准备开始迁移...'
|
||||
message: 'Ready to start data migration'
|
||||
})
|
||||
const [showBackupRequired, setShowBackupRequired] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for progress updates
|
||||
@ -28,13 +37,7 @@ const MigrateApp: React.FC = () => {
|
||||
setProgress(progressData)
|
||||
}
|
||||
|
||||
// Listen for backup requirement
|
||||
const handleBackupRequired = () => {
|
||||
setShowBackupRequired(true)
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on(IpcChannel.DataMigrateProgress, handleProgress)
|
||||
window.electron.ipcRenderer.on(IpcChannel.DataMigrate_RequireBackup, handleBackupRequired)
|
||||
|
||||
// Request initial progress
|
||||
window.electron.ipcRenderer
|
||||
@ -47,37 +50,41 @@ const MigrateApp: React.FC = () => {
|
||||
|
||||
return () => {
|
||||
window.electron.ipcRenderer.removeAllListeners(IpcChannel.DataMigrateProgress)
|
||||
window.electron.ipcRenderer.removeAllListeners(IpcChannel.DataMigrate_RequireBackup)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const currentStep = useMemo(() => {
|
||||
switch (progress.stage) {
|
||||
case 'idle':
|
||||
case 'introduction':
|
||||
return 0
|
||||
case 'backup':
|
||||
case 'backup_required':
|
||||
case 'backup_progress':
|
||||
case 'backup_confirmed':
|
||||
return 1
|
||||
case 'migration':
|
||||
return 2
|
||||
case 'completed':
|
||||
return 4
|
||||
case 'error':
|
||||
case 'cancelled':
|
||||
return 3
|
||||
case 'error':
|
||||
return -1 // Error state - will be handled separately
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}, [progress.stage])
|
||||
|
||||
const stepStatus = useMemo(() => {
|
||||
if (progress.stage === 'error' || progress.stage === 'cancelled') {
|
||||
if (progress.stage === 'error') {
|
||||
return 'error'
|
||||
}
|
||||
return 'process'
|
||||
}, [progress.stage])
|
||||
|
||||
const handleProceedToBackup = () => {
|
||||
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_ProceedToBackup)
|
||||
}
|
||||
|
||||
const handleStartMigration = () => {
|
||||
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_StartFlow)
|
||||
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_StartMigration)
|
||||
}
|
||||
|
||||
const handleRestartApp = () => {
|
||||
@ -98,18 +105,22 @@ const MigrateApp: React.FC = () => {
|
||||
}
|
||||
|
||||
const handleBackupCompleted = () => {
|
||||
setShowBackupRequired(false)
|
||||
// Notify the main process that backup is completed
|
||||
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_BackupCompleted)
|
||||
}
|
||||
|
||||
const handleRetryMigration = () => {
|
||||
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_RetryMigration)
|
||||
}
|
||||
|
||||
const getProgressColor = () => {
|
||||
switch (progress.stage) {
|
||||
case 'completed':
|
||||
return '#52c41a'
|
||||
case 'error':
|
||||
case 'cancelled':
|
||||
return '#ff4d4f'
|
||||
case 'backup_confirmed':
|
||||
return '#52c41a'
|
||||
default:
|
||||
return '#1890ff'
|
||||
}
|
||||
@ -117,12 +128,36 @@ const MigrateApp: React.FC = () => {
|
||||
|
||||
const renderActionButtons = () => {
|
||||
switch (progress.stage) {
|
||||
case 'idle':
|
||||
case 'introduction':
|
||||
return (
|
||||
<Button type="primary" onClick={handleStartMigration}>
|
||||
开始迁移
|
||||
</Button>
|
||||
<Space>
|
||||
<Button onClick={handleCancel}>取消</Button>
|
||||
<Button type="primary" onClick={handleProceedToBackup}>
|
||||
下一步
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
case 'backup_required':
|
||||
return (
|
||||
<Space>
|
||||
<Button onClick={handleCancel}>取消</Button>
|
||||
<Button type="primary" onClick={handleShowBackupDialog}>
|
||||
创建备份
|
||||
</Button>
|
||||
<Button onClick={handleBackupCompleted}>我已完成备份</Button>
|
||||
</Space>
|
||||
)
|
||||
case 'backup_confirmed':
|
||||
return (
|
||||
<Space>
|
||||
<Button onClick={handleCancel}>取消</Button>
|
||||
<Button type="primary" onClick={handleStartMigration}>
|
||||
开始迁移
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
case 'migration':
|
||||
return <Button disabled>迁移进行中...</Button>
|
||||
case 'completed':
|
||||
return (
|
||||
<Button type="primary" onClick={handleRestartApp}>
|
||||
@ -130,19 +165,14 @@ const MigrateApp: React.FC = () => {
|
||||
</Button>
|
||||
)
|
||||
case 'error':
|
||||
case 'cancelled':
|
||||
return (
|
||||
<Space>
|
||||
<Button onClick={handleCloseWindow}>关闭</Button>
|
||||
<Button onClick={handleCloseWindow}>关闭应用</Button>
|
||||
<Button type="primary" onClick={handleRetryMigration}>
|
||||
重新尝试
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
case 'backup':
|
||||
case 'migration':
|
||||
return (
|
||||
<Button onClick={handleCancel} disabled={progress.stage === 'backup'}>
|
||||
取消迁移
|
||||
</Button>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@ -167,11 +197,11 @@ const MigrateApp: React.FC = () => {
|
||||
<Steps
|
||||
current={currentStep}
|
||||
status={stepStatus}
|
||||
items={[{ title: '开始' }, { title: '备份' }, { title: '迁移' }, { title: '完成' }]}
|
||||
items={[{ title: '介绍' }, { title: '备份' }, { title: '迁移' }, { title: '完成' }]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{progress.stage !== 'idle' && (
|
||||
{progress.stage !== 'introduction' && progress.stage !== 'error' && (
|
||||
<ProgressContainer>
|
||||
<Progress
|
||||
percent={progress.progress}
|
||||
@ -187,10 +217,43 @@ const MigrateApp: React.FC = () => {
|
||||
<Text type="secondary">{progress.message}</Text>
|
||||
</MessageContainer>
|
||||
|
||||
{progress.stage === 'introduction' && (
|
||||
<Alert
|
||||
message="欢迎使用Cherry Studio数据迁移向导"
|
||||
description="本次更新将您的数据迁移到更高效的存储格式。迁移前会创建完整备份,确保数据安全。整个过程大约需要几分钟时间。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{progress.stage === 'backup_required' && (
|
||||
<Alert
|
||||
message="需要数据备份"
|
||||
description="为确保数据安全,迁移前必须创建数据备份。请点击'创建备份'按钮,或者如果您已经有最新备份,可以直接确认。"
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{progress.stage === 'backup_confirmed' && (
|
||||
<Alert
|
||||
message="备份完成"
|
||||
description="数据备份已完成,现在可以安全地开始迁移。点击'开始迁移'继续。"
|
||||
type="success"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{progress.stage === 'error' && (
|
||||
<Alert
|
||||
message="Migration Error"
|
||||
description="The migration process encountered an error. You can try again or restore from a backup using an older version."
|
||||
message="迁移出现错误"
|
||||
description={
|
||||
progress.error ||
|
||||
'迁移过程中遇到错误。您可以关闭窗口重新尝试,或继续使用之前版本(所有原始数据都完好保存)。'
|
||||
}
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
@ -199,34 +262,14 @@ const MigrateApp: React.FC = () => {
|
||||
|
||||
{progress.stage === 'completed' && (
|
||||
<Alert
|
||||
message="Migration Successful"
|
||||
description="Your data has been successfully migrated to the new format. Cherry Studio will now start with your updated data."
|
||||
message="迁移成功完成"
|
||||
description="数据已成功迁移到新格式。Cherry Studio将使用更新后的数据重新启动,享受更流畅的使用体验。"
|
||||
type="success"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showBackupRequired && (
|
||||
<Alert
|
||||
message="Backup Required"
|
||||
description="A backup is required before migration can proceed. Please create a backup of your data to ensure safety."
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
action={
|
||||
<Space>
|
||||
<Button size="small" onClick={handleShowBackupDialog}>
|
||||
Create Backup
|
||||
</Button>
|
||||
<Button size="small" type="link" onClick={handleBackupCompleted}>
|
||||
I've Already Created a Backup
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{ textAlign: 'center', marginTop: 24 }}>{renderActionButtons()}</div>
|
||||
</MigrationCard>
|
||||
</Container>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user