file-box
Version:
Pack a File into Box for easy move/transfer between servers no matter of where it is.(local path, remote url, or cloud storage)
790 lines (789 loc) • 25.9 kB
JavaScript
"use strict";
/**
* File Box
* https://github.com/huan/file-box
*
* 2018 Huan LI <zixia@zixia.net>
*/
/* eslint no-use-before-define: off */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileBox = void 0;
const FS = __importStar(require("fs"));
const PATH = __importStar(require("path"));
const URL = __importStar(require("url"));
const mime_1 = __importDefault(require("mime"));
const stream_1 = require("stream");
const clone_class_1 = require("clone-class");
const config_js_1 = require("./config.js");
const file_box_type_js_1 = require("./file-box.type.js");
const misc_js_1 = require("./misc.js");
const qrcode_js_1 = require("./qrcode.js");
const sized_chunk_transformer_js_1 = require("./pure-functions/sized-chunk-transformer.js");
const EMPTY_META_DATA = Object.freeze({});
const UNKNOWN_SIZE = -1;
let interfaceOfFileBox = (_) => false;
let looseInstanceOfFileBox = (_) => false;
class FileBox {
/**
*
* Static Properties
*
*/
static version = config_js_1.VERSION;
/**
* Symbol.hasInstance: instanceof
*
* @link https://www.keithcirkel.co.uk/metaprogramming-in-es6-symbols/
*/
static [Symbol.hasInstance](lho) {
return this.validInterface(lho);
}
/**
* Check if obj satisfy FileBox interface
*/
static valid(target) {
return this.validInstance(target) || this.validInterface(target);
}
/**
* Check if obj satisfy FileBox interface
*/
static validInterface(target) {
return interfaceOfFileBox(target);
}
/**
* loose check instance of FileBox
*/
static validInstance(target) {
return looseInstanceOfFileBox(target);
}
/**
* fromUrl()
*/
static fromUrl(url, nameOrOptions, headers) {
let name;
let size;
if (typeof nameOrOptions === 'object') {
headers = nameOrOptions.headers;
name = nameOrOptions.name;
size = nameOrOptions.size;
}
else {
name = nameOrOptions;
}
if (!name) {
const parsedUrl = new URL.URL(url);
name = parsedUrl.pathname;
}
const options = {
headers,
name,
size,
type: file_box_type_js_1.FileBoxType.Url,
url,
};
return new this(options);
}
/**
* Alias for `FileBox.fromFile()`
*
* @alias fromFile
*/
static fromFile(path, name) {
if (!name) {
name = PATH.parse(path).base;
}
const options = {
name,
path,
type: file_box_type_js_1.FileBoxType.File,
};
return new this(options);
}
/**
* TODO: add `FileBoxStreamOptions` with `size` support (@huan, 202111)
*/
static fromStream(stream, name) {
const options = {
name: name || 'stream.dat',
stream,
type: file_box_type_js_1.FileBoxType.Stream,
};
return new this(options);
}
static fromBuffer(buffer, name) {
const options = {
buffer,
name: name || 'buffer.dat',
type: file_box_type_js_1.FileBoxType.Buffer,
};
return new this(options);
}
/**
* @param base64
* @param name the file name of the base64 data
*/
static fromBase64(base64, name) {
const options = {
base64,
name: name || 'base64.dat',
type: file_box_type_js_1.FileBoxType.Base64,
};
return new this(options);
}
/**
* dataURL: `data:image/png;base64,${base64Text}`,
*/
static fromDataURL(dataUrl, name) {
return this.fromBase64((0, misc_js_1.dataUrlToBase64)(dataUrl), name || 'data-url.dat');
}
/**
*
* @param qrCode the value of the QR Code. For example: `https://github.com`
*/
static fromQRCode(qrCode) {
const options = {
name: 'qrcode.png',
qrCode,
type: file_box_type_js_1.FileBoxType.QRCode,
};
return new this(options);
}
static uuidToStream;
static uuidFromStream;
/**
* @param uuid the UUID of the file. For example: `6f88b03c-1237-4f46-8db2-98ef23200551`
* @param name the name of the file. For example: `video.mp4`
*/
static fromUuid(uuid, nameOrOptions) {
let name;
let size;
if (typeof nameOrOptions === 'object') {
name = nameOrOptions.name;
size = nameOrOptions.size;
}
else {
name = nameOrOptions;
}
const options = {
name: name || `${uuid}.dat`,
size,
type: file_box_type_js_1.FileBoxType.Uuid,
uuid,
};
return new this(options);
}
/**
* @deprecated use `setUuidLoader()` instead
*/
static setUuidResolver(loader) {
console.error('FileBox.sxetUuidResolver() is deprecated. Use `setUuidLoader()` instead.\n', new Error().stack);
return this.setUuidLoader(loader);
}
static setUuidLoader(loader) {
if (Object.prototype.hasOwnProperty.call(this, 'uuidToStream')) {
throw new Error('this FileBox has been set resolver before, can not set twice');
}
this.uuidToStream = loader;
}
/**
* @deprecated use `setUuidSaver()` instead
*/
static setUuidRegister() {
console.error('FileBox.setUuidRegister() is deprecated. Use `setUuidSaver()` instead.\n', new Error().stack);
}
static setUuidSaver(saver) {
if (Object.prototype.hasOwnProperty.call(this, 'uuidFromStream')) {
throw new Error('this FileBox has been set register before, can not set twice');
}
this.uuidFromStream = saver;
}
/**
*
* @static
* @param {(FileBoxJsonObject | string)} obj
* @returns {FileBox}
*/
static fromJSON(obj) {
if (typeof obj === 'string') {
obj = JSON.parse(obj);
}
/**
* Huan(202111): compatible with old FileBox.toJSON() key: `boxType`
* this is a breaking change made by v1.0
*
* convert `obj.boxType` to `obj.type`
* (will be removed after Dec 31, 2022)
*/
if (!obj.type && 'boxType' in obj) {
obj.type = obj['boxType'];
}
let fileBox;
switch (obj.type) {
case file_box_type_js_1.FileBoxType.Base64:
fileBox = FileBox.fromBase64(obj.base64, obj.name);
break;
case file_box_type_js_1.FileBoxType.Url:
fileBox = FileBox.fromUrl(obj.url, {
name: obj.name,
size: obj.size,
});
break;
case file_box_type_js_1.FileBoxType.QRCode:
fileBox = FileBox.fromQRCode(obj.qrCode);
break;
case file_box_type_js_1.FileBoxType.Uuid:
fileBox = FileBox.fromUuid(obj.uuid, {
name: obj.name,
size: obj.size,
});
break;
default:
throw new Error(`unknown filebox json object{type}: ${JSON.stringify(obj)}`);
}
if (obj.metadata) {
fileBox.metadata = obj.metadata;
}
return fileBox;
}
/**
*
* Instance Properties
*
*/
version = config_js_1.VERSION;
/**
* We are using a getter for `type` is because
* getter name can be enumurated by the `Object.hasOwnProperties()`*
* but property name can not.
*
* * required by `validInterface()`
*/
_type;
get type() { return this._type; }
/**
* the Content-Length of the file
* `SIZE_UNKNOWN(-1)` means unknown
*
* @example
* ```ts
* const fileBox = FileBox.fromUrl('http://example.com/image.png')
* await fileBox.ready()
* console.log(fileBox.size)
* // > 102400 <- this is the size of the remote image.png
* ```
*/
_size;
get size() {
if (this._size) {
return this._size;
}
return UNKNOWN_SIZE;
}
/**
*
/**
* @deprecated: use `mediaType` instead. will be removed after Dec 31, 2022
*/
mimeType = 'application/unknown';
/**
* (Internet) Media Type is the proper technical term of `MIME Type`
* @see https://stackoverflow.com/a/9277778/1123955
*
* @example 'text/plain'
*/
_mediaType;
get mediaType() {
if (this._mediaType) {
return this._mediaType;
}
return 'application/unknown';
}
_name;
get name() {
return this._name;
}
_metadata;
get metadata() {
if (this._metadata) {
return this._metadata;
}
return EMPTY_META_DATA;
}
set metadata(data) {
if (this._metadata) {
throw new Error('metadata can not be modified after set');
}
this._metadata = { ...data };
Object.freeze(this._metadata);
}
/**
* Lazy load data: (can be serialized to JSON)
* Do not read file to Buffer until there's a consumer.
*/
base64;
remoteUrl;
qrCode;
uuid;
/**
* Can not be serialized to JSON
*/
buffer;
localPath;
stream;
headers;
constructor(options) {
// Only keep `basename` in this.name
this._name = PATH.basename(options.name);
this._type = options.type;
/**
* Unknown file type MIME: `'application/unknown'`
* @see https://stackoverflow.com/a/6080707/1123955
*/
this._mediaType = mime_1.default.getType(this.name) ?? undefined;
switch (options.type) {
case file_box_type_js_1.FileBoxType.Buffer:
this.buffer = options.buffer;
this._size = options.buffer.length;
break;
case file_box_type_js_1.FileBoxType.File:
if (!options.path) {
throw new Error('no path');
}
this.localPath = options.path;
this._size = FS.statSync(this.localPath).size;
break;
case file_box_type_js_1.FileBoxType.Url:
if (!options.url) {
throw new Error('no url');
}
this.remoteUrl = options.url;
if (options.headers) {
this.headers = options.headers;
}
if (options.size) {
this._size = options.size;
}
else {
/**
* Add a background task to fetch remote file name & size
*
* TODO: how to improve it?
*/
// this.syncUrlMetadata().catch(console.error)
}
break;
case file_box_type_js_1.FileBoxType.Stream:
this.stream = options.stream;
if (options.size) {
this._size = options.size;
}
break;
case file_box_type_js_1.FileBoxType.QRCode:
if (!options.qrCode) {
throw new Error('no QR Code');
}
this.qrCode = options.qrCode;
break;
case file_box_type_js_1.FileBoxType.Base64:
if (!options.base64) {
throw new Error('no Base64 data');
}
this.base64 = options.base64;
this._size = Buffer.byteLength(options.base64, 'base64');
break;
case file_box_type_js_1.FileBoxType.Uuid:
if (!options.uuid) {
throw new Error('no UUID data');
}
this.uuid = options.uuid;
if (options.size) {
this._size = options.size;
}
break;
default:
throw new Error(`unknown options(type): ${JSON.stringify(options)}`);
}
}
async ready() {
switch (this.type) {
case file_box_type_js_1.FileBoxType.Url:
await this._syncUrlMetadata();
break;
case file_box_type_js_1.FileBoxType.QRCode:
if (this.size === UNKNOWN_SIZE) {
this._size = (await this.toBuffer()).length;
}
break;
default:
break;
}
}
/**
* @todo use http.get/gets instead of Request
*/
async _syncUrlMetadata() {
/**
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
* > Content-Disposition: attachment; filename="cool.html"
*/
if (this.type !== file_box_type_js_1.FileBoxType.Url) {
throw new Error('type is not Url');
}
if (!this.remoteUrl) {
throw new Error('no url');
}
const headers = await (0, misc_js_1.httpHeadHeader)(this.remoteUrl);
const httpFilename = (0, misc_js_1.httpHeaderToFileName)(headers);
if (httpFilename) {
this._name = httpFilename;
}
if (!this.name) {
throw new Error('NONAME');
}
const httpMediaType = headers['content-type'] || (httpFilename && mime_1.default.getType(httpFilename));
if (httpMediaType) {
this._mediaType = httpMediaType;
}
if (headers['content-length']) {
this._size = Number(headers['content-length']);
}
}
/**
*
* toXXX methods
*
*/
toString() {
return [
'FileBox#',
file_box_type_js_1.FileBoxType[this.type],
'<',
this.name,
'>',
].join('');
}
toJSON() {
const objCommon = {
metadata: this.metadata,
name: this.name,
};
if (typeof this.size !== 'undefined') {
objCommon.size = this.size;
}
let obj;
switch (this.type) {
case file_box_type_js_1.FileBoxType.Url: {
if (!this.remoteUrl) {
throw new Error('no url');
}
const objUrl = {
headers: this.headers,
type: file_box_type_js_1.FileBoxType.Url,
url: this.remoteUrl,
};
obj = {
...objCommon,
...objUrl,
};
break;
}
case file_box_type_js_1.FileBoxType.QRCode: {
if (!this.qrCode) {
throw new Error('no qr code');
}
const objQRCode = {
qrCode: this.qrCode,
type: file_box_type_js_1.FileBoxType.QRCode,
};
obj = {
...objCommon,
...objQRCode,
};
break;
}
case file_box_type_js_1.FileBoxType.Base64: {
if (!this.base64) {
throw new Error('no base64 data');
}
const objBase64 = {
base64: this.base64,
type: file_box_type_js_1.FileBoxType.Base64,
};
obj = {
...objCommon,
...objBase64,
};
break;
}
case file_box_type_js_1.FileBoxType.Uuid: {
if (!this.uuid) {
throw new Error('no uuid data');
}
const objUuid = {
type: file_box_type_js_1.FileBoxType.Uuid,
uuid: this.uuid,
};
obj = {
...objCommon,
...objUuid,
};
break;
}
default:
void this.type;
throw new Error('FileBox.toJSON() can only work on limited FileBoxType(s). See: <https://github.com/huan/file-box/issues/25>');
}
/**
* Huan(202111): compatible with old FileBox.toJSON() key: `boxType`
* this is a breaking change made by v1.0
*
* save `obj.type` a copy to `obj.boxType`
* (will be removed after Dec 31, 2022)
*/
obj['boxType'] = obj.type;
return obj;
}
async toStream() {
let stream;
switch (this.type) {
case file_box_type_js_1.FileBoxType.Buffer:
stream = this._transformBufferToStream();
break;
case file_box_type_js_1.FileBoxType.File:
stream = this._transformFileToStream();
break;
case file_box_type_js_1.FileBoxType.Url:
stream = await this._transformUrlToStream();
break;
case file_box_type_js_1.FileBoxType.Stream:
if (!this.stream) {
throw new Error('no stream');
}
/**
* Huan(202109): the stream.destroyed will not be `true`
* when we have read all the data
* after we change some code.
* The reason is unbase64 : this.base64,
type : FileBoxType.Base64,known... so we change to check `readable`
*/
if (!this.stream.readable) {
throw new Error('The stream is not readable. Maybe has already been consumed, and now it was drained. See: https://github.com/huan/file-box/issues/50');
}
stream = this.stream;
break;
case file_box_type_js_1.FileBoxType.QRCode:
if (!this.qrCode) {
throw new Error('no QR Code');
}
stream = await this._transformQRCodeToStream();
break;
case file_box_type_js_1.FileBoxType.Base64:
if (!this.base64) {
throw new Error('no base64 data');
}
stream = this._transformBase64ToStream();
break;
case file_box_type_js_1.FileBoxType.Uuid: {
if (!this.uuid) {
throw new Error('no uuid data');
}
const FileBoxKlass = (0, clone_class_1.instanceToClass)(this, FileBox);
if (typeof FileBoxKlass.uuidToStream !== 'function') {
throw new Error('need to call FileBox.setUuidLoader() to set UUID loader first.');
}
stream = await FileBoxKlass.uuidToStream.call(this, this.uuid);
break;
}
default:
throw new Error('not supported FileBoxType: ' + file_box_type_js_1.FileBoxType[this.type]);
}
return stream;
}
/**
* https://stackoverflow.com/a/16044400/1123955
*/
_transformBufferToStream(buffer) {
const bufferStream = new stream_1.PassThrough();
bufferStream.end(buffer || this.buffer);
/**
* Use small `chunks` with `toStream()` #44
* https://github.com/huan/file-box/issues/44
*/
return bufferStream.pipe((0, sized_chunk_transformer_js_1.sizedChunkTransformer)());
}
_transformBase64ToStream() {
if (!this.base64) {
throw new Error('no base64 data');
}
const buffer = Buffer.from(this.base64, 'base64');
return this._transformBufferToStream(buffer);
}
_transformFileToStream() {
if (!this.localPath) {
throw new Error('no url(path)');
}
return FS.createReadStream(this.localPath);
}
async _transformUrlToStream() {
return new Promise((resolve, reject) => {
if (this.remoteUrl) {
(0, misc_js_1.httpStream)(this.remoteUrl, this.headers)
.then(resolve)
.catch(reject);
}
else {
reject(new Error('no url'));
}
});
}
async _transformQRCodeToStream() {
if (!this.qrCode) {
throw new Error('no QR Code Value found');
}
const stream = (0, qrcode_js_1.qrValueToStream)(this.qrCode);
return stream;
}
/**
* save file
*
* @param filePath save file
*/
async toFile(filePath, overwrite = false) {
if (this.type === file_box_type_js_1.FileBoxType.Url) {
if (!this.mediaType || !this.name) {
await this._syncUrlMetadata();
}
}
const fullFilePath = PATH.resolve(filePath || this.name);
const exist = FS.existsSync(fullFilePath);
if (exist && !overwrite) {
throw new Error(`FileBox.toFile(${fullFilePath}): file exist. use FileBox.toFile(${fullFilePath}, true) to force overwrite.`);
}
const writeStream = FS.createWriteStream(fullFilePath);
/**
* Huan(202109): make sure the file can be opened for writting
* before we pipe the stream to it
*/
await new Promise((resolve, reject) => writeStream
.once('open', resolve)
.once('error', reject));
/**
* Start pipe
*/
await new Promise((resolve, reject) => {
writeStream
.once('close', resolve)
.once('error', reject);
this.pipe(writeStream);
});
}
async toBase64() {
if (this.type === file_box_type_js_1.FileBoxType.Base64) {
if (!this.base64) {
throw new Error('no base64 data');
}
return this.base64;
}
const buffer = await this.toBuffer();
return buffer.toString('base64');
}
/**
* dataUrl: `data:image/png;base64,${base64Text}',
*/
async toDataURL() {
const base64Text = await this.toBase64();
if (!this.mediaType) {
throw new Error('no mediaType found');
}
const dataUrl = [
'data:',
this.mediaType,
';base64,',
base64Text,
].join('');
return dataUrl;
}
async toBuffer() {
if (this.type === file_box_type_js_1.FileBoxType.Buffer) {
if (!this.buffer) {
throw new Error('no buffer!');
}
return this.buffer;
}
const stream = new stream_1.PassThrough();
this.pipe(stream);
const buffer = await (0, misc_js_1.streamToBuffer)(stream);
return buffer;
}
async toQRCode() {
if (this.type === file_box_type_js_1.FileBoxType.QRCode) {
if (!this.qrCode) {
throw new Error('no QR Code!');
}
return this.qrCode;
}
const buf = await this.toBuffer();
const qrValue = await (0, qrcode_js_1.bufferToQrValue)(buf);
return qrValue;
}
async toUuid() {
if (this.type === file_box_type_js_1.FileBoxType.Uuid) {
if (!this.uuid) {
throw new Error('no uuid found for a UUID type file box!');
}
return this.uuid;
}
const FileBoxKlass = (0, clone_class_1.instanceToClass)(this, FileBox);
if (typeof FileBoxKlass.uuidFromStream !== 'function') {
throw new Error('need to use FileBox.setUuidSaver() before dealing with UUID');
}
const stream = new stream_1.PassThrough();
this.pipe(stream);
return FileBoxKlass.uuidFromStream.call(this, stream);
}
/**
*
* toXXX methods END
*
*/
pipe(destination) {
this.toStream().then(stream => {
stream.on('error', e => {
console.info('error:', e);
destination.emit('error', e);
});
return stream.pipe(destination);
}).catch(e => destination.emit('error', e));
return destination;
}
}
exports.FileBox = FileBox;
/**
* Huan(202110): lazy initialize `interfaceOfClass(FileBox)`
* because we only can reference a class after its declaration
*/
interfaceOfFileBox = (0, clone_class_1.interfaceOfClass)(FileBox)();
looseInstanceOfFileBox = (0, clone_class_1.looseInstanceOfClass)(FileBox);
//# sourceMappingURL=file-box.js.map