mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-24 00:21:12 +01:00
New data model backups and move backups to API endpoints
This commit is contained in:
parent
eea3e2583c
commit
c9ea5dd2d7
@ -23,7 +23,7 @@
|
|||||||
<div class="w-full flex flex-row items-center justify-center">
|
<div class="w-full flex flex-row items-center justify-center">
|
||||||
<ui-btn small color="primary" @click="applyBackup(backup)">Apply</ui-btn>
|
<ui-btn small color="primary" @click="applyBackup(backup)">Apply</ui-btn>
|
||||||
|
|
||||||
<a :href="`/metadata/${backup.path.replace(/%/g, '%25').replace(/#/g, '%23')}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
|
<a :href="`/metadata/${$encodeUriPath(backup.path)}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
|
||||||
|
|
||||||
<span class="material-icons text-xl hover:text-error hover:text-opacity-100 text-opacity-70 text-white cursor-pointer mx-1" @click="deleteBackupClick(backup)">delete</span>
|
<span class="material-icons text-xl hover:text-error hover:text-opacity-100 text-opacity-70 text-white cursor-pointer mx-1" @click="deleteBackupClick(backup)">delete</span>
|
||||||
</div>
|
</div>
|
||||||
@ -42,7 +42,7 @@
|
|||||||
<div v-if="selectedBackup" class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
<div v-if="selectedBackup" class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
||||||
<p class="text-error text-lg font-semibold">Important Notice!</p>
|
<p class="text-error text-lg font-semibold">Important Notice!</p>
|
||||||
<p class="text-base py-1">Applying a backup will overwrite users, user progress, book details, settings, and covers stored in metadata with the backed up data.</p>
|
<p class="text-base py-1">Applying a backup will overwrite users, user progress, book details, settings, and covers stored in metadata with the backed up data.</p>
|
||||||
<p class="text-base py-1">Backups <strong>do not</strong> modify any files in your library folders, only data in the audiobookshelf created <span class="font-mono">/config</span> and <span class="font-mono">/metadata</span> directories.</p>
|
<p class="text-base py-1">Backups <strong>do not</strong> modify any files in your library folders, only data in the audiobookshelf created <span class="font-mono">/config</span> and <span class="font-mono">/metadata</span> directories. If you have enabled server settings to store cover art and metadata in your library folders then those are not backup up or overwritten.</p>
|
||||||
<p class="text-base py-1">All clients using your server will be automatically refreshed.</p>
|
<p class="text-base py-1">All clients using your server will be automatically refreshed.</p>
|
||||||
|
|
||||||
<p class="text-lg text-center my-8">Are you sure you want to apply the backup created on {{ selectedBackup.datePretty }}?</p>
|
<p class="text-lg text-center my-8">Are you sure you want to apply the backup created on {{ selectedBackup.datePretty }}?</p>
|
||||||
@ -77,14 +77,24 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
confirm() {
|
confirm() {
|
||||||
this.showConfirmApply = false
|
this.showConfirmApply = false
|
||||||
this.$root.socket.once('apply_backup_complete', this.applyBackupComplete)
|
|
||||||
this.$root.socket.emit('apply_backup', this.selectedBackup.id)
|
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', error)
|
||||||
|
this.$toast.error('Failed to apply backup')
|
||||||
|
})
|
||||||
},
|
},
|
||||||
deleteBackupClick(backup) {
|
deleteBackupClick(backup) {
|
||||||
if (confirm(`Are you sure you want to delete backup for ${backup.datePretty}?`)) {
|
if (confirm(`Are you sure you want to delete backup for ${backup.datePretty}?`)) {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/backup/${backup.id}`)
|
.$delete(`/api/backups/${backup.id}`)
|
||||||
.then((backups) => {
|
.then((backups) => {
|
||||||
console.log('Backup deleted', backups)
|
console.log('Backup deleted', backups)
|
||||||
this.$store.commit('setBackups', backups)
|
this.$store.commit('setBackups', backups)
|
||||||
@ -98,29 +108,24 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
applyBackupComplete(success) {
|
|
||||||
if (success) {
|
|
||||||
// this.$toast.success('Backup Applied, refresh the page')
|
|
||||||
location.replace('/config/backups?backup=1')
|
|
||||||
} else {
|
|
||||||
this.$toast.error('Failed to apply backup')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
applyBackup(backup) {
|
applyBackup(backup) {
|
||||||
this.selectedBackup = backup
|
this.selectedBackup = backup
|
||||||
this.showConfirmApply = true
|
this.showConfirmApply = true
|
||||||
},
|
},
|
||||||
backupComplete(backups) {
|
|
||||||
this.isBackingUp = false
|
|
||||||
if (backups) {
|
|
||||||
this.$toast.success('Backup Successful')
|
|
||||||
this.$store.commit('setBackups', backups)
|
|
||||||
} else this.$toast.error('Backup Failed')
|
|
||||||
},
|
|
||||||
clickCreateBackup() {
|
clickCreateBackup() {
|
||||||
this.isBackingUp = true
|
this.isBackingUp = true
|
||||||
this.$root.socket.once('backup_complete', this.backupComplete)
|
this.$axios
|
||||||
this.$root.socket.emit('create_backup')
|
.$post('/api/backups')
|
||||||
|
.then((backups) => {
|
||||||
|
this.isBackingUp = false
|
||||||
|
this.$toast.success('Backup Successful')
|
||||||
|
this.$store.commit('setBackups', backups)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.isBackingUp = false
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.$toast.error('Backup Failed')
|
||||||
|
})
|
||||||
},
|
},
|
||||||
backupUploaded(file) {
|
backupUploaded(file) {
|
||||||
var form = new FormData()
|
var form = new FormData()
|
||||||
@ -129,7 +134,7 @@ export default {
|
|||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post('/api/backup/upload', form)
|
.$post('/api/backups/upload', form)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
console.log('Upload backup result', result)
|
console.log('Upload backup result', result)
|
||||||
this.$store.commit('setBackups', result)
|
this.$store.commit('setBackups', result)
|
||||||
|
@ -40,8 +40,8 @@
|
|||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-6 truncate">{{ index + 1 }}. </p>
|
<p class="text-sm font-book text-white text-opacity-70 w-6 truncate">{{ index + 1 }}. </p>
|
||||||
<div class="w-56">
|
<div class="w-56">
|
||||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata.title }}</p>
|
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.lastUpdate) }}</p>
|
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div class="w-18 text-right">
|
<div class="w-18 text-right">
|
||||||
|
@ -13,11 +13,12 @@ const Logger = require('./Logger')
|
|||||||
const Backup = require('./objects/Backup')
|
const Backup = require('./objects/Backup')
|
||||||
|
|
||||||
class BackupManager {
|
class BackupManager {
|
||||||
constructor(db) {
|
constructor(db, emitter) {
|
||||||
this.BackupPath = Path.join(global.MetadataPath, 'backups')
|
this.BackupPath = Path.join(global.MetadataPath, 'backups')
|
||||||
this.MetadataBooksPath = Path.join(global.MetadataPath, 'books')
|
this.MetadataBooksPath = Path.join(global.MetadataPath, 'books')
|
||||||
|
|
||||||
this.db = db
|
this.db = db
|
||||||
|
this.emitter = emitter
|
||||||
|
|
||||||
this.scheduleTask = null
|
this.scheduleTask = null
|
||||||
|
|
||||||
@ -104,59 +105,20 @@ class BackupManager {
|
|||||||
return res.json(this.backups.map(b => b.toJSON()))
|
return res.json(this.backups.map(b => b.toJSON()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestCreateBackup(socket) {
|
async requestCreateBackup(res) {
|
||||||
// Only Root User allowed
|
|
||||||
var client = socket.sheepClient
|
|
||||||
if (!client || !client.user) {
|
|
||||||
Logger.error(`[BackupManager] Invalid user attempting to create backup`)
|
|
||||||
socket.emit('backup_complete', false)
|
|
||||||
return
|
|
||||||
} else if (!client.user.isRoot) {
|
|
||||||
Logger.error(`[BackupManager] Non-Root user attempting to create backup`)
|
|
||||||
socket.emit('backup_complete', false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var backupSuccess = await this.runBackup()
|
var backupSuccess = await this.runBackup()
|
||||||
socket.emit('backup_complete', backupSuccess ? this.backups.map(b => b.toJSON()) : false)
|
if (backupSuccess) res.json(this.backups.map(b => b.toJSON()))
|
||||||
|
else res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestApplyBackup(socket, id) {
|
async requestApplyBackup(backup) {
|
||||||
// Only Root User allowed
|
|
||||||
var client = socket.sheepClient
|
|
||||||
if (!client || !client.user) {
|
|
||||||
Logger.error(`[BackupManager] Invalid user attempting to create backup`)
|
|
||||||
socket.emit('apply_backup_complete', false)
|
|
||||||
return
|
|
||||||
} else if (!client.user.isRoot) {
|
|
||||||
Logger.error(`[BackupManager] Non-Root user attempting to create backup`)
|
|
||||||
socket.emit('apply_backup_complete', false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var backup = this.backups.find(b => b.id === id)
|
|
||||||
if (!backup) {
|
|
||||||
socket.emit('apply_backup_complete', false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const zip = new StreamZip.async({ file: backup.fullPath })
|
const zip = new StreamZip.async({ file: backup.fullPath })
|
||||||
await zip.extract('config/', global.ConfigPath)
|
await zip.extract('config/', global.ConfigPath)
|
||||||
if (backup.backupMetadataCovers) {
|
if (backup.backupMetadataCovers) {
|
||||||
await zip.extract('metadata-books/', this.MetadataBooksPath)
|
await zip.extract('metadata-books/', this.MetadataBooksPath)
|
||||||
}
|
}
|
||||||
await this.db.reinit()
|
await this.db.reinit()
|
||||||
socket.emit('apply_backup_complete', true)
|
this.emitter('backup_applied')
|
||||||
socket.broadcast.emit('backup_applied')
|
|
||||||
}
|
|
||||||
|
|
||||||
async setLastBackup() {
|
|
||||||
this.backups.sort((a, b) => b.createdAt - a.createdAt)
|
|
||||||
var lastBackup = this.backups.shift()
|
|
||||||
|
|
||||||
const zip = new StreamZip.async({ file: lastBackup.fullPath })
|
|
||||||
await zip.extract('config/', global.ConfigPath)
|
|
||||||
console.log('Set Last Backup')
|
|
||||||
await this.db.reinit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBackups() {
|
async loadBackups() {
|
||||||
@ -179,7 +141,6 @@ class BackupManager {
|
|||||||
this.backups.push(backup)
|
this.backups.push(backup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Logger.debug(`[BackupManager] Backup found "${backup.id}"`)
|
Logger.debug(`[BackupManager] Backup found "${backup.id}"`)
|
||||||
zip.close()
|
zip.close()
|
||||||
}
|
}
|
||||||
@ -304,12 +265,14 @@ class BackupManager {
|
|||||||
// pipe archive data to the file
|
// pipe archive data to the file
|
||||||
archive.pipe(output)
|
archive.pipe(output)
|
||||||
|
|
||||||
archive.directory(this.db.AudiobooksPath, 'config/audiobooks')
|
archive.directory(this.db.LibraryItemsPath, 'config/libraryItems')
|
||||||
archive.directory(this.db.LibrariesPath, 'config/libraries')
|
|
||||||
archive.directory(this.db.SettingsPath, 'config/settings')
|
|
||||||
archive.directory(this.db.UsersPath, 'config/users')
|
archive.directory(this.db.UsersPath, 'config/users')
|
||||||
archive.directory(this.db.SessionsPath, 'config/sessions')
|
archive.directory(this.db.SessionsPath, 'config/sessions')
|
||||||
|
archive.directory(this.db.LibrariesPath, 'config/libraries')
|
||||||
|
archive.directory(this.db.SettingsPath, 'config/settings')
|
||||||
archive.directory(this.db.CollectionsPath, 'config/collections')
|
archive.directory(this.db.CollectionsPath, 'config/collections')
|
||||||
|
archive.directory(this.db.AuthorsPath, 'config/authors')
|
||||||
|
archive.directory(this.db.SeriesPath, 'config/series')
|
||||||
|
|
||||||
if (metadataBooksPath) {
|
if (metadataBooksPath) {
|
||||||
Logger.debug(`[BackupManager] Backing up Metadata Books "${metadataBooksPath}"`)
|
Logger.debug(`[BackupManager] Backing up Metadata Books "${metadataBooksPath}"`)
|
||||||
|
@ -49,7 +49,7 @@ class Server {
|
|||||||
|
|
||||||
this.db = new Db()
|
this.db = new Db()
|
||||||
this.auth = new Auth(this.db)
|
this.auth = new Auth(this.db)
|
||||||
this.backupManager = new BackupManager(this.db)
|
this.backupManager = new BackupManager(this.db, this.emitter.bind(this))
|
||||||
this.logManager = new LogManager(this.db)
|
this.logManager = new LogManager(this.db)
|
||||||
this.cacheManager = new CacheManager()
|
this.cacheManager = new CacheManager()
|
||||||
this.watcher = new Watcher()
|
this.watcher = new Watcher()
|
||||||
@ -158,9 +158,11 @@ class Server {
|
|||||||
const distPath = Path.join(global.appRoot, '/client/dist')
|
const distPath = Path.join(global.appRoot, '/client/dist')
|
||||||
app.use(express.static(distPath))
|
app.use(express.static(distPath))
|
||||||
|
|
||||||
// TODO: Are these necessary?
|
|
||||||
// Metadata folder static path
|
// Metadata folder static path
|
||||||
// app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
||||||
|
|
||||||
|
// TODO: Are these necessary?
|
||||||
// Downloads folder static path
|
// Downloads folder static path
|
||||||
// app.use('/downloads', this.authMiddleware.bind(this), express.static(this.downloadManager.downloadDirPath))
|
// app.use('/downloads', this.authMiddleware.bind(this), express.static(this.downloadManager.downloadDirPath))
|
||||||
// Static folder
|
// Static folder
|
||||||
@ -222,9 +224,6 @@ class Server {
|
|||||||
|
|
||||||
socket.on('auth', (token) => this.authenticateSocket(socket, token))
|
socket.on('auth', (token) => this.authenticateSocket(socket, token))
|
||||||
|
|
||||||
// TODO: Most of these web socket listeners will be moved to API routes instead
|
|
||||||
// with the goal of the web socket connection being a nice-to-have not need-to-have
|
|
||||||
|
|
||||||
// Scanning
|
// Scanning
|
||||||
socket.on('cancel_scan', this.cancelScan.bind(this))
|
socket.on('cancel_scan', this.cancelScan.bind(this))
|
||||||
socket.on('save_metadata', (libraryItemId) => this.saveMetadata(socket, libraryItemId))
|
socket.on('save_metadata', (libraryItemId) => this.saveMetadata(socket, libraryItemId))
|
||||||
@ -237,10 +236,6 @@ class Server {
|
|||||||
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
|
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
|
||||||
socket.on('fetch_daily_logs', () => this.logManager.socketRequestDailyLogs(socket))
|
socket.on('fetch_daily_logs', () => this.logManager.socketRequestDailyLogs(socket))
|
||||||
|
|
||||||
// Backups
|
|
||||||
socket.on('create_backup', () => this.backupManager.requestCreateBackup(socket))
|
|
||||||
socket.on('apply_backup', (id) => this.backupManager.requestApplyBackup(socket, id))
|
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
Logger.removeSocketListener(socket.id)
|
Logger.removeSocketListener(socket.id)
|
||||||
|
|
||||||
|
@ -3,6 +3,14 @@ const Logger = require('../Logger')
|
|||||||
class BackupController {
|
class BackupController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
async create(req, res) {
|
||||||
|
if (!req.user.isRoot) {
|
||||||
|
Logger.error(`[BackupController] Non-Root user attempting to craete backup`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
this.backupManager.requestCreateBackup(res)
|
||||||
|
}
|
||||||
|
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isRoot) {
|
||||||
Logger.error(`[BackupController] Non-Root user attempting to delete backup`, req.user)
|
Logger.error(`[BackupController] Non-Root user attempting to delete backup`, req.user)
|
||||||
@ -27,5 +35,18 @@ class BackupController {
|
|||||||
}
|
}
|
||||||
this.backupManager.uploadBackup(req, res)
|
this.backupManager.uploadBackup(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async apply(req, res) {
|
||||||
|
if (!req.user.isRoot) {
|
||||||
|
Logger.error(`[BackupController] Non-Root user attempting to apply backup`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
|
||||||
|
if (!backup) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
await this.backupManager.requestApplyBackup(backup)
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new BackupController()
|
module.exports = new BackupController()
|
@ -142,8 +142,10 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
// Backup Routes
|
// Backup Routes
|
||||||
//
|
//
|
||||||
this.router.delete('/backup/:id', BackupController.delete.bind(this))
|
this.router.post('/backups', BackupController.create.bind(this))
|
||||||
this.router.post('/backup/upload', BackupController.upload.bind(this))
|
this.router.delete('/backups/:id', BackupController.delete.bind(this))
|
||||||
|
this.router.get('/backups/:id/apply', BackupController.apply.bind(this))
|
||||||
|
this.router.post('/backups/upload', BackupController.upload.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// File System Routes
|
// File System Routes
|
||||||
|
@ -373,7 +373,7 @@ function cleanSessionObj(db, userListeningSession) {
|
|||||||
bookMetadata.title = userListeningSession.audiobookTitle || ''
|
bookMetadata.title = userListeningSession.audiobookTitle || ''
|
||||||
newPlaybackSession.mediaMetadata = bookMetadata
|
newPlaybackSession.mediaMetadata = bookMetadata
|
||||||
|
|
||||||
return db.sessionsDb.update((record) => record.id === newPlaybackSession.id, () => newPlaybackSession).then((results) => true).catch((error) => {
|
return db.sessionsDb.update((record) => record.id === userListeningSession.id, () => newPlaybackSession).then((results) => true).catch((error) => {
|
||||||
Logger.error(`[dbMigration] Update Session Failed: ${error}`)
|
Logger.error(`[dbMigration] Update Session Failed: ${error}`)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user