mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Fix:Applying backup not properly overwriting existing sqlite file
- Fixed resetting api cache on backup - Added loading indicator in backups table - Fixed apply backup api not responding with 200 http status code - Added additional logging and failsafes
This commit is contained in:
parent
88f9533b37
commit
a2b2a2d060
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="text-center mt-4">
|
||||
<div class="text-center mt-4 relative">
|
||||
<div class="flex py-4">
|
||||
<ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">{{ $strings.ButtonUploadBackup }}</ui-file-input>
|
||||
<div class="flex-grow" />
|
||||
@ -54,6 +54,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</prompt-dialog>
|
||||
|
||||
<div v-if="isApplyingBackup" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/20 rounded-md">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -64,6 +68,7 @@ export default {
|
||||
showConfirmApply: false,
|
||||
selectedBackup: null,
|
||||
isBackingUp: false,
|
||||
isApplyingBackup: false,
|
||||
processing: false,
|
||||
backups: []
|
||||
}
|
||||
@ -85,19 +90,21 @@ export default {
|
||||
},
|
||||
confirm() {
|
||||
this.showConfirmApply = false
|
||||
this.isApplyingBackup = true
|
||||
|
||||
this.$axios
|
||||
.$get(`/api/backups/${this.selectedBackup.id}/apply`)
|
||||
.then(() => {
|
||||
this.isBackingUp = false
|
||||
location.replace('/config/backups?backup=1')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.isBackingUp = false
|
||||
console.error('Failed to apply backup', error)
|
||||
const errorMsg = error.response.data || this.$strings.ToastBackupRestoreFailed
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isApplyingBackup = false
|
||||
})
|
||||
},
|
||||
deleteBackupClick(backup) {
|
||||
if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) {
|
||||
@ -180,7 +187,6 @@ export default {
|
||||
this.loadBackups()
|
||||
if (this.$route.query.backup) {
|
||||
this.$toast.success('Backup applied successfully')
|
||||
this.$router.replace('/config')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -217,7 +217,6 @@ class Database {
|
||||
async disconnect() {
|
||||
Logger.info(`[Database] Disconnecting sqlite db`)
|
||||
await this.sequelize.close()
|
||||
this.sequelize = null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,8 +49,13 @@ class BackupController {
|
||||
res.sendFile(req.backup.fullPath)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
apply(req, res) {
|
||||
this.backupManager.requestApplyBackup(req.backup, res)
|
||||
this.backupManager.requestApplyBackup(this.apiCacheManager, req.backup, res)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
|
@ -22,6 +22,16 @@ class ApiCacheManager {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset hooks and clear cache. Used when applying backups
|
||||
*/
|
||||
reset() {
|
||||
Logger.info(`[ApiCacheManager] Resetting cache`)
|
||||
|
||||
this.init()
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
get middleware() {
|
||||
return (req, res, next) => {
|
||||
const key = { user: req.user.username, url: req.url }
|
||||
|
@ -146,23 +146,73 @@ class BackupManager {
|
||||
}
|
||||
}
|
||||
|
||||
async requestApplyBackup(backup, res) {
|
||||
/**
|
||||
*
|
||||
* @param {import('./ApiCacheManager')} apiCacheManager
|
||||
* @param {Backup} backup
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async requestApplyBackup(apiCacheManager, backup, res) {
|
||||
Logger.info(`[BackupManager] Applying backup at "${backup.fullPath}"`)
|
||||
|
||||
const zip = new StreamZip.async({ file: backup.fullPath })
|
||||
|
||||
const entries = await zip.entries()
|
||||
|
||||
// Ensure backup has an absdatabase.sqlite file
|
||||
if (!Object.keys(entries).includes('absdatabase.sqlite')) {
|
||||
Logger.error(`[BackupManager] Cannot apply old backup ${backup.fullPath}`)
|
||||
await zip.close()
|
||||
return res.status(500).send('Invalid backup file. Does not include absdatabase.sqlite. This might be from an older Audiobookshelf server.')
|
||||
}
|
||||
|
||||
await Database.disconnect()
|
||||
|
||||
await zip.extract('absdatabase.sqlite', global.ConfigPath)
|
||||
const dbPath = Path.join(global.ConfigPath, 'absdatabase.sqlite')
|
||||
const tempDbPath = Path.join(global.ConfigPath, 'absdatabase-temp.sqlite')
|
||||
|
||||
// Extract backup sqlite file to temporary path
|
||||
await zip.extract('absdatabase.sqlite', tempDbPath)
|
||||
Logger.info(`[BackupManager] Extracted backup sqlite db to temp path ${tempDbPath}`)
|
||||
|
||||
// Verify extract - Abandon backup if sqlite file did not extract
|
||||
if (!await fs.pathExists(tempDbPath)) {
|
||||
Logger.error(`[BackupManager] Sqlite file not found after extract - abandon backup apply and reconnect db`)
|
||||
await zip.close()
|
||||
await Database.reconnect()
|
||||
return res.status(500).send('Failed to extract sqlite db from backup')
|
||||
}
|
||||
|
||||
// Attempt to remove existing db file
|
||||
try {
|
||||
await fs.remove(dbPath)
|
||||
} catch (error) {
|
||||
// Abandon backup and remove extracted sqlite file if unable to remove existing db file
|
||||
Logger.error(`[BackupManager] Unable to overwrite existing db file - abandon backup apply and reconnect db`, error)
|
||||
await fs.remove(tempDbPath)
|
||||
await zip.close()
|
||||
await Database.reconnect()
|
||||
return res.status(500).send(`Failed to overwrite sqlite db: ${error?.message || 'Unknown Error'}`)
|
||||
}
|
||||
|
||||
// Rename temp db
|
||||
await fs.move(tempDbPath, dbPath)
|
||||
Logger.info(`[BackupManager] Saved backup sqlite file at "${dbPath}"`)
|
||||
|
||||
// Extract /metadata/items and /metadata/authors folders
|
||||
await zip.extract('metadata-items/', this.ItemsMetadataPath)
|
||||
await zip.extract('metadata-authors/', this.AuthorsMetadataPath)
|
||||
await zip.close()
|
||||
|
||||
// Reconnect db
|
||||
await Database.reconnect()
|
||||
|
||||
// Reset api cache, set hooks again
|
||||
await apiCacheManager.reset()
|
||||
|
||||
res.sendStatus(200)
|
||||
|
||||
// Triggers browser refresh for all clients
|
||||
SocketAuthority.emitter('backup_applied')
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user