mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 18:37:00 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			262 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			262 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * Modified from https://github.com/nika-begiashvili/libarchivejs
 | |
|  */
 | |
| 
 | |
| const Path = require('path')
 | |
| const { Worker } = require('worker_threads')
 | |
| 
 | |
| /**
 | |
|  * Represents compressed file before extraction
 | |
|  */
 | |
| class CompressedFile {
 | |
| 
 | |
|     constructor(name, size, path, archiveRef) {
 | |
|         this._name = name
 | |
|         this._size = size
 | |
|         this._path = path
 | |
|         this._archiveRef = archiveRef
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * file name
 | |
|      */
 | |
|     get name() {
 | |
|         return this._name
 | |
|     }
 | |
|     /**
 | |
|      * file size
 | |
|      */
 | |
|     get size() {
 | |
|         return this._size
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Extract file from archive
 | |
|      * @returns {Promise<File>} extracted file
 | |
|      */
 | |
|     extract() {
 | |
|         return this._archiveRef.extractSingleFile(this._path)
 | |
|     }
 | |
| 
 | |
| }
 | |
| 
 | |
| class Archive {
 | |
|     /**
 | |
|      * Creates new archive instance from browser native File object
 | |
|      * @param {Buffer} fileBuffer
 | |
|      * @param {object} options
 | |
|      * @returns {Archive}
 | |
|      */
 | |
|     static open(fileBuffer) {
 | |
|         const arch = new Archive(fileBuffer, { workerUrl: Path.join(__dirname, 'libarchiveWorker.js') })
 | |
|         return arch.open()
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create new archive
 | |
|      * @param {File} file 
 | |
|      * @param {Object} options 
 | |
|      */
 | |
|     constructor(file, options) {
 | |
|         this._worker = new Worker(options.workerUrl)
 | |
|         this._worker.on('message', this._workerMsg.bind(this))
 | |
| 
 | |
|         this._callbacks = []
 | |
|         this._content = {}
 | |
|         this._processed = 0
 | |
|         this._file = file
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Prepares file for reading
 | |
|      * @returns {Promise<Archive>} archive instance
 | |
|      */
 | |
|     async open() {
 | |
|         await this._postMessage({ type: 'HELLO' }, (resolve, reject, msg) => {
 | |
|             if (msg.type === 'READY') {
 | |
|                 resolve()
 | |
|             }
 | |
|         })
 | |
|         return await this._postMessage({ type: 'OPEN', file: this._file }, (resolve, reject, msg) => {
 | |
|             if (msg.type === 'OPENED') {
 | |
|                 resolve(this)
 | |
|             }
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Terminate worker to free up memory
 | |
|      */
 | |
|     close() {
 | |
|         this._worker.terminate()
 | |
|         this._worker = null
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * detect if archive has encrypted data
 | |
|      * @returns {boolean|null} null if could not be determined
 | |
|      */
 | |
|     hasEncryptedData() {
 | |
|         return this._postMessage({ type: 'CHECK_ENCRYPTION' },
 | |
|             (resolve, reject, msg) => {
 | |
|                 if (msg.type === 'ENCRYPTION_STATUS') {
 | |
|                     resolve(msg.status)
 | |
|                 }
 | |
|             }
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * set password to be used when reading archive
 | |
|      */
 | |
|     usePassword(archivePassword) {
 | |
|         return this._postMessage({ type: 'SET_PASSPHRASE', passphrase: archivePassword },
 | |
|             (resolve, reject, msg) => {
 | |
|                 if (msg.type === 'PASSPHRASE_STATUS') {
 | |
|                     resolve(msg.status)
 | |
|                 }
 | |
|             }
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns object containing directory structure and file information 
 | |
|      * @returns {Promise<object>}
 | |
|      */
 | |
|     getFilesObject() {
 | |
|         if (this._processed > 0) {
 | |
|             return Promise.resolve().then(() => this._content)
 | |
|         }
 | |
|         return this._postMessage({ type: 'LIST_FILES' }, (resolve, reject, msg) => {
 | |
|             if (msg.type === 'ENTRY') {
 | |
|                 const entry = msg.entry
 | |
|                 const [target, prop] = this._getProp(this._content, entry.path)
 | |
|                 if (entry.type === 'FILE') {
 | |
|                     target[prop] = new CompressedFile(entry.fileName, entry.size, entry.path, this)
 | |
|                 }
 | |
|                 return true
 | |
|             } else if (msg.type === 'END') {
 | |
|                 this._processed = 1
 | |
|                 resolve(this._cloneContent(this._content))
 | |
|             }
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     getFilesArray() {
 | |
|         return this.getFilesObject().then((obj) => {
 | |
|             return this._objectToArray(obj)
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     extractSingleFile(target) {
 | |
|         // Prevent extraction if worker already terminated
 | |
|         if (this._worker === null) {
 | |
|             throw new Error("Archive already closed")
 | |
|         }
 | |
| 
 | |
|         return this._postMessage({ type: 'EXTRACT_SINGLE_FILE', target: target },
 | |
|             (resolve, reject, msg) => {
 | |
|                 if (msg.type === 'FILE') {
 | |
|                     resolve(msg.entry)
 | |
|                 }
 | |
|             }
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns object containing directory structure and extracted File objects 
 | |
|      * @param {Function} extractCallback
 | |
|      * 
 | |
|      */
 | |
|     extractFiles(extractCallback) {
 | |
|         if (this._processed > 1) {
 | |
|             return Promise.resolve().then(() => this._content)
 | |
|         }
 | |
|         return this._postMessage({ type: 'EXTRACT_FILES' }, (resolve, reject, msg) => {
 | |
|             if (msg.type === 'ENTRY') {
 | |
|                 const [target, prop] = this._getProp(this._content, msg.entry.path)
 | |
|                 if (msg.entry.type === 'FILE') {
 | |
|                     target[prop] = msg.entry
 | |
|                     if (extractCallback !== undefined) {
 | |
|                         setTimeout(extractCallback.bind(null, {
 | |
|                             file: target[prop],
 | |
|                             path: msg.entry.path,
 | |
|                         }))
 | |
|                     }
 | |
|                 }
 | |
|                 return true
 | |
|             } else if (msg.type === 'END') {
 | |
|                 this._processed = 2
 | |
|                 this._worker.terminate()
 | |
|                 resolve(this._cloneContent(this._content))
 | |
|             }
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     _cloneContent(obj) {
 | |
|         if (obj instanceof CompressedFile || obj === null) return obj
 | |
|         const o = {}
 | |
|         for (const prop of Object.keys(obj)) {
 | |
|             o[prop] = this._cloneContent(obj[prop])
 | |
|         }
 | |
|         return o
 | |
|     }
 | |
| 
 | |
|     _objectToArray(obj, path = '') {
 | |
|         const files = []
 | |
|         for (const key of Object.keys(obj)) {
 | |
|             if (obj[key] instanceof CompressedFile || obj[key] === null) {
 | |
|                 files.push({
 | |
|                     file: obj[key] || key,
 | |
|                     path: path
 | |
|                 })
 | |
|             } else {
 | |
|                 files.push(...this._objectToArray(obj[key], `${path}${key}/`))
 | |
|             }
 | |
|         }
 | |
|         return files
 | |
|     }
 | |
| 
 | |
|     _getProp(obj, path) {
 | |
|         const parts = path.split('/')
 | |
|         if (parts[parts.length - 1] === '') parts.pop()
 | |
|         let cur = obj, prev = null
 | |
|         for (const part of parts) {
 | |
|             cur[part] = cur[part] || {}
 | |
|             prev = cur
 | |
|             cur = cur[part]
 | |
|         }
 | |
|         return [prev, parts[parts.length - 1]]
 | |
|     }
 | |
| 
 | |
|     _postMessage(msg, callback) {
 | |
|         this._worker.postMessage(msg)
 | |
|         return new Promise((resolve, reject) => {
 | |
|             this._callbacks.push(this._msgHandler.bind(this, callback, resolve, reject))
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     _msgHandler(callback, resolve, reject, msg) {
 | |
|         if (!msg) {
 | |
|             reject('invalid msg')
 | |
|             return
 | |
|         }
 | |
|         if (msg.type === 'BUSY') {
 | |
|             reject('worker is busy')
 | |
|         } else if (msg.type === 'ERROR') {
 | |
|             reject(msg.error)
 | |
|         } else {
 | |
|             return callback(resolve, reject, msg)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     _workerMsg(msg) {
 | |
|         const callback = this._callbacks[this._callbacks.length - 1]
 | |
|         const next = callback(msg)
 | |
|         if (!next) {
 | |
|             this._callbacks.pop()
 | |
|         }
 | |
|     }
 | |
| 
 | |
| }
 | |
| module.exports = Archive |