UNPKG

@empathize/framework

Version:

Framework for Neutralino

391 lines (314 loc) 11.6 kB
import promisify from '../async/promisify.js'; import path from '../paths/path.js'; import { DebugThread } from '../meta/Debug.js'; type ArchiveType = 'tar' | 'zip' | '7z'; type Size = { compressed?: number | null; uncompressed?: number | null; }; type File = { path: string; size: Size; }; type ArchiveInfo = { size: Size; type: ArchiveType; files: File[]; }; declare const Neutralino; declare const NL_CWD; class Stream { protected _id: number = -1; /** * ID of the archive unpacker process */ public get id(): number { return this._id; } /** * The interval in ms between progress event calls */ public progressInterval: number = 500; protected path: string; protected unpackDir: string|null; protected unpacked: number = 0; protected archive?: ArchiveInfo; protected onStart?: () => void; protected onProgress?: (current: number, total: number, difference: number) => void; protected onFinish?: () => void; protected onError?: () => void; protected started: boolean = false; protected finished: boolean = false; protected throwedError: boolean = false; /** * @param archive path to archive * @param unpackDir directory to extract the files to */ public constructor(archive: string, unpackDir: string|null = null) { this.path = archive; this.unpackDir = unpackDir; this.started = true; const debugThread = new DebugThread('Archive/Stream', { message: { 'path': archive, 'unpack dir': unpackDir } }); if (this.onStart) this.onStart(); Archive.getInfo(archive).then((info) => { if (info === null) { this.throwedError = true; if (this.onError) this.onError(); } else { this.archive = info; let command = { tar: `tar -xvf "${path.addSlashes(archive)}"${unpackDir ? ` -C "${path.addSlashes(unpackDir)}"` : ''}`, zip: `unzip -o "${path.addSlashes(archive)}"${unpackDir ? ` -d "${path.addSlashes(unpackDir)}"` : ''}`, '7z': `7z x "${path.addSlashes(archive)}"${unpackDir ? ` -o"${path.addSlashes(unpackDir)}"` : ''} -aoa` }[this.archive.type!]; if (unpackDir) command = `mkdir -p "${path.addSlashes(unpackDir)}" && ${command}`; let remainedFiles = this.archive.files; const baseDir = unpackDir ?? NL_CWD; Neutralino.os.execCommand(command, { background: true }).then((result) => { this._id = result.pid; }); debugThread.log(`Unpacking started with command: ${command}`); const updateProgress = async () => { let difference: number = 0; let pool: any[] = []; remainedFiles.forEach((file) => { if (file.path != '#unpacked#') { pool.push((): Promise<void> => { return new Promise((resolve) => { Neutralino.filesystem.getStats(`${baseDir}/${file.path}`) .then(() => { this.unpacked += file.size.uncompressed!; difference += file.size.uncompressed!; file.path = '#unpacked#'; resolve(); }) .catch(() => resolve()) }); }); } }); await promisify({ callbacks: pool, callAtOnce: true, interval: 200 }); remainedFiles = remainedFiles.filter((file) => file.path != '#unpacked#'); if (this.onProgress) this.onProgress(this.unpacked, this.archive!.size.uncompressed!, difference); if (this.unpacked >= this.archive!.size.uncompressed!) { this.finished = true; debugThread.log('Unpacking finished'); if (this.onFinish) this.onFinish(); } if (!this.finished) setTimeout(updateProgress, this.progressInterval); }; setTimeout(updateProgress, this.progressInterval); } }); } /** * Specify event that will be called when the extraction has started * * @param callback */ public start(callback: () => void) { this.onStart = callback; if (this.started) callback(); } /** * Specify event that will be called every [this.progressInterval] ms while extracting the archive * * @param callback */ public progress(callback: (current: number, total: number, difference: number) => void) { this.onProgress = callback; } /** * Specify event that will be called after the archive has been extracted * * @param callback */ public finish(callback: () => void) { this.onFinish = callback; if (this.finished) callback(); } /** * Specify event that will be called if archive can't be extracted * * @param callback */ public error(callback: () => void) { this.onError = callback; if (this.throwedError) callback(); } /** * Close unpacking stream */ public close(forced: boolean = false) { Neutralino.os.execCommand(`kill ${forced ? '-9' : '-15'} ${this._id}`); } } export default class Archive { protected static streams: Stream[] = []; /** * Get type of archive * * @param path path to archive * @returns supported archive type or null */ public static getType(path: string): ArchiveType|null { if (path.substring(path.length - 4) == '.zip') return 'zip'; else if (path.substring(path.length - 7, path.length - 2) == '.tar.') return 'tar'; else if (path.substring(path.length - 3) == '.7z') return '7z'; else return null; } /** * Get archive info * * @param path path to archive * @returns null if the archive has unsupported type. Otherwise - archive info */ public static getInfo(path: string): Promise<ArchiveInfo|null> { const debugThread = new DebugThread('Archive.getInfo', `Getting info about archive: ${path}`); return new Promise(async (resolve) => { const archiveType = this.getType(path); if (archiveType === null) resolve(null); else { let archive: ArchiveInfo = { type: archiveType, size: { compressed: null, uncompressed: null }, files: [] }; switch (archive.type) { case 'tar': const tarOutput = await Neutralino.os.execCommand(`tar -tvf "${path}"`); for (const match of tarOutput.stdOut.matchAll(/^[dwxr\-]+ [\w/]+[ ]+(\d+) [0-9\-]+ [0-9\:]+ (.+)/gm)) { const fileSize = parseInt(match[1]); archive.size.uncompressed! += fileSize; archive.files.push({ path: match[2], size: { compressed: null, uncompressed: fileSize } }); } break; case 'zip': const zipOutput = await Neutralino.os.execCommand(`unzip -v "${path}"`); for (const match of zipOutput.stdOut.matchAll(/^(\d+) [a-zA-Z\:]+[ ]+(\d+)[ ]+[0-9\-]+% [0-9\-]+ [0-9\:]+ [a-f0-9]{8} (.+)/gm)) { const uncompressedSize = parseInt(match[1]), compressedSize = parseInt(match[2]); archive.size.compressed! += compressedSize; archive.size.uncompressed! += uncompressedSize; archive.files.push({ path: match[3], size: { compressed: compressedSize, uncompressed: uncompressedSize } }); } break; case '7z': const output = (await Neutralino.os.execCommand(`7z l "${path}"`)) .stdOut.split('-------------------').slice(1, -1).join('-------------------'); for (const match of output.matchAll(/^[\d]+-[\d]+-[\d]+ [\d]+:[\d]+:[\d]+[a-zA-Z\. ]+([\d ]+)[ ]+([\d ]+)[ ]+(.+)/gm)) { const fileSize = parseInt(match[1].trim()); archive.size.uncompressed! += fileSize; archive.files.push({ path: match[3], size: { compressed: null, uncompressed: fileSize } }); } break; } debugThread.log({ message: { 'type': archive.type, 'compressed size': archive.size.compressed, 'uncompressed size': archive.size.uncompressed, 'files amount': archive.files.length } }); resolve(archive); } }); } /** * Extract Archive * * @param path path to archive * @param unpackDir directory to extract the files to */ public static extract(path: string, unpackDir: string|null = null): Promise<Stream> { return new Promise((resolve) => { const stream = new Stream(path, unpackDir); this.streams.push(stream); resolve(stream); }); } /** * Close every open archive extracting stream */ public static closeStreams(forced: boolean = false) { this.streams.forEach((stream) => { stream.close(forced); }); } }; export { Stream }; export type { ArchiveType, File, Size, ArchiveInfo };