Merge branch 'advplyr:master' into master

This commit is contained in:
arcmagedr 2024-03-17 22:26:49 +02:00 committed by GitHub
commit 4ad09ec3d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 121 additions and 39 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="text-center mt-4"> <div class="text-center mt-4 relative">
<div class="flex py-4"> <div class="flex py-4">
<ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">{{ $strings.ButtonUploadBackup }}</ui-file-input> <ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">{{ $strings.ButtonUploadBackup }}</ui-file-input>
<div class="flex-grow" /> <div class="flex-grow" />
@ -54,6 +54,10 @@
</div> </div>
</div> </div>
</prompt-dialog> </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> </div>
</template> </template>
@ -64,6 +68,7 @@ export default {
showConfirmApply: false, showConfirmApply: false,
selectedBackup: null, selectedBackup: null,
isBackingUp: false, isBackingUp: false,
isApplyingBackup: false,
processing: false, processing: false,
backups: [] backups: []
} }
@ -85,19 +90,21 @@ export default {
}, },
confirm() { confirm() {
this.showConfirmApply = false this.showConfirmApply = false
this.isApplyingBackup = true
this.$axios this.$axios
.$get(`/api/backups/${this.selectedBackup.id}/apply`) .$get(`/api/backups/${this.selectedBackup.id}/apply`)
.then(() => { .then(() => {
this.isBackingUp = false
location.replace('/config/backups?backup=1') location.replace('/config/backups?backup=1')
}) })
.catch((error) => { .catch((error) => {
this.isBackingUp = false
console.error('Failed to apply backup', error) console.error('Failed to apply backup', error)
const errorMsg = error.response.data || this.$strings.ToastBackupRestoreFailed const errorMsg = error.response.data || this.$strings.ToastBackupRestoreFailed
this.$toast.error(errorMsg) this.$toast.error(errorMsg)
}) })
.finally(() => {
this.isApplyingBackup = false
})
}, },
deleteBackupClick(backup) { deleteBackupClick(backup) {
if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) { if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) {
@ -180,7 +187,6 @@ export default {
this.loadBackups() this.loadBackups()
if (this.$route.query.backup) { if (this.$route.query.backup) {
this.$toast.success('Backup applied successfully') this.$toast.success('Backup applied successfully')
this.$router.replace('/config')
} }
} }
} }

View File

@ -25,7 +25,8 @@ module.exports = {
meta: [ meta: [
{ charset: 'utf-8' }, { charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' } { hid: 'description', name: 'description', content: '' },
{ hid: 'robots', name: 'robots', content: 'noindex' }
], ],
script: [], script: [],
link: [ link: [

View File

@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.8.0", "version": "2.8.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.8.0", "version": "2.8.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
@ -16976,4 +16976,4 @@
} }
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.8.0", "version": "2.8.1",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
@ -36,4 +36,4 @@
"postcss": "^8.3.6", "postcss": "^8.3.6",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"
} }
} }

View File

@ -1,6 +1,13 @@
<template> <template>
<div class="w-full h-screen bg-bg"> <div id="page-wrapper" class="w-full h-screen overflow-y-auto">
<div class="w-full flex h-full items-center justify-center"> <div class="absolute z-0 top-0 left-0 px-6 py-3">
<div class="flex items-center">
<img src="~static/icon.svg" alt="Audiobookshelf Logo" class="w-10 min-w-10 h-10" />
<h1 class="text-xl ml-4 hidden lg:block hover:underline">audiobookshelf</h1>
</div>
</div>
<div class="relative z-10 w-full flex h-full items-center justify-center">
<div v-if="criticalError" class="w-full max-w-md rounded border border-error border-opacity-25 bg-error bg-opacity-10 p-4"> <div v-if="criticalError" class="w-full max-w-md rounded border border-error border-opacity-25 bg-error bg-opacity-10 p-4">
<p class="text-center text-lg font-semibold">{{ $strings.MessageServerCouldNotBeReached }}</p> <p class="text-center text-lg font-semibold">{{ $strings.MessageServerCouldNotBeReached }}</p>
</div> </div>
@ -23,32 +30,34 @@
</div> </div>
</form> </form>
</div> </div>
<div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40"> <div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 lg:-mt-40">
<p class="text-3xl text-white text-center mb-4">{{ $strings.HeaderLogin }}</p> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4">
<p class="text-2xl font-semibold text-center text-white mb-4">{{ $strings.HeaderLogin }}</p>
<div class="w-full h-px bg-white bg-opacity-10 my-4" /> <div class="w-full h-px bg-white bg-opacity-10 my-4" />
<p v-if="loginCustomMessage" class="py-2 default-style mb-2" v-html="loginCustomMessage"></p> <p v-if="loginCustomMessage" class="py-2 default-style mb-2" v-html="loginCustomMessage"></p>
<p v-if="error" class="text-error text-center py-2">{{ error }}</p> <p v-if="error" class="text-error text-center py-2">{{ error }}</p>
<form v-show="login_local" @submit.prevent="submitForm"> <form v-show="login_local" @submit.prevent="submitForm">
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
<ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" /> <ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
<ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" inputName="password" /> <ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" inputName="password" />
<div class="w-full flex justify-end py-3"> <div class="w-full flex justify-end py-3">
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn> <ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
</div>
</form>
<div v-if="login_local && login_openid" class="w-full h-px bg-white bg-opacity-10 my-4" />
<div class="w-full flex py-3">
<a v-if="login_openid" :href="openidAuthUri" class="w-full abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center bg-primary text-white px-8 py-2 leading-none">
{{ openIDButtonText }}
</a>
</div> </div>
</form>
<div v-if="login_local && login_openid" class="w-full h-px bg-white bg-opacity-10 my-4" />
<div class="w-full flex py-3">
<a v-if="login_openid" :href="openidAuthUri" class="w-full abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center bg-primary text-white px-8 py-2 leading-none">
{{ openIDButtonText }}
</a>
</div> </div>
</div> </div>
</div> </div>

2
client/static/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-Agent: *
Disallow: /

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.8.0", "version": "2.8.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.8.0", "version": "2.8.1",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.8.0", "version": "2.8.1",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast server", "description": "Self-hosted audiobook and podcast server",
"main": "index.js", "main": "index.js",

View File

@ -217,7 +217,6 @@ class Database {
async disconnect() { async disconnect() {
Logger.info(`[Database] Disconnecting sqlite db`) Logger.info(`[Database] Disconnecting sqlite db`)
await this.sequelize.close() await this.sequelize.close()
this.sequelize = null
} }
/** /**

View File

@ -49,8 +49,13 @@ class BackupController {
res.sendFile(req.backup.fullPath) res.sendFile(req.backup.fullPath)
} }
/**
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
apply(req, res) { apply(req, res) {
this.backupManager.requestApplyBackup(req.backup, res) this.backupManager.requestApplyBackup(this.apiCacheManager, req.backup, res)
} }
middleware(req, res, next) { middleware(req, res, next) {

View File

@ -22,6 +22,16 @@ class ApiCacheManager {
this.cache.clear() 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() { get middleware() {
return (req, res, next) => { return (req, res, next) => {
const key = { user: req.user.username, url: req.url } const key = { user: req.user.username, url: req.url }

View File

@ -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 zip = new StreamZip.async({ file: backup.fullPath })
const entries = await zip.entries() const entries = await zip.entries()
// Ensure backup has an absdatabase.sqlite file
if (!Object.keys(entries).includes('absdatabase.sqlite')) { if (!Object.keys(entries).includes('absdatabase.sqlite')) {
Logger.error(`[BackupManager] Cannot apply old backup ${backup.fullPath}`) 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.') 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 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-items/', this.ItemsMetadataPath)
await zip.extract('metadata-authors/', this.AuthorsMetadataPath) await zip.extract('metadata-authors/', this.AuthorsMetadataPath)
await zip.close()
// Reconnect db
await Database.reconnect() 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') SocketAuthority.emitter('backup_applied')
} }