添加启动时的数据库表及表结构完整性校验

pull/1211/head
lyswhut 2023-02-06 17:33:21 +08:00
parent c8eac2690f
commit cf960e3695
5 changed files with 260 additions and 95 deletions

View File

@ -8,6 +8,7 @@
- 新增当前版本更新日志显示弹窗(建议大家阅读更新日志以了解当前版本的变化),在更新版本后将自动弹出
- 新增是否在更新版本的首次启动时显示更新日志弹窗设置,默认开启,可以去设置-软件更新更改
- 添加wy、tx源逐字歌词的支持
- 添加启动时的数据库表及表结构完整性校验,若未通过校验,则会显示弹窗提示后将该数据库重命名添加`.bak`后缀后重建数据库启动。对于某些人遇到更新到v2.0.0后出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表的问题,可以通过此特性自动重建数据库并重新迁移数据,不再需要手动去数据目录删除数据库
### 优化

View File

@ -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()

View File

@ -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"')

View File

@ -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<string, string>()
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

View File

@ -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<string, string>()
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() }))
}