mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +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"> | ||||
|               <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> | ||||
|             </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"> | ||||
|         <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">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-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: { | ||||
|     confirm() { | ||||
|       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) { | ||||
|       if (confirm(`Are you sure you want to delete backup for ${backup.datePretty}?`)) { | ||||
|         this.processing = true | ||||
|         this.$axios | ||||
|           .$delete(`/api/backup/${backup.id}`) | ||||
|           .$delete(`/api/backups/${backup.id}`) | ||||
|           .then((backups) => { | ||||
|             console.log('Backup deleted', 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) { | ||||
|       this.selectedBackup = backup | ||||
|       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() { | ||||
|       this.isBackingUp = true | ||||
|       this.$root.socket.once('backup_complete', this.backupComplete) | ||||
|       this.$root.socket.emit('create_backup') | ||||
|       this.$axios | ||||
|         .$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) { | ||||
|       var form = new FormData() | ||||
| @ -129,7 +134,7 @@ export default { | ||||
|       this.processing = true | ||||
| 
 | ||||
|       this.$axios | ||||
|         .$post('/api/backup/upload', form) | ||||
|         .$post('/api/backups/upload', form) | ||||
|         .then((result) => { | ||||
|           console.log('Upload backup result', result) | ||||
|           this.$store.commit('setBackups', result) | ||||
|  | ||||
| @ -40,8 +40,8 @@ | ||||
|             <div class="flex items-center mb-1"> | ||||
|               <p class="text-sm font-book text-white text-opacity-70 w-6 truncate">{{ index + 1 }}. </p> | ||||
|               <div class="w-56"> | ||||
|                 <p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata.title }}</p> | ||||
|                 <p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.lastUpdate) }}</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.updatedAt) }}</p> | ||||
|               </div> | ||||
|               <div class="flex-grow" /> | ||||
|               <div class="w-18 text-right"> | ||||
|  | ||||
| @ -13,11 +13,12 @@ const Logger = require('./Logger') | ||||
| const Backup = require('./objects/Backup') | ||||
| 
 | ||||
| class BackupManager { | ||||
|   constructor(db) { | ||||
|   constructor(db, emitter) { | ||||
|     this.BackupPath = Path.join(global.MetadataPath, 'backups') | ||||
|     this.MetadataBooksPath = Path.join(global.MetadataPath, 'books') | ||||
| 
 | ||||
|     this.db = db | ||||
|     this.emitter = emitter | ||||
| 
 | ||||
|     this.scheduleTask = null | ||||
| 
 | ||||
| @ -104,59 +105,20 @@ class BackupManager { | ||||
|     return res.json(this.backups.map(b => b.toJSON())) | ||||
|   } | ||||
| 
 | ||||
|   async requestCreateBackup(socket) { | ||||
|     // 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 | ||||
|     } | ||||
| 
 | ||||
|   async requestCreateBackup(res) { | ||||
|     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) { | ||||
|     // 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 | ||||
|     } | ||||
|   async requestApplyBackup(backup) { | ||||
|     const zip = new StreamZip.async({ file: backup.fullPath }) | ||||
|     await zip.extract('config/', global.ConfigPath) | ||||
|     if (backup.backupMetadataCovers) { | ||||
|       await zip.extract('metadata-books/', this.MetadataBooksPath) | ||||
|     } | ||||
|     await this.db.reinit() | ||||
|     socket.emit('apply_backup_complete', true) | ||||
|     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() | ||||
|     this.emitter('backup_applied') | ||||
|   } | ||||
| 
 | ||||
|   async loadBackups() { | ||||
| @ -179,7 +141,6 @@ class BackupManager { | ||||
|             this.backups.push(backup) | ||||
|           } | ||||
| 
 | ||||
| 
 | ||||
|           Logger.debug(`[BackupManager] Backup found "${backup.id}"`) | ||||
|           zip.close() | ||||
|         } | ||||
| @ -304,12 +265,14 @@ class BackupManager { | ||||
|       // pipe archive data to the file
 | ||||
|       archive.pipe(output) | ||||
| 
 | ||||
|       archive.directory(this.db.AudiobooksPath, 'config/audiobooks') | ||||
|       archive.directory(this.db.LibrariesPath, 'config/libraries') | ||||
|       archive.directory(this.db.SettingsPath, 'config/settings') | ||||
|       archive.directory(this.db.LibraryItemsPath, 'config/libraryItems') | ||||
|       archive.directory(this.db.UsersPath, 'config/users') | ||||
|       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.AuthorsPath, 'config/authors') | ||||
|       archive.directory(this.db.SeriesPath, 'config/series') | ||||
| 
 | ||||
|       if (metadataBooksPath) { | ||||
|         Logger.debug(`[BackupManager] Backing up Metadata Books "${metadataBooksPath}"`) | ||||
|  | ||||
| @ -49,7 +49,7 @@ class Server { | ||||
| 
 | ||||
|     this.db = new 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.cacheManager = new CacheManager() | ||||
|     this.watcher = new Watcher() | ||||
| @ -158,9 +158,11 @@ class Server { | ||||
|     const distPath = Path.join(global.appRoot, '/client/dist') | ||||
|     app.use(express.static(distPath)) | ||||
| 
 | ||||
|     // TODO: Are these necessary?
 | ||||
| 
 | ||||
|     // 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
 | ||||
|     // app.use('/downloads', this.authMiddleware.bind(this), express.static(this.downloadManager.downloadDirPath))
 | ||||
|     // Static folder
 | ||||
| @ -222,9 +224,6 @@ class Server { | ||||
| 
 | ||||
|       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
 | ||||
|       socket.on('cancel_scan', this.cancelScan.bind(this)) | ||||
|       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('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', () => { | ||||
|         Logger.removeSocketListener(socket.id) | ||||
| 
 | ||||
|  | ||||
| @ -3,6 +3,14 @@ const Logger = require('../Logger') | ||||
| class BackupController { | ||||
|   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) { | ||||
|     if (!req.user.isRoot) { | ||||
|       Logger.error(`[BackupController] Non-Root user attempting to delete backup`, req.user) | ||||
| @ -27,5 +35,18 @@ class BackupController { | ||||
|     } | ||||
|     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() | ||||
| @ -142,8 +142,10 @@ class ApiRouter { | ||||
|     //
 | ||||
|     // Backup Routes
 | ||||
|     //
 | ||||
|     this.router.delete('/backup/:id', BackupController.delete.bind(this)) | ||||
|     this.router.post('/backup/upload', BackupController.upload.bind(this)) | ||||
|     this.router.post('/backups', BackupController.create.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
 | ||||
|  | ||||
| @ -373,7 +373,7 @@ function cleanSessionObj(db, userListeningSession) { | ||||
|   bookMetadata.title = userListeningSession.audiobookTitle || '' | ||||
|   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}`) | ||||
|     return false | ||||
|   }) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user