@@ -131,6 +131,19 @@ export async function migrateDatabaseFiles(
131131 const sourceStats = await stat ( sourceDb ) ;
132132 let totalSize = sourceStats . size ;
133133
134+ // CL-M9: 复制前检查目标目录是否已有数据库——copyFile 默认直接覆盖,
135+ // 若用户选择已存在 keban.db 的目录(历史存储路径/手动复制残留),
136+ // 旧库会被静默替换且无备份,造成不可逆数据丢失
137+ const targetDb = path . join ( targetDir , 'keban.db' ) ;
138+ if ( await fileExists ( targetDb ) ) {
139+ return {
140+ success : false ,
141+ sourcePath : sourceDir ,
142+ targetPath : targetDir ,
143+ error : '目标目录已存在数据库文件(keban.db),为防止覆盖旧数据,请选择空目录或将旧库改名后重试' ,
144+ } ;
145+ }
146+
134147 // 计算所有存在的文件大小
135148 for ( const fileName of DB_FILES ) {
136149 const filePath = path . join ( sourceDir , fileName ) ;
@@ -227,15 +240,38 @@ export function verifyDatabaseIntegrity(dbPath: string): boolean {
227240
228241/**
229242 * 为旧路径数据库创建备份
230- * 将 keban.db / keban.db-wal / keban.db-shm 各复制为 .bak(同目录)
243+ * 将 keban.db / keban.db-wal / keban.db-shm 各复制为带时间戳的 .bak(同目录)
244+ * CL-M9: 备份名固定为 keban.db.bak 会被多次切换路径时互相覆盖,回滚点丢失;
245+ * 改为时间戳后缀并仅保留最近 5 份。
231246 */
232247export async function createBackup ( dbDir : string ) : Promise < void > {
248+ const timestamp = new Date ( ) . toISOString ( ) . replace ( / [ - : T ] / g, '' ) . slice ( 0 , 14 ) ;
233249 for ( const fileName of DB_FILES ) {
234250 const sourcePath = path . join ( dbDir , fileName ) ;
235- const backupPath = path . join ( dbDir , fileName + '. bak' ) ;
251+ const backupPath = path . join ( dbDir , ` ${ fileName } . ${ timestamp } . bak` ) ;
236252 if ( await fileExists ( sourcePath ) ) {
237253 await copyFile ( sourcePath , backupPath ) ;
238- logger . info ( `[DBMigrator] Backup created: ${ fileName } .bak ` ) ;
254+ logger . info ( `[DBMigrator] Backup created: ${ path . basename ( backupPath ) } ` ) ;
239255 }
240256 }
257+ // 清理超出保留份数的旧备份(按文件名时间戳排序,保留最近 5 份)
258+ await pruneOldBackups ( dbDir ) ;
259+ }
260+
261+ /** 仅保留最近 5 份 .bak 备份(按时间戳后缀排序) */
262+ async function pruneOldBackups ( dbDir : string ) : Promise < void > {
263+ try {
264+ const { readdir } = await import ( 'fs/promises' ) ;
265+ const entries = await readdir ( dbDir ) ;
266+ const backupFiles = entries
267+ . filter ( ( name ) => name . endsWith ( '.bak' ) )
268+ . sort ( )
269+ . reverse ( ) ;
270+ for ( const name of backupFiles . slice ( 5 ) ) {
271+ await rm ( path . join ( dbDir , name ) ) ;
272+ logger . info ( `[DBMigrator] Pruned old backup: ${ name } ` ) ;
273+ }
274+ } catch ( err ) {
275+ logger . warn ( `[DBMigrator] Failed to prune old backups: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
276+ }
241277}
0 commit comments