+
+
{{ $strings.HeaderLogin }}
-
+
-
+
-
{{ error }}
+
{{ error }}
-
diff --git a/client/static/robots.txt b/client/static/robots.txt
new file mode 100644
index 00000000..95458b58
--- /dev/null
+++ b/client/static/robots.txt
@@ -0,0 +1,2 @@
+User-Agent: *
+Disallow: /
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index a62598af..a16b0c19 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 88b1581f..0bcb88be 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/server/Database.js b/server/Database.js
index e3fabe1f..64dc518e 100644
--- a/server/Database.js
+++ b/server/Database.js
@@ -217,7 +217,6 @@ class Database {
async disconnect() {
Logger.info(`[Database] Disconnecting sqlite db`)
await this.sequelize.close()
- this.sequelize = null
}
/**
diff --git a/server/controllers/BackupController.js b/server/controllers/BackupController.js
index 8104623e..bd6caa0b 100644
--- a/server/controllers/BackupController.js
+++ b/server/controllers/BackupController.js
@@ -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) {
diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js
index 1af069f3..bb99b8cb 100644
--- a/server/managers/ApiCacheManager.js
+++ b/server/managers/ApiCacheManager.js
@@ -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 }
diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js
index ef8ed643..40a74f14 100644
--- a/server/managers/BackupManager.js
+++ b/server/managers/BackupManager.js
@@ -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')
}