@empathize/framework
Version:
Framework for Neutralino
391 lines (314 loc) • 11.6 kB
text/typescript
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
};