Update:Add server setting for backupPath and allow overriding with BACKUP_PATH env variable #2973

This commit is contained in:
advplyr 2024-06-18 17:10:49 -05:00
parent a75ad5d659
commit 7bc70effb0
4 changed files with 66 additions and 54 deletions

View File

@ -10,6 +10,7 @@ if (isDev) {
if (devEnv.FFmpegPath) process.env.FFMPEG_PATH = devEnv.FFmpegPath if (devEnv.FFmpegPath) process.env.FFMPEG_PATH = devEnv.FFmpegPath
if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath
if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1' if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
process.env.SOURCE = 'local' process.env.SOURCE = 'local'
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || '' process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
} }

View File

@ -2,12 +2,12 @@ const Logger = require('../Logger')
const { encodeUriPath } = require('../utils/fileUtils') const { encodeUriPath } = require('../utils/fileUtils')
class BackupController { class BackupController {
constructor() { } constructor() {}
getAll(req, res) { getAll(req, res) {
res.json({ res.json({
backups: this.backupManager.backups.map(b => b.toJSON()), backups: this.backupManager.backups.map((b) => b.toJSON()),
backupLocation: this.backupManager.backupLocation backupLocation: this.backupManager.backupPath
}) })
} }
@ -19,7 +19,7 @@ class BackupController {
await this.backupManager.removeBackup(req.backup) await this.backupManager.removeBackup(req.backup)
res.json({ res.json({
backups: this.backupManager.backups.map(b => b.toJSON()) backups: this.backupManager.backups.map((b) => b.toJSON())
}) })
} }
@ -33,9 +33,9 @@ class BackupController {
/** /**
* api/backups/:id/download * api/backups/:id/download
* *
* @param {*} req * @param {*} req
* @param {*} res * @param {*} res
*/ */
download(req, res) { download(req, res) {
if (global.XAccel) { if (global.XAccel) {
@ -50,9 +50,9 @@ class BackupController {
} }
/** /**
* *
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
*/ */
apply(req, res) { apply(req, res) {
this.backupManager.requestApplyBackup(this.apiCacheManager, req.backup, res) this.backupManager.requestApplyBackup(this.apiCacheManager, req.backup, res)
@ -65,7 +65,7 @@ class BackupController {
} }
if (req.params.id) { if (req.params.id) {
req.backup = this.backupManager.backups.find(b => b.id === req.params.id) req.backup = this.backupManager.backups.find((b) => b.id === req.params.id)
if (!req.backup) { if (!req.backup) {
return res.sendStatus(404) return res.sendStatus(404)
} }

View File

@ -17,7 +17,6 @@ const Backup = require('../objects/Backup')
class BackupManager { class BackupManager {
constructor() { constructor() {
this.BackupPath = Path.join(global.MetadataPath, 'backups')
this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items') this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items')
this.AuthorsMetadataPath = Path.join(global.MetadataPath, 'authors') this.AuthorsMetadataPath = Path.join(global.MetadataPath, 'authors')
@ -26,8 +25,8 @@ class BackupManager {
this.backups = [] this.backups = []
} }
get backupLocation() { get backupPath() {
return this.BackupPath return global.ServerSettings.backupPath
} }
get backupSchedule() { get backupSchedule() {
@ -43,9 +42,9 @@ class BackupManager {
} }
async init() { async init() {
const backupsDirExists = await fs.pathExists(this.BackupPath) const backupsDirExists = await fs.pathExists(this.backupPath)
if (!backupsDirExists) { if (!backupsDirExists) {
await fs.ensureDir(this.BackupPath) await fs.ensureDir(this.backupPath)
} }
await this.loadBackups() await this.loadBackups()
@ -87,11 +86,14 @@ class BackupManager {
return res.status(500).send('Invalid backup file') return res.status(500).send('Invalid backup file')
} }
const tempPath = Path.join(this.BackupPath, fileUtils.sanitizeFilename(backupFile.name)) const tempPath = Path.join(this.backupPath, fileUtils.sanitizeFilename(backupFile.name))
const success = await backupFile.mv(tempPath).then(() => true).catch((error) => { const success = await backupFile
Logger.error('[BackupManager] Failed to move backup file', path, error) .mv(tempPath)
return false .then(() => true)
}) .catch((error) => {
Logger.error('[BackupManager] Failed to move backup file', path, error)
return false
})
if (!success) { if (!success) {
return res.status(500).send('Failed to move backup file into backups directory') return res.status(500).send('Failed to move backup file into backups directory')
} }
@ -122,7 +124,7 @@ class BackupManager {
backup.fileSize = await getFileSize(backup.fullPath) backup.fileSize = await getFileSize(backup.fullPath)
const existingBackupIndex = this.backups.findIndex(b => b.id === backup.id) const existingBackupIndex = this.backups.findIndex((b) => b.id === backup.id)
if (existingBackupIndex >= 0) { if (existingBackupIndex >= 0) {
Logger.warn(`[BackupManager] Backup already exists with id ${backup.id} - overwriting`) Logger.warn(`[BackupManager] Backup already exists with id ${backup.id} - overwriting`)
this.backups.splice(existingBackupIndex, 1, backup) this.backups.splice(existingBackupIndex, 1, backup)
@ -131,7 +133,7 @@ class BackupManager {
} }
res.json({ res.json({
backups: this.backups.map(b => b.toJSON()) backups: this.backups.map((b) => b.toJSON())
}) })
} }
@ -139,7 +141,7 @@ class BackupManager {
var backupSuccess = await this.runBackup() var backupSuccess = await this.runBackup()
if (backupSuccess) { if (backupSuccess) {
res.json({ res.json({
backups: this.backups.map(b => b.toJSON()) backups: this.backups.map((b) => b.toJSON())
}) })
} else { } else {
res.sendStatus(500) res.sendStatus(500)
@ -147,10 +149,10 @@ class BackupManager {
} }
/** /**
* *
* @param {import('./ApiCacheManager')} apiCacheManager * @param {import('./ApiCacheManager')} apiCacheManager
* @param {Backup} backup * @param {Backup} backup
* @param {import('express').Response} res * @param {import('express').Response} res
*/ */
async requestApplyBackup(apiCacheManager, backup, res) { async requestApplyBackup(apiCacheManager, backup, res) {
Logger.info(`[BackupManager] Applying backup at "${backup.fullPath}"`) Logger.info(`[BackupManager] Applying backup at "${backup.fullPath}"`)
@ -176,7 +178,7 @@ class BackupManager {
Logger.info(`[BackupManager] Extracted backup sqlite db to temp path ${tempDbPath}`) Logger.info(`[BackupManager] Extracted backup sqlite db to temp path ${tempDbPath}`)
// Verify extract - Abandon backup if sqlite file did not extract // Verify extract - Abandon backup if sqlite file did not extract
if (!await fs.pathExists(tempDbPath)) { if (!(await fs.pathExists(tempDbPath))) {
Logger.error(`[BackupManager] Sqlite file not found after extract - abandon backup apply and reconnect db`) Logger.error(`[BackupManager] Sqlite file not found after extract - abandon backup apply and reconnect db`)
await zip.close() await zip.close()
await Database.reconnect() await Database.reconnect()
@ -218,12 +220,12 @@ class BackupManager {
async loadBackups() { async loadBackups() {
try { try {
const filesInDir = await fs.readdir(this.BackupPath) const filesInDir = await fs.readdir(this.backupPath)
for (let i = 0; i < filesInDir.length; i++) { for (let i = 0; i < filesInDir.length; i++) {
const filename = filesInDir[i] const filename = filesInDir[i]
if (filename.endsWith('.audiobookshelf')) { if (filename.endsWith('.audiobookshelf')) {
const fullFilePath = Path.join(this.BackupPath, filename) const fullFilePath = Path.join(this.backupPath, filename)
let zip = null let zip = null
let data = null let data = null
@ -239,14 +241,16 @@ class BackupManager {
const backup = new Backup({ details, fullPath: fullFilePath }) const backup = new Backup({ details, fullPath: fullFilePath })
if (!backup.serverVersion) { // Backups before v2 if (!backup.serverVersion) {
// Backups before v2
Logger.error(`[BackupManager] Old unsupported backup was found "${backup.filename}"`) Logger.error(`[BackupManager] Old unsupported backup was found "${backup.filename}"`)
} else if (!backup.key) { // Backups before sqlite migration } else if (!backup.key) {
// Backups before sqlite migration
Logger.warn(`[BackupManager] Old unsupported backup was found "${backup.filename}" (pre sqlite migration)`) Logger.warn(`[BackupManager] Old unsupported backup was found "${backup.filename}" (pre sqlite migration)`)
} }
backup.fileSize = await getFileSize(backup.fullPath) backup.fileSize = await getFileSize(backup.fullPath)
const existingBackupWithId = this.backups.find(b => b.id === backup.id) const existingBackupWithId = this.backups.find((b) => b.id === backup.id)
if (existingBackupWithId) { if (existingBackupWithId) {
Logger.warn(`[BackupManager] Backup already loaded with id ${backup.id} - ignoring`) Logger.warn(`[BackupManager] Backup already loaded with id ${backup.id} - ignoring`)
} else { } else {
@ -267,7 +271,7 @@ class BackupManager {
// Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself) // Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself)
Logger.info(`[BackupManager] Running Backup`) Logger.info(`[BackupManager] Running Backup`)
const newBackup = new Backup() const newBackup = new Backup()
newBackup.setData(this.BackupPath) newBackup.setData(this.backupPath)
await fs.ensureDir(this.AuthorsMetadataPath) await fs.ensureDir(this.AuthorsMetadataPath)
@ -296,7 +300,7 @@ class BackupManager {
newBackup.fileSize = await getFileSize(newBackup.fullPath) newBackup.fileSize = await getFileSize(newBackup.fullPath)
const existingIndex = this.backups.findIndex(b => b.id === newBackup.id) const existingIndex = this.backups.findIndex((b) => b.id === newBackup.id)
if (existingIndex >= 0) { if (existingIndex >= 0) {
this.backups.splice(existingIndex, 1, newBackup) this.backups.splice(existingIndex, 1, newBackup)
} else { } else {
@ -318,7 +322,7 @@ class BackupManager {
try { try {
Logger.debug(`[BackupManager] Removing Backup "${backup.fullPath}"`) Logger.debug(`[BackupManager] Removing Backup "${backup.fullPath}"`)
await fs.remove(backup.fullPath) await fs.remove(backup.fullPath)
this.backups = this.backups.filter(b => b.id !== backup.id) this.backups = this.backups.filter((b) => b.id !== backup.id)
Logger.info(`[BackupManager] Backup "${backup.id}" Removed`) Logger.info(`[BackupManager] Backup "${backup.id}" Removed`)
} catch (error) { } catch (error) {
Logger.error(`[BackupManager] Failed to remove backup`, error) Logger.error(`[BackupManager] Failed to remove backup`, error)
@ -425,4 +429,4 @@ class BackupManager {
}) })
} }
} }
module.exports = BackupManager module.exports = BackupManager

View File

@ -1,3 +1,4 @@
const Path = require('path')
const packageJson = require('../../../package.json') const packageJson = require('../../../package.json')
const { BookshelfView } = require('../../utils/constants') const { BookshelfView } = require('../../utils/constants')
const Logger = require('../../Logger') const Logger = require('../../Logger')
@ -25,6 +26,7 @@ class ServerSettings {
this.rateLimitLoginWindow = 10 * 60 * 1000 // 10 Minutes this.rateLimitLoginWindow = 10 * 60 * 1000 // 10 Minutes
// Backups // Backups
this.backupPath = Path.join(global.MetadataPath, 'backups')
this.backupSchedule = false // If false then auto-backups are disabled this.backupSchedule = false // If false then auto-backups are disabled
this.backupsToKeep = 2 this.backupsToKeep = 2
this.maxBackupSize = 1 this.maxBackupSize = 1
@ -97,6 +99,7 @@ class ServerSettings {
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10 this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
this.backupPath = settings.backupPath || Path.join(global.MetadataPath, 'backups')
this.backupSchedule = settings.backupSchedule || false this.backupSchedule = settings.backupSchedule || false
this.backupsToKeep = settings.backupsToKeep || 2 this.backupsToKeep = settings.backupsToKeep || 2
this.maxBackupSize = settings.maxBackupSize || 1 this.maxBackupSize = settings.maxBackupSize || 1
@ -147,22 +150,26 @@ class ServerSettings {
this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('openid', 0), 1) this.authActiveAuthMethods.splice(this.authActiveAuthMethods.indexOf('openid', 0), 1)
} }
// fallback to local // fallback to local
if (!Array.isArray(this.authActiveAuthMethods) || this.authActiveAuthMethods.length == 0) { if (!Array.isArray(this.authActiveAuthMethods) || this.authActiveAuthMethods.length == 0) {
this.authActiveAuthMethods = ['local'] this.authActiveAuthMethods = ['local']
} }
// Migrations // Migrations
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was renamed to storeCoverWithItem in 2.0.0 if (settings.storeCoverWithBook != undefined) {
// storeCoverWithBook was renamed to storeCoverWithItem in 2.0.0
this.storeCoverWithItem = !!settings.storeCoverWithBook this.storeCoverWithItem = !!settings.storeCoverWithBook
} }
if (settings.storeMetadataWithBook != undefined) { // storeMetadataWithBook was renamed to storeMetadataWithItem in 2.0.0 if (settings.storeMetadataWithBook != undefined) {
// storeMetadataWithBook was renamed to storeMetadataWithItem in 2.0.0
this.storeMetadataWithItem = !!settings.storeMetadataWithBook this.storeMetadataWithItem = !!settings.storeMetadataWithBook
} }
if (settings.homeBookshelfView == undefined) { // homeBookshelfView was added in 2.1.3 if (settings.homeBookshelfView == undefined) {
// homeBookshelfView was added in 2.1.3
this.homeBookshelfView = settings.bookshelfView this.homeBookshelfView = settings.bookshelfView
} }
if (settings.metadataFileFormat == undefined) { // metadataFileFormat was added in 2.2.21 if (settings.metadataFileFormat == undefined) {
// metadataFileFormat was added in 2.2.21
// All users using old settings will stay abs until changed // All users using old settings will stay abs until changed
this.metadataFileFormat = 'abs' this.metadataFileFormat = 'abs'
} }
@ -176,9 +183,15 @@ class ServerSettings {
if (this.logLevel !== Logger.logLevel) { if (this.logLevel !== Logger.logLevel) {
Logger.setLogLevel(this.logLevel) Logger.setLogLevel(this.logLevel)
} }
if (process.env.BACKUP_PATH && this.backupPath !== process.env.BACKUP_PATH) {
Logger.info(`[ServerSettings] Using backup path from environment variable ${process.env.BACKUP_PATH}`)
this.backupPath = process.env.BACKUP_PATH
}
} }
toJSON() { // Use toJSONForBrowser if sending to client toJSON() {
// Use toJSONForBrowser if sending to client
return { return {
id: this.id, id: this.id,
tokenSecret: this.tokenSecret, // Do not return to client tokenSecret: this.tokenSecret, // Do not return to client
@ -192,6 +205,7 @@ class ServerSettings {
metadataFileFormat: this.metadataFileFormat, metadataFileFormat: this.metadataFileFormat,
rateLimitLoginRequests: this.rateLimitLoginRequests, rateLimitLoginRequests: this.rateLimitLoginRequests,
rateLimitLoginWindow: this.rateLimitLoginWindow, rateLimitLoginWindow: this.rateLimitLoginWindow,
backupPath: this.backupPath,
backupSchedule: this.backupSchedule, backupSchedule: this.backupSchedule,
backupsToKeep: this.backupsToKeep, backupsToKeep: this.backupsToKeep,
maxBackupSize: this.maxBackupSize, maxBackupSize: this.maxBackupSize,
@ -249,14 +263,7 @@ class ServerSettings {
* Auth settings required for openid to be valid * Auth settings required for openid to be valid
*/ */
get isOpenIDAuthSettingsValid() { get isOpenIDAuthSettingsValid() {
return this.authOpenIDIssuerURL && return this.authOpenIDIssuerURL && this.authOpenIDAuthorizationURL && this.authOpenIDTokenURL && this.authOpenIDUserInfoURL && this.authOpenIDJwksURL && this.authOpenIDClientID && this.authOpenIDClientSecret && this.authOpenIDTokenSigningAlgorithm
this.authOpenIDAuthorizationURL &&
this.authOpenIDTokenURL &&
this.authOpenIDUserInfoURL &&
this.authOpenIDJwksURL &&
this.authOpenIDClientID &&
this.authOpenIDClientSecret &&
this.authOpenIDTokenSigningAlgorithm
} }
get authenticationSettings() { get authenticationSettings() {
@ -297,8 +304,8 @@ class ServerSettings {
/** /**
* Update server settings * Update server settings
* *
* @param {Object} payload * @param {Object} payload
* @returns {boolean} true if updates were made * @returns {boolean} true if updates were made
*/ */
update(payload) { update(payload) {