mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-23 04:31:01 +02:00
Make migration management more robust
This commit is contained in:
@@ -1,20 +1,21 @@
|
||||
const { Umzug, SequelizeStorage } = require('umzug')
|
||||
const { Sequelize } = require('sequelize')
|
||||
const { Sequelize, DataTypes } = require('sequelize')
|
||||
const semver = require('semver')
|
||||
const path = require('path')
|
||||
const Module = require('module')
|
||||
const fs = require('../libs/fsExtra')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
class MigrationManager {
|
||||
static MIGRATIONS_META_TABLE = 'migrationsMeta'
|
||||
|
||||
constructor(sequelize, configPath = global.configPath) {
|
||||
if (!sequelize || !(sequelize instanceof Sequelize)) {
|
||||
throw new Error('Sequelize instance is required for MigrationManager.')
|
||||
}
|
||||
if (!sequelize || !(sequelize instanceof Sequelize)) throw new Error('Sequelize instance is required for MigrationManager.')
|
||||
this.sequelize = sequelize
|
||||
if (!configPath) {
|
||||
throw new Error('Config path is required for MigrationManager.')
|
||||
}
|
||||
if (!configPath) throw new Error('Config path is required for MigrationManager.')
|
||||
this.configPath = configPath
|
||||
this.migrationsSourceDir = path.join(__dirname, '..', 'migrations')
|
||||
this.initialized = false
|
||||
this.migrationsDir = null
|
||||
this.maxVersion = null
|
||||
this.databaseVersion = null
|
||||
@@ -22,8 +23,36 @@ class MigrationManager {
|
||||
this.umzug = null
|
||||
}
|
||||
|
||||
async runMigrations(serverVersion) {
|
||||
await this.init(serverVersion)
|
||||
async init(serverVersion) {
|
||||
if (!(await fs.pathExists(this.configPath))) throw new Error(`Config path does not exist: ${this.configPath}`)
|
||||
|
||||
this.migrationsDir = path.join(this.configPath, 'migrations')
|
||||
|
||||
this.serverVersion = this.extractVersionFromTag(serverVersion)
|
||||
if (!this.serverVersion) throw new Error(`Invalid server version: ${serverVersion}. Expected a version tag like v1.2.3.`)
|
||||
|
||||
await this.fetchVersionsFromDatabase()
|
||||
if (!this.maxVersion || !this.databaseVersion) throw new Error('Failed to fetch versions from the database.')
|
||||
|
||||
if (semver.gt(this.serverVersion, this.maxVersion)) {
|
||||
try {
|
||||
await this.copyMigrationsToConfigDir()
|
||||
} catch (error) {
|
||||
throw new Error('Failed to copy migrations to the config directory.', { cause: error })
|
||||
}
|
||||
|
||||
try {
|
||||
await this.updateMaxVersion()
|
||||
} catch (error) {
|
||||
throw new Error('Failed to update max version in the database.', { cause: error })
|
||||
}
|
||||
}
|
||||
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
async runMigrations() {
|
||||
if (!this.initialized) throw new Error('MigrationManager is not initialized. Call init() first.')
|
||||
|
||||
const versionCompare = semver.compare(this.serverVersion, this.databaseVersion)
|
||||
if (versionCompare == 0) {
|
||||
@@ -31,6 +60,7 @@ class MigrationManager {
|
||||
return
|
||||
}
|
||||
|
||||
this.initUmzug()
|
||||
const migrations = await this.umzug.migrations()
|
||||
const executedMigrations = (await this.umzug.executed()).map((m) => m.name)
|
||||
|
||||
@@ -51,7 +81,7 @@ class MigrationManager {
|
||||
Logger.info('Created a backup of the original database.')
|
||||
|
||||
// Run migrations
|
||||
await this.umzug[migrationDirection]({ migrations: migrationsToRun })
|
||||
await this.umzug[migrationDirection]({ migrations: migrationsToRun, rerun: 'ALLOW' })
|
||||
|
||||
// Clean up the backup
|
||||
await fs.remove(backupDbPath)
|
||||
@@ -60,7 +90,7 @@ class MigrationManager {
|
||||
} catch (error) {
|
||||
Logger.error('[MigrationManager] Migration failed:', error)
|
||||
|
||||
this.sequelize.close()
|
||||
await this.sequelize.close()
|
||||
|
||||
// Step 3: If migration fails, save the failed original and restore the backup
|
||||
const failedDbPath = path.join(this.configPath, 'absdatabase.failed.sqlite')
|
||||
@@ -75,45 +105,40 @@ class MigrationManager {
|
||||
} else {
|
||||
Logger.info('[MigrationManager] No migrations to run.')
|
||||
}
|
||||
|
||||
await this.updateDatabaseVersion()
|
||||
}
|
||||
|
||||
async init(serverVersion, umzugStorage = new SequelizeStorage({ sequelize: this.sequelize })) {
|
||||
if (!(await fs.pathExists(this.configPath))) throw new Error(`Config path does not exist: ${this.configPath}`)
|
||||
|
||||
this.migrationsDir = path.join(this.configPath, 'migrations')
|
||||
|
||||
this.serverVersion = this.extractVersionFromTag(serverVersion)
|
||||
if (!this.serverVersion) throw new Error(`Invalid server version: ${serverVersion}. Expected a version tag like v1.2.3.`)
|
||||
|
||||
await this.fetchVersionsFromDatabase()
|
||||
if (!this.maxVersion || !this.databaseVersion) throw new Error('Failed to fetch versions from the database.')
|
||||
|
||||
if (semver.gt(this.serverVersion, this.maxVersion)) {
|
||||
try {
|
||||
await this.copyMigrationsToConfigDir()
|
||||
} catch (error) {
|
||||
throw new Error('Failed to copy migrations to the config directory.', { cause: error })
|
||||
}
|
||||
|
||||
try {
|
||||
await this.updateMaxVersion(serverVersion)
|
||||
} catch (error) {
|
||||
throw new Error('Failed to update max version in the database.', { cause: error })
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Initialize the Umzug instance
|
||||
initUmzug(umzugStorage = new SequelizeStorage({ sequelize: this.sequelize })) {
|
||||
if (!this.umzug) {
|
||||
// This check is for dependency injection in tests
|
||||
const cwd = this.migrationsDir
|
||||
|
||||
const parent = new Umzug({
|
||||
migrations: {
|
||||
glob: ['*.js', { cwd }]
|
||||
glob: ['*.js', { cwd }],
|
||||
resolve: (params) => {
|
||||
// make script think it's in migrationsSourceDir
|
||||
const migrationPath = params.path
|
||||
const migrationName = params.name
|
||||
const contents = fs.readFileSync(migrationPath, 'utf8')
|
||||
const fakePath = path.join(this.migrationsSourceDir, path.basename(migrationPath))
|
||||
const module = new Module(fakePath)
|
||||
module.filename = fakePath
|
||||
module.paths = Module._nodeModulePaths(this.migrationsSourceDir)
|
||||
module._compile(contents, fakePath)
|
||||
const script = module.exports
|
||||
return {
|
||||
name: migrationName,
|
||||
path: migrationPath,
|
||||
up: script.up,
|
||||
down: script.down
|
||||
}
|
||||
}
|
||||
},
|
||||
context: this.sequelize.getQueryInterface(),
|
||||
context: { queryInterface: this.sequelize.getQueryInterface(), logger: Logger },
|
||||
storage: umzugStorage,
|
||||
logger: Logger.info
|
||||
logger: Logger
|
||||
})
|
||||
|
||||
// Sort migrations by version
|
||||
@@ -130,18 +155,38 @@ class MigrationManager {
|
||||
}
|
||||
|
||||
async fetchVersionsFromDatabase() {
|
||||
const [result] = await this.sequelize.query("SELECT json_extract(value, '$.version') AS version, json_extract(value, '$.maxVersion') AS maxVersion FROM settings WHERE key = :key", {
|
||||
replacements: { key: 'server-settings' },
|
||||
await this.checkOrCreateMigrationsMetaTable()
|
||||
|
||||
const [{ version }] = await this.sequelize.query("SELECT value as version FROM :migrationsMeta WHERE key = 'version'", {
|
||||
replacements: { migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
|
||||
type: Sequelize.QueryTypes.SELECT
|
||||
})
|
||||
this.databaseVersion = version
|
||||
|
||||
if (result) {
|
||||
try {
|
||||
this.maxVersion = this.extractVersionFromTag(result.maxVersion) || '0.0.0'
|
||||
this.databaseVersion = this.extractVersionFromTag(result.version)
|
||||
} catch (error) {
|
||||
Logger.error('[MigrationManager] Failed to parse server settings from the database.', error)
|
||||
}
|
||||
const [{ maxVersion }] = await this.sequelize.query("SELECT value as maxVersion FROM :migrationsMeta WHERE key = 'maxVersion'", {
|
||||
replacements: { migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
|
||||
type: Sequelize.QueryTypes.SELECT
|
||||
})
|
||||
this.maxVersion = maxVersion
|
||||
}
|
||||
|
||||
async checkOrCreateMigrationsMetaTable() {
|
||||
const queryInterface = this.sequelize.getQueryInterface()
|
||||
if (!(await queryInterface.tableExists(MigrationManager.MIGRATIONS_META_TABLE))) {
|
||||
await queryInterface.createTable(MigrationManager.MIGRATIONS_META_TABLE, {
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
})
|
||||
await this.sequelize.query("INSERT INTO :migrationsMeta (key, value) VALUES ('version', :version), ('maxVersion', '0.0.0')", {
|
||||
replacements: { version: this.serverVersion, migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
|
||||
type: Sequelize.QueryTypes.INSERT
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,18 +197,16 @@ class MigrationManager {
|
||||
}
|
||||
|
||||
async copyMigrationsToConfigDir() {
|
||||
const migrationsSourceDir = path.join(__dirname, '..', 'migrations')
|
||||
|
||||
await fs.ensureDir(this.migrationsDir) // Ensure the target directory exists
|
||||
|
||||
if (!(await fs.pathExists(migrationsSourceDir))) return
|
||||
if (!(await fs.pathExists(this.migrationsSourceDir))) return
|
||||
|
||||
const files = await fs.readdir(migrationsSourceDir)
|
||||
const files = await fs.readdir(this.migrationsSourceDir)
|
||||
await Promise.all(
|
||||
files
|
||||
.filter((file) => path.extname(file) === '.js')
|
||||
.map(async (file) => {
|
||||
const sourceFile = path.join(migrationsSourceDir, file)
|
||||
const sourceFile = path.join(this.migrationsSourceDir, file)
|
||||
const targetFile = path.join(this.migrationsDir, file)
|
||||
await fs.copy(sourceFile, targetFile) // Asynchronously copy the files
|
||||
})
|
||||
@@ -189,13 +232,29 @@ class MigrationManager {
|
||||
}
|
||||
}
|
||||
|
||||
async updateMaxVersion(serverVersion) {
|
||||
await this.sequelize.query("UPDATE settings SET value = JSON_SET(value, '$.maxVersion', ?) WHERE key = 'server-settings'", {
|
||||
replacements: [serverVersion],
|
||||
type: Sequelize.QueryTypes.UPDATE
|
||||
})
|
||||
async updateMaxVersion() {
|
||||
try {
|
||||
await this.sequelize.query("UPDATE :migrationsMeta SET value = :maxVersion WHERE key = 'maxVersion'", {
|
||||
replacements: { maxVersion: this.serverVersion, migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
|
||||
type: Sequelize.QueryTypes.UPDATE
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error('Failed to update maxVersion in the migrationsMeta table.', { cause: error })
|
||||
}
|
||||
this.maxVersion = this.serverVersion
|
||||
}
|
||||
|
||||
async updateDatabaseVersion() {
|
||||
try {
|
||||
await this.sequelize.query("UPDATE :migrationsMeta SET value = :version WHERE key = 'version'", {
|
||||
replacements: { version: this.serverVersion, migrationsMeta: MigrationManager.MIGRATIONS_META_TABLE },
|
||||
type: Sequelize.QueryTypes.UPDATE
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error('Failed to update version in the migrationsMeta table.', { cause: error })
|
||||
}
|
||||
this.databaseVersion = this.serverVersion
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MigrationManager
|
||||
|
Reference in New Issue
Block a user