refactor(migration): rename data migration files and update migration logic

This commit renames the data migration files for clarity, changing `dataMigrate.html` to `dataRefactorMigrate.html` and updating the corresponding service imports. It also enhances the migration logic by implementing a new `app_state` table structure and removing deprecated migration files, streamlining the overall migration process.
This commit is contained in:
fullex 2025-08-09 22:37:20 +08:00
parent ff965402cd
commit 92eb5aed7f
17 changed files with 138 additions and 246 deletions

View File

@ -103,7 +103,7 @@ export default defineConfig({
selectionToolbar: resolve(__dirname, 'src/renderer/selectionToolbar.html'),
selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html'),
traceWindow: resolve(__dirname, 'src/renderer/traceWindow.html'),
dataMigrate: resolve(__dirname, 'src/renderer/dataMigrate.html')
dataRefactorMigrate: resolve(__dirname, 'src/renderer/dataRefactorMigrate.html')
}
}
},

View File

@ -1,7 +1,6 @@
**THIS IS NOT FOR RUNTIME USE**
**THIS DIRECTORY IS NOT FOR RUNTIME USE**
Using `libsql` as the `sqlite3` driver, and `drizzle` as the ORM and database migration tool
`migrations/sqlite-drizzle` contains auto-generated migration data. Please **DO NOT** modify it.
To generate migrations, use the command `yarn run migrations:generate`
- Using `libsql` as the `sqlite3` driver, and `drizzle` as the ORM and database migration tool
- `migrations/sqlite-drizzle` contains auto-generated migration data. Please **DO NOT** modify it.
- If table structure changes, we should run migrations.
- To generate migrations, use the command `yarn run migrations:generate`

View File

@ -1,10 +1,17 @@
CREATE TABLE `app_state` (
`key` text PRIMARY KEY NOT NULL,
`value` text NOT NULL,
`description` text,
`created_at` integer,
`updated_at` integer
);
--> statement-breakpoint
CREATE TABLE `preference` (
`scope` text NOT NULL,
`key` text NOT NULL,
`value` text,
`created_at` integer,
`updated_at` integer,
`deleted_at` integer
`updated_at` integer
);
--> statement-breakpoint
CREATE INDEX `scope_name_idx` ON `preference` (`scope`,`key`);

View File

@ -1,9 +1,54 @@
{
"version": "6",
"dialect": "sqlite",
"id": "c8c54066-c6f6-404b-a46d-84787ae22485",
"id": "de8009d7-95b9-4f99-99fa-4b8795708f21",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"app_state": {
"name": "app_state",
"columns": {
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"preference": {
"name": "preference",
"columns": {
@ -41,13 +86,6 @@
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"deleted_at": {
"name": "deleted_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {

View File

@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
"when": 1747914098702,
"tag": "0000_panoramic_morlun",
"when": 1754745234572,
"tag": "0000_solid_lord_hawal",
"breakpoints": true
}
]

View File

@ -1,28 +1,10 @@
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { crudTimestamps } from './columnHelpers'
import { createUpdateTimestamps } from './columnHelpers'
export const appStateTable = sqliteTable('app_state', {
key: text().primaryKey(),
value: text({ mode: 'json' }).notNull(), // JSON field, drizzle handles serialization automatically
description: text(), // Optional description field
...crudTimestamps
...createUpdateTimestamps
})
export type AppStateTable = typeof appStateTable
export type AppStateInsert = typeof appStateTable.$inferInsert
export type AppStateSelect = typeof appStateTable.$inferSelect
// State key constants
export const APP_STATE_KEYS = {
DATA_REFACTOR_MIGRATION_STATUS: 'data_refactor_migration_status',
// Future state keys can be added here
// FIRST_RUN_COMPLETED: 'first_run_completed',
// USER_ONBOARDING_COMPLETED: 'user_onboarding_completed',
} as const
// Data refactor migration status interface
export interface DataRefactorMigrationStatus {
completed: boolean
completedAt?: number
version?: string
}

View File

@ -4,7 +4,12 @@ const createTimestamp = () => {
return Date.now()
}
export const crudTimestamps = {
export const createUpdateTimestamps = {
createdAt: integer().$defaultFn(createTimestamp),
updatedAt: integer().$defaultFn(createTimestamp).$onUpdateFn(createTimestamp)
}
export const createUpdateDeleteTimestamps = {
createdAt: integer().$defaultFn(createTimestamp),
updatedAt: integer().$defaultFn(createTimestamp).$onUpdateFn(createTimestamp),
deletedAt: integer()

View File

@ -1,6 +1,6 @@
import { index, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { crudTimestamps } from './columnHelpers'
import { createUpdateTimestamps } from './columnHelpers'
export const preferenceTable = sqliteTable(
'preference',
@ -8,7 +8,7 @@ export const preferenceTable = sqliteTable(
scope: text().notNull(), // scope is reserved for future use, now only 'default' is supported
key: text().notNull(),
value: text({ mode: 'json' }),
...crudTimestamps
...createUpdateTimestamps
},
(t) => [index('scope_name_idx').on(t.scope, t.key)]
)

View File

@ -1,34 +1,42 @@
import dbService from '@data/db/DbService'
import { APP_STATE_KEYS, appStateTable, DataRefactorMigrationStatus } from '@data/db/schemas/appState'
import { appStateTable } from '@data/db/schemas/appState'
import { loggerService } from '@logger'
import { isDev } from '@main/constant'
import BackupManager from '@main/services/BackupManager'
import { IpcChannel } from '@shared/IpcChannel'
import { eq } from 'drizzle-orm'
import { app, BrowserWindow, ipcMain } from 'electron'
import { app as electronApp } from 'electron'
import fs from 'fs-extra'
import { join } from 'path'
import icon from '../../../../build/icon.png?asset'
import BackupManager from '../../services/BackupManager'
import { PreferencesMigrator } from './PreferencesMigrator'
import { PreferencesMigrator } from './migrators/PreferencesMigrator'
const logger = loggerService.withContext('MigrateService')
const logger = loggerService.withContext('DataRefactorMigrateService')
export interface MigrationProgress {
const DATA_REFACTOR_MIGRATION_STATUS = 'data_refactor_migration_status'
// Data refactor migration status interface
interface DataRefactorMigrationStatus {
completed: boolean
completedAt?: number
version?: string
}
interface MigrationProgress {
stage: string
progress: number
total: number
message: string
}
export interface MigrationResult {
interface MigrationResult {
success: boolean
error?: string
migratedCount: number
}
export class MigrateService {
private static instance: MigrateService | null = null
class DataRefactorMigrateService {
private static instance: DataRefactorMigrateService | null = null
private migrateWindow: BrowserWindow | null = null
private backupManager: BackupManager
private backupCompletionResolver: ((value: boolean) => void) | null = null
@ -63,7 +71,7 @@ export class MigrateService {
// Only register the minimal IPC handlers needed for migration
ipcMain.handle(IpcChannel.DataMigrate_CheckNeeded, async () => {
try {
return await this.checkMigrationNeeded()
return await this.isMigrated()
} catch (error) {
logger.error('IPC handler error: checkMigrationNeeded', error as Error)
throw error
@ -143,96 +151,31 @@ export class MigrateService {
}
}
public static getInstance(): MigrateService {
if (!MigrateService.instance) {
MigrateService.instance = new MigrateService()
public static getInstance(): DataRefactorMigrateService {
if (!DataRefactorMigrateService.instance) {
DataRefactorMigrateService.instance = new DataRefactorMigrateService()
}
return MigrateService.instance
return DataRefactorMigrateService.instance
}
/**
* Check if migration is needed
*/
async checkMigrationNeeded(): Promise<boolean> {
async isMigrated(): Promise<boolean> {
try {
logger.info('Checking if migration is needed')
// 1. Check migration completion status
const isMigrated = await this.isMigrationCompleted()
if (isMigrated) {
logger.info('Migration already completed')
return false
logger.info('Data Refactor Migration already completed')
return true
}
// 2. Check if there's old data that needs migration
const hasOldData = await this.hasOldFormatData()
logger.info('Migration check result', {
isMigrated,
hasOldData
})
return hasOldData
return false
} catch (error) {
logger.error('Failed to check migration status', error as Error)
return false
}
}
/**
* Check if old format data exists
*/
private async hasOldFormatData(): Promise<boolean> {
const hasReduxData = await this.checkReduxPersistData()
const hasElectronStoreData = await this.checkElectronStoreData()
logger.debug('Old format data check', {
hasReduxData,
hasElectronStoreData
})
return hasReduxData || hasElectronStoreData
}
/**
* Check if Redux persist data exists
*/
private async checkReduxPersistData(): Promise<boolean> {
try {
// In Electron, localStorage data is stored in userData/Local Storage/leveldb
// We'll check for the existence of these files as a proxy for Redux persist data
const userDataPath = app.getPath('userData')
const localStoragePath = join(userDataPath, 'Local Storage', 'leveldb')
const exists = await fs.pathExists(localStoragePath)
logger.debug('Redux persist data check', { localStoragePath, exists })
return exists
} catch (error) {
logger.warn('Failed to check Redux persist data', error as Error)
return false
}
}
/**
* Check if ElectronStore data exists
*/
private async checkElectronStoreData(): Promise<boolean> {
try {
// ElectronStore typically stores data in config files
const userDataPath = app.getPath('userData')
const configPath = join(userDataPath, 'config.json')
const exists = await fs.pathExists(configPath)
logger.debug('ElectronStore data check', { configPath, exists })
return exists
} catch (error) {
logger.warn('Failed to check ElectronStore data', error as Error)
return false
}
}
/**
* Check if migration is already completed
*/
@ -241,7 +184,7 @@ export class MigrateService {
const result = await this.db
.select()
.from(appStateTable)
.where(eq(appStateTable.key, APP_STATE_KEYS.DATA_REFACTOR_MIGRATION_STATUS))
.where(eq(appStateTable.key, DATA_REFACTOR_MIGRATION_STATUS))
.limit(1)
if (result.length === 0) return false
@ -268,7 +211,7 @@ export class MigrateService {
await this.db
.insert(appStateTable)
.values({
key: APP_STATE_KEYS.DATA_REFACTOR_MIGRATION_STATUS,
key: DATA_REFACTOR_MIGRATION_STATUS,
value: migrationStatus, // drizzle handles JSON serialization automatically
description: 'Data refactoring migration status from legacy format (ElectronStore + Redux persist) to SQLite',
createdAt: Date.now(),
@ -318,15 +261,14 @@ export class MigrateService {
sandbox: false,
webSecurity: false,
contextIsolation: true
},
...(process.platform === 'linux' ? { icon } : {})
}
})
// Load the migration window
if (app.isPackaged) {
this.migrateWindow.loadFile(join(__dirname, '../renderer/dataMigrate.html'))
if (isDev && process.env['ELECTRON_RENDERER_URL']) {
this.migrateWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/dataRefactorMigrate.html')
} else {
this.migrateWindow.loadURL('http://localhost:5173/dataMigrate.html')
this.migrateWindow.loadFile(join(__dirname, '../renderer/dataRefactorMigrate.html'))
}
this.migrateWindow.once('ready-to-show', () => {
@ -619,4 +561,4 @@ export class MigrateService {
}
// Export singleton instance
export const migrateService = MigrateService.getInstance()
export const dataRefactorMigrateService = DataRefactorMigrateService.getInstance()

View File

@ -3,7 +3,7 @@ import { preferenceTable } from '@data/db/schemas/preference'
import { loggerService } from '@logger'
import { and, eq } from 'drizzle-orm'
import { configManager } from '../../services/ConfigManager'
import { configManager } from '../../../../services/ConfigManager'
const logger = loggerService.withContext('PreferencesMigrator')

View File

@ -1,86 +0,0 @@
/**
* Auto-generated migration index
* Generated at: 2025-08-09T07:20:05.909Z
*
* This file is automatically generated from classification.json
* To update this file, modify classification.json and run:
* node .claude/data-classify/scripts/generate-migration.js
*
* === AUTO-GENERATED CONTENT START ===
*/
// LEGACY MIGRATION SYSTEM - COMMENTED OUT
// These files have been replaced by PreferencesMigrator.ts
// import { ElectronStoreMigrator } from './electronStoreToPreferences'
// import { ReduxMigrator } from './reduxToPreferences'
import { loggerService } from '@logger'
const logger = loggerService.withContext('MigrationManager')
export interface MigrationResult {
success: boolean
migratedCount: number
errors: Array<{
key: string
error: string
}>
source: 'electronStore' | 'redux'
}
export interface MigrationSummary {
totalItems: number
successCount: number
errorCount: number
electronStore: MigrationResult
redux: MigrationResult
}
export class MigrationManager {
// LEGACY MIGRATION SYSTEM - COMMENTED OUT
// private electronStoreMigrator: ElectronStoreMigrator
// private reduxMigrator: ReduxMigrator
constructor() {
// this.electronStoreMigrator = new ElectronStoreMigrator()
// this.reduxMigrator = new ReduxMigrator()
logger.warn('MigrationManager is deprecated. Use PreferencesMigrator instead.')
}
/**
* preferences迁移
* @returns
*/
async migrateAllPreferences(): Promise<MigrationSummary> {
logger.warn('MigrationManager.migrateAllPreferences is deprecated. Use PreferencesMigrator instead.')
// Return a placeholder summary since the actual migration is handled by PreferencesMigrator
const summary: MigrationSummary = {
totalItems: 0,
successCount: 0,
errorCount: 0,
electronStore: { success: false, migratedCount: 0, errors: [], source: 'electronStore' },
redux: { success: false, migratedCount: 0, errors: [], source: 'redux' }
}
return summary
}
/**
*
* @param summary
* @returns
*/
async validateMigration(_summary: MigrationSummary): Promise<boolean> {
logger.warn('MigrationManager.validateMigration is deprecated. Use PreferencesMigrator validation instead.')
return true
}
}
// === AUTO-GENERATED CONTENT END ===
/**
* :
* - 总迁移项: 158
* - ElectronStore项: 4
* - Redux项: 154
*/

View File

@ -9,7 +9,7 @@ import { loggerService } from '@logger'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import dbService from '@data/db/DbService'
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
import { app } from 'electron'
import { app, dialog } from 'electron'
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
import { isDev, isLinux, isWin } from './constant'
@ -27,7 +27,7 @@ import selectionService, { initSelectionService } from './services/SelectionServ
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
import { migrateService } from './data/migrate/MigrateService'
import { dataRefactorMigrateService } from './data/migrate/dataRefactor/DataRefactorMigrateService'
import process from 'node:process'
const logger = loggerService.withContext('MainEntry')
@ -102,14 +102,35 @@ if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
} else {
dbService.migrateDb().then(async () => {
await dbService.migrateSeed('preference')
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
// First of all, init & migrate the database
await dbService.migrateDb()
await dbService.migrateSeed('preference')
// Data Refactor Migration
// Check if data migration is needed BEFORE creating any windows
try {
const isMigrated = await dataRefactorMigrateService.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
}
} 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}`
)
app.quit()
return
}
// Set app user model id for windows
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
@ -119,22 +140,6 @@ if (!app.requestSingleInstanceLock()) {
app.dock?.hide()
}
// Check if data migration is needed BEFORE creating any windows
try {
const needsMigration = await migrateService.checkMigrationNeeded()
if (needsMigration) {
logger.info('Migration needed, starting migration process')
await migrateService.runMigration()
logger.info('Migration completed, app will restart automatically')
// Migration service will handle app restart, no need to continue startup
return
}
} catch (error) {
logger.error('Migration process failed', error as Error)
// Continue with normal startup even if migration fails
// The user can retry migration later or use backup recovery
}
// Only create main window if no migration was needed or migration failed
const mainWindow = windowService.createMainWindow()

View File

@ -3,14 +3,14 @@
<head>
<meta charset="UTF-8" />
<title>Cherry Studio - Data Migration</title>
<title>Cherry Studio - Data Refactor Migration</title>
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body id="root">
<script type="module" src="/src/windows/dataMigrate/entryPoint.tsx"></script>
<script type="module" src="/src/windows/dataRefactorMigrate/entryPoint.tsx"></script>
</body>
</html>