mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01: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 |