439 lines
12 KiB
TypeScript
439 lines
12 KiB
TypeScript
//
|
|
// None of the following code was written for mson-three explicity.
|
|
// attribution is provided where necessary.
|
|
//
|
|
// all of this junk was copied from abandoned npm modules which mimic browser
|
|
// APIs in Node.js in order to make GLTFExporter work.
|
|
//
|
|
// this code is taken from npm package "vblob" by karikera
|
|
|
|
import * as fs from 'fs';
|
|
import { randomBytes } from 'crypto';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
import { Blob as NodeBlob } from 'buffer';
|
|
import { EventTarget } from './eventtarget';
|
|
|
|
interface EventBeforeDispatch {
|
|
[key: string]: any;
|
|
type: string;
|
|
}
|
|
|
|
interface Event {
|
|
readonly cancelable: boolean;
|
|
readonly defaultPrevented: boolean;
|
|
readonly isTrusted: boolean;
|
|
readonly target: EventTarget | null;
|
|
readonly timeStamp: number;
|
|
readonly bubbles: boolean;
|
|
/** @deprecated */
|
|
cancelBubble: boolean;
|
|
readonly composed: boolean;
|
|
readonly currentTarget: EventTarget | null;
|
|
readonly eventPhase: number;
|
|
/** @deprecated */
|
|
returnValue: boolean;
|
|
/** @deprecated */
|
|
readonly srcElement: EventTarget | null;
|
|
readonly type: string;
|
|
|
|
preventDefault(): void;
|
|
stopPropagation(): void;
|
|
stopImmediatePropagation(): void;
|
|
composedPath(): EventTarget[];
|
|
/** @deprecated */
|
|
initEvent(type: string, bubbles: boolean, cancelable: boolean): void;
|
|
|
|
readonly NONE: number;
|
|
readonly CAPTURING_PHASE: number;
|
|
readonly AT_TARGET: number;
|
|
readonly BUBBLING_PHASE: number;
|
|
}
|
|
|
|
interface ProgressEvent<T extends EventTarget = EventTarget> extends Event {
|
|
readonly lengthComputable: boolean;
|
|
readonly loaded: number;
|
|
readonly target: T | null;
|
|
readonly total: number;
|
|
}
|
|
|
|
interface EventTargetConstructor {
|
|
new (): EventTarget;
|
|
}
|
|
|
|
interface FileReaderEvent extends Event {}
|
|
|
|
function getTempPath(): Promise<string> {
|
|
const file = `vblob-${randomBytes(4).readUInt32LE(0)}`;
|
|
const path = join(tmpdir(), file);
|
|
tempFiles.add(path);
|
|
return Promise.resolve(path);
|
|
}
|
|
|
|
function fdopen(path: string, flags: string): Promise<number> {
|
|
return new Promise<number>((resolve, reject) =>
|
|
fs.open(path, flags, (err, fd) => {
|
|
if (err) reject(err);
|
|
else resolve(fd);
|
|
}),
|
|
);
|
|
}
|
|
|
|
function fdclose(fd: number): Promise<void> {
|
|
return new Promise((resolve, reject) =>
|
|
fs.close(fd, (err) => {
|
|
if (err) reject(err);
|
|
else resolve();
|
|
}),
|
|
);
|
|
}
|
|
|
|
function fdwriteFile(fd: number, path: string): Promise<void> {
|
|
return new Promise<void>((resolve, reject) => {
|
|
const writer = fs.createWriteStream(<any>null, { fd });
|
|
const reader = fs.createReadStream(path);
|
|
reader.on('error', reject);
|
|
reader.on('end', resolve);
|
|
writer.on('error', reject);
|
|
reader.pipe(writer, { end: false });
|
|
});
|
|
}
|
|
|
|
function fdwrite(fd: number, str: string | Uint8Array): Promise<void> {
|
|
return new Promise((resolve, reject) =>
|
|
fs.write(fd, str as any, (err) => {
|
|
if (err) reject(err);
|
|
else resolve();
|
|
}),
|
|
);
|
|
}
|
|
|
|
function fdread(fd: number, size: number, position: number): Promise<Buffer> {
|
|
const buffer = Buffer.alloc(size);
|
|
return new Promise<Buffer>((resolve, reject) =>
|
|
fs.read(fd, buffer, 0, size, position, (err) => {
|
|
if (err) reject(err);
|
|
else resolve(buffer);
|
|
}),
|
|
);
|
|
}
|
|
|
|
const tempFiles: Set<string> = new Set();
|
|
|
|
const onExit: Array<() => void> = [];
|
|
|
|
process.on('exit', (code) => {
|
|
for (const cb of onExit) cb();
|
|
process.exit(code);
|
|
});
|
|
|
|
onExit.push(() => {
|
|
for (const file of tempFiles) {
|
|
fs.unlinkSync(file);
|
|
}
|
|
});
|
|
|
|
interface BlobPropertyBag {
|
|
type?: string;
|
|
ending?: 'transparent' | 'native';
|
|
}
|
|
|
|
export interface Blob {
|
|
readonly size: number;
|
|
readonly type: string;
|
|
slice(start?: number, end?: number, contentType?: string): Blob;
|
|
}
|
|
|
|
export interface FileReader extends EventTarget {
|
|
readonly error: any | null;
|
|
readonly readyState: number;
|
|
readonly result: any;
|
|
readonly EMPTY: number;
|
|
readonly LOADING: number;
|
|
readonly DONE: number;
|
|
|
|
onabort: ((ev: ProgressEvent<FileReader>) => void) | null;
|
|
onerror: ((ev: FileReaderEvent) => void) | null;
|
|
onload: ((ev: FileReaderEvent) => void) | null;
|
|
onloadstart: ((ev: FileReaderEvent) => void) | null;
|
|
onloadend: ((ev: FileReaderEvent) => void) | null;
|
|
onprogress: ((ev: FileReaderEvent) => void) | null;
|
|
|
|
abort(): void;
|
|
readAsArrayBuffer(blob: Blob): void;
|
|
readAsBinaryString(blob: Blob): void;
|
|
readAsDataURL(blob: Blob): void;
|
|
readAsText(blob: Blob): void;
|
|
}
|
|
|
|
export class VBlob implements Blob {
|
|
_path: string = '';
|
|
_size: number;
|
|
_offset: number = 0;
|
|
_type: string;
|
|
_writeTask: Promise<number> = Promise.resolve(0);
|
|
|
|
private _write(fn: (fd: number) => void | Promise<void>): void {
|
|
this._writeTask = this._writeTask.then(async (fd) => {
|
|
if (!fd) {
|
|
this._path = await getTempPath();
|
|
fd = await fdopen(this._path, 'w+');
|
|
}
|
|
await fn(fd);
|
|
return fd;
|
|
});
|
|
}
|
|
|
|
private _writeEnd(): void {
|
|
this._writeTask = this._writeTask.then((fd) => fdclose(fd)).then(() => 0);
|
|
}
|
|
|
|
constructor(array?: any[], options?: BlobPropertyBag) {
|
|
this._type = (options && options.type) || '';
|
|
|
|
if (!array) {
|
|
this._path = '';
|
|
this._size = 0;
|
|
} else {
|
|
var size = 0;
|
|
for (const value of array) {
|
|
if (value instanceof ArrayBuffer) {
|
|
if (value.byteLength === 0) continue;
|
|
this._write((fd) => fdwrite(fd, new Uint8Array(value)));
|
|
size += value.byteLength;
|
|
} else if (value instanceof Uint8Array) {
|
|
if (value.byteLength === 0) continue;
|
|
this._write((fd) => fdwrite(fd, value));
|
|
size += value.byteLength;
|
|
} else if (
|
|
value instanceof Int8Array ||
|
|
value instanceof Uint8ClampedArray ||
|
|
value instanceof Int16Array ||
|
|
value instanceof Uint16Array ||
|
|
value instanceof Int32Array ||
|
|
value instanceof Uint32Array ||
|
|
value instanceof Float32Array ||
|
|
value instanceof Float64Array ||
|
|
value instanceof DataView
|
|
) {
|
|
if (value.byteLength === 0) continue;
|
|
this._write((fd) =>
|
|
fdwrite(
|
|
fd,
|
|
new Uint8Array(value.buffer, value.byteOffset, value.byteLength),
|
|
),
|
|
);
|
|
size += value.byteLength;
|
|
} else if (value instanceof VBlob) {
|
|
if (value._size === 0) continue;
|
|
this._write((fd) => fdwriteFile(fd, value._path));
|
|
size += value._size;
|
|
} else {
|
|
const str = value + '';
|
|
if (str.length === 0) continue;
|
|
this._write((fd) => fdwrite(fd, str));
|
|
size += str.length;
|
|
}
|
|
}
|
|
this._writeEnd();
|
|
this._size = size;
|
|
}
|
|
}
|
|
|
|
get size(): number {
|
|
return this._size;
|
|
}
|
|
|
|
get type(): string {
|
|
return this._type;
|
|
}
|
|
|
|
slice(start?: number, end?: number, contentType?: string): Blob {
|
|
if (!start) start = 0;
|
|
else if (start < 0) start = this._size + start;
|
|
if (!end) end = this._size;
|
|
|
|
if (end < 0) end = this._size - end;
|
|
else if (end >= this._size) end = this._size;
|
|
if (start >= end) return new VBlob([]);
|
|
|
|
const newblob = new VBlob();
|
|
newblob._type = contentType || this._type;
|
|
newblob._writeTask = this._writeTask;
|
|
newblob._offset = this._offset + start;
|
|
newblob._size = end - start;
|
|
this._writeTask.then(() => (newblob._path = this._path));
|
|
return newblob;
|
|
}
|
|
|
|
readBuffer(fd: number): Promise<ArrayBuffer> {
|
|
return fdread(fd, this._size, this._offset).then((buffer) => buffer.buffer);
|
|
}
|
|
}
|
|
|
|
export var Blob: { new (array?: any[], options?: BlobPropertyBag): Blob } =
|
|
global['Blob'] || VBlob;
|
|
|
|
interface Aborted {
|
|
aborted: boolean;
|
|
}
|
|
export class VFileReader extends EventTarget implements FileReader {
|
|
static readonly EMPTY = 0;
|
|
static readonly LOADING = 1;
|
|
static readonly DONE = 2;
|
|
readonly EMPTY = 0;
|
|
readonly LOADING = 1;
|
|
readonly DONE = 2;
|
|
|
|
onabort: ((ev: ProgressEvent<FileReader>) => void) | null = null;
|
|
onerror: ((ev: FileReaderEvent) => void) | null = null;
|
|
onload: ((ev: FileReaderEvent) => void) | null = null;
|
|
onloadstart: ((ev: FileReaderEvent) => void) | null = null;
|
|
onloadend: ((ev: FileReaderEvent) => void) | null = null;
|
|
onprogress: ((ev: FileReaderEvent) => void) | null = null;
|
|
|
|
private _readyState: 0 | 1 | 2;
|
|
private _abort: (() => void) | null = null;
|
|
private _abortPromise: Promise<null> | null = null;
|
|
|
|
public result: any;
|
|
public error: any | null = null;
|
|
|
|
constructor() {
|
|
super();
|
|
this._readyState = 0;
|
|
}
|
|
|
|
get readyState(): 0 | 1 | 2 {
|
|
return this._readyState;
|
|
}
|
|
|
|
abort(): void {
|
|
if (this._abort !== null) {
|
|
this._abort();
|
|
this._abort = null;
|
|
this._abortPromise = null;
|
|
this.dispatchEvent({ type: 'abort' });
|
|
}
|
|
if (this._readyState === 1) {
|
|
this._finishWork();
|
|
}
|
|
}
|
|
|
|
private _startWork(methodName: string): Aborted {
|
|
if (this._readyState === 1) {
|
|
throw Error(
|
|
`Failed to execute '${methodName}' on 'FileReader': The object is already busy reading Blobs.`,
|
|
);
|
|
}
|
|
this.result = null;
|
|
this.error = null;
|
|
this._readyState = 1;
|
|
const aborted: Aborted = { aborted: false };
|
|
if (this._abortPromise === null) {
|
|
this._abortPromise = new Promise<null>((resolve) => {
|
|
this._abort = () => {
|
|
aborted.aborted = true;
|
|
resolve(null);
|
|
};
|
|
});
|
|
}
|
|
return aborted;
|
|
}
|
|
|
|
private _finishWork() {
|
|
this.dispatchEvent({ type: 'loadend' });
|
|
this._readyState = 2;
|
|
}
|
|
|
|
private _readBuffer(
|
|
methodName: string,
|
|
blob: Blob,
|
|
cb: (buffer: Buffer) => any,
|
|
): Promise<void> {
|
|
const aborted = this._startWork(methodName);
|
|
|
|
if (!(blob instanceof VBlob) && !(blob instanceof NodeBlob)) {
|
|
throw TypeError(
|
|
`vblob cannot handle the ${blob.constructor.name} class.`,
|
|
);
|
|
}
|
|
|
|
const prom = new Promise((resolve) => process.nextTick(resolve)).then(
|
|
() => {
|
|
if (aborted.aborted) return null;
|
|
this.dispatchEvent({ type: 'loadstart' });
|
|
if (blob instanceof VBlob) {
|
|
return this._readVBlob(blob);
|
|
} else if (blob instanceof NodeBlob) {
|
|
return this._readNodeBlob(blob);
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
);
|
|
return Promise.race([this._abortPromise, prom]).then(
|
|
(data) => {
|
|
if (data === null) return;
|
|
if (aborted.aborted) return;
|
|
this.result = cb(data);
|
|
this.dispatchEvent({ type: 'load' });
|
|
this._finishWork();
|
|
},
|
|
(err) => {
|
|
if (aborted.aborted) return;
|
|
this.error = err;
|
|
this.dispatchEvent({
|
|
type: 'error',
|
|
message: err ? err.message : 'Error',
|
|
});
|
|
this._finishWork();
|
|
},
|
|
);
|
|
}
|
|
|
|
private async _readVBlob(blob: VBlob): Promise<Buffer> {
|
|
if (blob._size === 0) {
|
|
return Buffer.alloc(0);
|
|
} else {
|
|
await blob._writeTask;
|
|
const fd = await fdopen(blob._path, 'r');
|
|
try {
|
|
return await fdread(fd, blob._size, blob._offset);
|
|
} finally {
|
|
fdclose(fd);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async _readNodeBlob(blob: NodeBlob): Promise<Buffer> {
|
|
const buf = await blob.arrayBuffer();
|
|
return Buffer.from(buf);
|
|
}
|
|
|
|
readAsArrayBuffer(blob: Blob): void {
|
|
this._readBuffer('readAsArrayBuffer', blob, (data) => data.buffer);
|
|
}
|
|
readAsBinaryString(blob: Blob): void {
|
|
this._readBuffer('readAsBinaryString', blob, (data) =>
|
|
data.toString('binary'),
|
|
);
|
|
}
|
|
readAsDataURL(blob: Blob): void {
|
|
this._readBuffer(
|
|
'readAsDataURL',
|
|
blob,
|
|
(data) =>
|
|
'data:' +
|
|
(blob.type || 'application/octet-stream') +
|
|
';base64,' +
|
|
data.toString('base64'),
|
|
);
|
|
}
|
|
readAsText(blob: Blob): void {
|
|
this._readBuffer('readAsText', blob, (data) => data.toString());
|
|
}
|
|
}
|
|
|
|
export var FileReader: { new (): FileReader } = VFileReader;
|