mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge branch 'advplyr:master' into master
This commit is contained in:
		
						commit
						4ad09ec3d8
					
				| @ -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') | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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: [ | ||||
|  | ||||
							
								
								
									
										6
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -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 @@ | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| } | ||||
| @ -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" | ||||
|   } | ||||
| } | ||||
| } | ||||
| @ -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
									
								
							
							
						
						
									
										2
									
								
								client/static/robots.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| User-Agent: *  | ||||
| Disallow: /  | ||||
							
								
								
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -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", | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -217,7 +217,6 @@ class Database { | ||||
|   async disconnect() { | ||||
|     Logger.info(`[Database] Disconnecting sqlite db`) | ||||
|     await this.sequelize.close() | ||||
|     this.sequelize = null | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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 } | ||||
|  | ||||
| @ -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') | ||||
|   } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user