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>
<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')
}
}
}

View File

@ -25,7 +25,8 @@ module.exports = {
meta: [
{ charset: 'utf-8' },
{ 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: [],
link: [

View File

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

View File

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

View File

@ -1,6 +1,13 @@
<template>
<div class="w-full h-screen bg-bg">
<div class="w-full flex h-full items-center justify-center">
<div id="page-wrapper" class="w-full h-screen overflow-y-auto">
<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">
<p class="text-center text-lg font-semibold">{{ $strings.MessageServerCouldNotBeReached }}</p>
</div>
@ -23,32 +30,34 @@
</div>
</form>
</div>
<div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40">
<p class="text-3xl text-white text-center mb-4">{{ $strings.HeaderLogin }}</p>
<div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 lg:-mt-40">
<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">
<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" />
<form v-show="login_local" @submit.prevent="submitForm">
<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" />
<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" />
<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>
<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" />
<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>
</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>
</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>

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",
"version": "2.8.0",
"version": "2.8.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
"version": "2.8.0",
"version": "2.8.1",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",

View File

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

View File

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

View File

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

View File

@ -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 }

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 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')
}