diff --git a/publish/changeLog.md b/publish/changeLog.md index 16a537f5..79e42e25 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -8,6 +8,7 @@ - 新增当前版本更新日志显示弹窗(建议大家阅读更新日志以了解当前版本的变化),在更新版本后将自动弹出 - 新增是否在更新版本的首次启动时显示更新日志弹窗设置,默认开启,可以去设置-软件更新更改 - 添加wy、tx源逐字歌词的支持 +- 添加启动时的数据库表及表结构完整性校验,若未通过校验,则会显示弹窗提示后将该数据库重命名添加`.bak`后缀后重建数据库启动。对于某些人遇到更新到v2.0.0后出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表的问题,可以通过此特性自动重建数据库并重新迁移数据,不再需要手动去数据目录删除数据库 ### 优化 diff --git a/src/main/app.ts b/src/main/app.ts index 4b9f24f5..7d5850f5 100644 --- a/src/main/app.ts +++ b/src/main/app.ts @@ -1,6 +1,6 @@ import { join, dirname } from 'path' -import { existsSync, mkdirSync } from 'fs' -import { app, shell, screen, nativeTheme } from 'electron' +import { existsSync, mkdirSync, renameSync } from 'fs' +import { app, shell, screen, nativeTheme, dialog } from 'electron' import { URL_SCHEME_RXP } from '@common/constants' import { getTheme, initHotKey, initSetting, parseEnvParams } from './utils' import { navigationUrlWhiteList } from '@common/config' @@ -10,6 +10,7 @@ import { createAppEvent, createListEvent } from '@main/event' import { isMac, log } from '@common/utils' import createWorkers from './worker' import { migrateDBData } from './utils/migrate' +import { openDirInExplorer } from '@common/utils/electron' export const initGlobalData = () => { global.isDev = process.env.NODE_ENV !== 'production' @@ -237,7 +238,18 @@ export const initAppSetting = async() => { } if (!isInitialized) { - const dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath) + let dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath) + if (dbFileExists === null) { + const backPath = join(global.lxDataPath, `lx.data.db.${Date.now()}.bak`) + dialog.showMessageBoxSync({ + type: 'warning', + message: 'Database verify failed', + detail: `数据库表结构校验失败,我们将把有问题的数据库备份到:${backPath}\n若此问题导致你的数据丢失,你可以尝试从备份文件找回它们。\n\nThe database table structure verification failed, we will back up the problematic database to: ${backPath}\nIf this problem causes your data to be lost, you can try to retrieve them from the backup file.`, + }) + renameSync(join(global.lxDataPath, 'lx.data.db'), backPath) + openDirInExplorer(backPath) + dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath) + } global.lx.appSetting = (await initSetting()).setting if (!dbFileExists) await migrateDBData().catch(err => { log.error(err) }) initTheme() diff --git a/src/main/worker/dbService/db.ts b/src/main/worker/dbService/db.ts index e3a85bc4..721bea40 100644 --- a/src/main/worker/dbService/db.ts +++ b/src/main/worker/dbService/db.ts @@ -1,108 +1,22 @@ import Database from 'better-sqlite3' import path from 'path' +import tables from './tables' +import verifyDB from './verifyDB' // import migrateData from './migrate' let db: Database.Database const initTables = (db: Database.Database) => { - const sql = ` - CREATE TABLE "db_info" ( - "id" INTEGER NOT NULL UNIQUE, - "field_name" TEXT, - "field_value" TEXT, - PRIMARY KEY("id" AUTOINCREMENT) - ); - - CREATE TABLE "my_list" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "source" TEXT, - "sourceListId" TEXT, - "position" INTEGER NOT NULL, - "locationUpdateTime" INTEGER, - PRIMARY KEY("id") - ); - - CREATE TABLE "my_list_music_info" ( - "id" TEXT NOT NULL, - "listId" TEXT NOT NULL, - "name" TEXT NOT NULL, - "singer" TEXT NOT NULL, - "source" TEXT NOT NULL, - "interval" TEXT, - "meta" TEXT NOT NULL, - UNIQUE("id","listId") - ); - CREATE INDEX "index_my_list_music_info" ON "my_list_music_info" ( - "id", - "listId" - ); - - CREATE TABLE "my_list_music_info_order" ( - "listId" TEXT NOT NULL, - "musicInfoId" TEXT NOT NULL, - "order" INTEGER NOT NULL - ); - CREATE INDEX "index_my_list_music_info_order" ON "my_list_music_info_order" ( - "listId", - "musicInfoId" - ); - - CREATE TABLE "music_info_other_source" ( - "source_id" TEXT NOT NULL, - "id" TEXT NOT NULL, - "source" TEXT NOT NULL, - "name" TEXT NOT NULL, - "singer" TEXT NOT NULL, - "meta" TEXT NOT NULL, - "order" INTEGER NOT NULL, - UNIQUE("source_id","id") - ); - CREATE INDEX "index_music_info_other_source" ON "music_info_other_source" ( - "source_id", - "id" - ); - - -- TODO "meta" TEXT NOT NULL, - CREATE TABLE "lyric" ( - "id" TEXT NOT NULL, - "source" TEXT NOT NULL, - "type" TEXT NOT NULL, - "text" TEXT NOT NULL - ); - - CREATE TABLE "music_url" ( - "id" TEXT NOT NULL, - "url" TEXT NOT NULL - ); - - CREATE TABLE "download_list" ( - "id" TEXT NOT NULL, - "isComplate" INTEGER NOT NULL, - "status" TEXT NOT NULL, - "statusText" TEXT NOT NULL, - "progress_downloaded" INTEGER NOT NULL, - "progress_total" INTEGER NOT NULL, - "url" TEXT, - "quality" TEXT NOT NULL, - "ext" TEXT NOT NULL, - "fileName" TEXT NOT NULL, - "filePath" TEXT NOT NULL, - "musicInfo" TEXT NOT NULL, - "position" INTEGER NOT NULL, - PRIMARY KEY("id") - ); - + db.exec(` + ${Array.from(tables.values()).join('\n')} INSERT INTO "main"."db_info" ("field_name", "field_value") VALUES ('version', '1'); - ` - - db.exec(sql) + `) } // 打开、初始化数据库 -export const init = (lxDataPath: string): boolean => { +export const init = (lxDataPath: string): boolean | null => { const databasePath = path.join(lxDataPath, 'lx.data.db') const nativeBinding = path.join(__dirname, '../node_modules/better-sqlite3/build/Release/better_sqlite3.node') let dbFileExists = true @@ -127,6 +41,10 @@ export const init = (lxDataPath: string): boolean => { // https://www.sqlite.org/pragma.html#pragma_optimize if (dbFileExists) db.exec('PRAGMA optimize;') + if (!verifyDB(db)) { + db.close() + return null + } // https://www.sqlite.org/lang_vacuum.html // db.exec('VACUUM "main"') diff --git a/src/main/worker/dbService/tables.ts b/src/main/worker/dbService/tables.ts new file mode 100644 index 00000000..7fe4cddb --- /dev/null +++ b/src/main/worker/dbService/tables.ts @@ -0,0 +1,209 @@ +// export const sql = ` +// CREATE TABLE "db_info" ( +// "id" INTEGER NOT NULL UNIQUE, +// "field_name" TEXT, +// "field_value" TEXT, +// PRIMARY KEY("id" AUTOINCREMENT) +// ); + +// CREATE TABLE "my_list" ( +// "id" TEXT NOT NULL, +// "name" TEXT NOT NULL, +// "source" TEXT, +// "sourceListId" TEXT, +// "position" INTEGER NOT NULL, +// "locationUpdateTime" INTEGER, +// PRIMARY KEY("id") +// ); + +// CREATE TABLE "my_list_music_info" ( +// "id" TEXT NOT NULL, +// "listId" TEXT NOT NULL, +// "name" TEXT NOT NULL, +// "singer" TEXT NOT NULL, +// "source" TEXT NOT NULL, +// "interval" TEXT, +// "meta" TEXT NOT NULL, +// UNIQUE("id","listId") +// ); +// CREATE INDEX "index_my_list_music_info" ON "my_list_music_info" ( +// "id", +// "listId" +// ); + +// CREATE TABLE "my_list_music_info_order" ( +// "listId" TEXT NOT NULL, +// "musicInfoId" TEXT NOT NULL, +// "order" INTEGER NOT NULL +// ); +// CREATE INDEX "index_my_list_music_info_order" ON "my_list_music_info_order" ( +// "listId", +// "musicInfoId" +// ); + +// CREATE TABLE "music_info_other_source" ( +// "source_id" TEXT NOT NULL, +// "id" TEXT NOT NULL, +// "source" TEXT NOT NULL, +// "name" TEXT NOT NULL, +// "singer" TEXT NOT NULL, +// "meta" TEXT NOT NULL, +// "order" INTEGER NOT NULL, +// UNIQUE("source_id","id") +// ); +// CREATE INDEX "index_music_info_other_source" ON "music_info_other_source" ( +// "source_id", +// "id" +// ); + +// -- TODO "meta" TEXT NOT NULL, +// CREATE TABLE "lyric" ( +// "id" TEXT NOT NULL, +// "source" TEXT NOT NULL, +// "type" TEXT NOT NULL, +// "text" TEXT NOT NULL +// ); + +// CREATE TABLE "music_url" ( +// "id" TEXT NOT NULL, +// "url" TEXT NOT NULL +// ); + +// CREATE TABLE "download_list" ( +// "id" TEXT NOT NULL, +// "isComplate" INTEGER NOT NULL, +// "status" TEXT NOT NULL, +// "statusText" TEXT NOT NULL, +// "progress_downloaded" INTEGER NOT NULL, +// "progress_total" INTEGER NOT NULL, +// "url" TEXT, +// "quality" TEXT NOT NULL, +// "ext" TEXT NOT NULL, +// "fileName" TEXT NOT NULL, +// "filePath" TEXT NOT NULL, +// "musicInfo" TEXT NOT NULL, +// "position" INTEGER NOT NULL, +// PRIMARY KEY("id") +// ); +// ` + +// export const tables = [ +// 'table_db_info', +// 'table_my_list', +// 'table_my_list_music_info', +// 'index_index_my_list_music_info', +// 'table_my_list_music_info_order', +// 'index_index_my_list_music_info_order', +// 'table_music_info_other_source', +// 'index_index_music_info_other_source', +// 'table_lyric', +// 'table_music_url', +// 'table_download_list', +// ] + +const tables = new Map() + + +tables.set('db_info', ` + CREATE TABLE "db_info" ( + "id" INTEGER NOT NULL UNIQUE, + "field_name" TEXT, + "field_value" TEXT, + PRIMARY KEY("id" AUTOINCREMENT) + ); +`) +tables.set('my_list', ` + CREATE TABLE "my_list" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "source" TEXT, + "sourceListId" TEXT, + "position" INTEGER NOT NULL, + "locationUpdateTime" INTEGER, + PRIMARY KEY("id") + ); +`) +tables.set('my_list_music_info', ` + CREATE TABLE "my_list_music_info" ( + "id" TEXT NOT NULL, + "listId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "singer" TEXT NOT NULL, + "source" TEXT NOT NULL, + "interval" TEXT, + "meta" TEXT NOT NULL, + UNIQUE("id","listId") + ); +`) +tables.set('index_my_list_music_info', ` + CREATE INDEX "index_my_list_music_info" ON "my_list_music_info" ( + "id", + "listId" + ); +`) +tables.set('my_list_music_info_order', ` + CREATE TABLE "my_list_music_info_order" ( + "listId" TEXT NOT NULL, + "musicInfoId" TEXT NOT NULL, + "order" INTEGER NOT NULL + ); +`) +tables.set('index_my_list_music_info_order', ` + CREATE INDEX "index_my_list_music_info_order" ON "my_list_music_info_order" ( + "listId", + "musicInfoId" + ); +`) +tables.set('music_info_other_source', ` + CREATE TABLE "music_info_other_source" ( + "source_id" TEXT NOT NULL, + "id" TEXT NOT NULL, + "source" TEXT NOT NULL, + "name" TEXT NOT NULL, + "singer" TEXT NOT NULL, + "meta" TEXT NOT NULL, + "order" INTEGER NOT NULL, + UNIQUE("source_id","id") + ); +`) +tables.set('index_music_info_other_source', ` + CREATE INDEX "index_music_info_other_source" ON "music_info_other_source" ( + "source_id", + "id" + ); +`) +tables.set('lyric', ` + -- TODO "meta" TEXT NOT NULL, + CREATE TABLE "lyric" ( + "id" TEXT NOT NULL, + "source" TEXT NOT NULL, + "type" TEXT NOT NULL, + "text" TEXT NOT NULL + ); +`) +tables.set('music_url', ` + CREATE TABLE "music_url" ( + "id" TEXT NOT NULL, + "url" TEXT NOT NULL + ); +`) +tables.set('download_list', ` + CREATE TABLE "download_list" ( + "id" TEXT NOT NULL, + "isComplate" INTEGER NOT NULL, + "status" TEXT NOT NULL, + "statusText" TEXT NOT NULL, + "progress_downloaded" INTEGER NOT NULL, + "progress_total" INTEGER NOT NULL, + "url" TEXT, + "quality" TEXT NOT NULL, + "ext" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "filePath" TEXT NOT NULL, + "musicInfo" TEXT NOT NULL, + "position" INTEGER NOT NULL, + PRIMARY KEY("id") + ); +`) + +export default tables diff --git a/src/main/worker/dbService/verifyDB.ts b/src/main/worker/dbService/verifyDB.ts new file mode 100644 index 00000000..e0411ea8 --- /dev/null +++ b/src/main/worker/dbService/verifyDB.ts @@ -0,0 +1,25 @@ +import type Database from 'better-sqlite3' +import tables from './tables' + +const rxp = /\n|\s|;|--.+/g +export default (db: Database.Database) => { + const result = db.prepare('SELECT type,name,tbl_name,sql FROM "main".sqlite_master WHERE sql NOT NULL;').all() + const dbTableMap = new Map() + for (const info of result) dbTableMap.set(info.name, info.sql.replace(rxp, '')) + return Array.from(tables.entries()).every(([name, sql]) => { + const dbSql = dbTableMap.get(name) + return dbSql && dbSql == sql.replace(rxp, '') + }) + // console.log(dbTableMap) + // for (const [name, sql] of tables.entries()) { + // const dbSql = dbTableMap.get(name) + // if (dbSql) { + // if (dbSql == sql.replace(rxp, '')) continue + // console.log(dbSql) + // console.log(sql.replace(rxp, '')) + // } else { + // console.log(name) + // } + // } + // if (result.every((info) => { tables.includes() })) +}