UNPKG

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)

375 lines 17.7 kB
#!/usr/bin/env -S node --no-warnings --loader ts-node/esm var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; import 'reflect-metadata'; import assert from 'assert'; import { PassThrough, } from 'stream'; import { test, sinon, } from 'tstest'; import { FileBox } from './file-box.js'; import { FileBoxType } from './file-box.type.js'; const requiredMetadataKey = Symbol('required'); const tstest = { classFixture() { return (constructor) => { console.info(constructor.name); console.info(constructor.prototype.name); }; }, methodFixture() { return (..._ // target : Object, // propertyKey : string, // descriptor : PropertyDescriptor, ) => { console.info('@fixture()'); }; }, parameterFixture() { return (target, propertyKey, parameterIndex) => { console.info(propertyKey); const existingRequiredParameters = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []; existingRequiredParameters.push(parameterIndex); Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey); }; }, }; test('File smoke testing', async (t) => { t.throws(() => FileBox.fromFile('x'), 'should throw for a non-existing file'); }); let FixtureFileBox = class FixtureFileBox { static localFileFixture() { return { content: 'T', name: 'test.txt', size: '1', type: 'plain/text', }; } }; __decorate([ tstest.methodFixture(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], FixtureFileBox, "localFileFixture", null); FixtureFileBox = __decorate([ tstest.classFixture() ], FixtureFileBox); export { FixtureFileBox }; // tslint:disable:max-classes-per-file export class TestFileBox { static testFileCreateLocal(localFileFixture) { const file = FileBox.fromFile(localFileFixture); test('File.createLocal()', async (t) => { t.ok(file, 'ok'); }); test('File.fromRemote()', async (t) => { const URL = 'http://httpbin.org/response-headers?Content-Type=text/plain;%20charset=UTF-8&Content-Disposition=attachment;%20filename%3d%22test.json%22'; assert(URL); t.pass('ok'); }); } } __decorate([ __param(0, tstest.parameterFixture()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", void 0) ], TestFileBox, "testFileCreateLocal", null); test('toBase64()', async (t) => { const BASE64_DECODED = 'FileBoxBase64\n'; const BASE64_ENCODED = 'RmlsZUJveEJhc2U2NAo='; const fileBox = FileBox.fromBase64(BASE64_ENCODED, 'test.txt'); const base64 = await fileBox.toBase64(); t.equal(base64, BASE64_ENCODED, 'should get base64 back'); const text = Buffer.from(base64, 'base64').toString(); t.equal(text, BASE64_DECODED, 'should get the text right'); }); test('fromBuffer() & toBase64()', async (t) => { const BASE64_ENCODED = 'RmlsZUJveEJhc2U2NAo='; const buffer = Buffer.from(BASE64_ENCODED, 'base64'); const fileBox = FileBox.fromBuffer(buffer, 'test.txt'); const base64 = await fileBox.toBase64(); t.equal(base64, BASE64_ENCODED, 'should get base64 back from buffer'); }); test('syncRemote()', async (t) => { class FileBoxTest extends FileBox { static fromUrl(...args) { return super.fromUrl(...args); } _syncUrlMetadata() { return super._syncUrlMetadata(); } } const URL = 'http://httpbin.org/response-headers?Content-Disposition=attachment;%20filename%3d%22test.txt%22&filename=test.txt'; const EXPECTED_NAME_FROM_URL = 'response-headers'; const EXPECTED_TYPE_FROM_URL = 'application/unknown'; const EXPECTED_NAME_FROM_HEADER = 'test.txt'; const EXPECTED_SIZE_FROM_HEADER = 159; const EXPECTED_TYPE_FROM_HEADER = 'application/json'; const fileBox = FileBoxTest.fromUrl(URL); t.equal(fileBox.name, EXPECTED_NAME_FROM_URL, 'should get the name from url'); t.equal(fileBox.mediaType, EXPECTED_TYPE_FROM_URL, 'should get the mime type from url'); await fileBox._syncUrlMetadata(); t.equal(fileBox.size, EXPECTED_SIZE_FROM_HEADER, 'should get the size from remote header'); t.equal(fileBox.name, EXPECTED_NAME_FROM_HEADER, 'should get the name from remote header'); t.equal(fileBox.mediaType, EXPECTED_TYPE_FROM_HEADER, 'should get the mime type from remote http header'); }); test('fromURL() deal with url with querystring', async (t) => { const URL = 'https://zixia.net/a.jpg?name=value&t=1324'; const EXPECTED_NAME = 'a.jpg'; const fileBox = FileBox.fromUrl(URL); t.equal(fileBox.name, EXPECTED_NAME, 'should get basename from url with querystring'); }); test('toDataURL()', async (t) => { const FILE_PATH = 'tests/fixtures/data.bin'; const EXPECTED_DATA_URL = 'data:application/octet-stream;base64,dGVzdA=='; const fileBox = FileBox.fromFile(FILE_PATH); const dataUrl = await fileBox.toDataURL(); t.equal(dataUrl, EXPECTED_DATA_URL, 'should get the data url right'); }); test('toString()', async (t) => { const FILE_PATH = 'tests/fixtures/data.bin'; const EXPECT_STRING = 'FileBox#File<data.bin>'; const fileBox = FileBox.fromFile(FILE_PATH); t.equal(fileBox.toString(), EXPECT_STRING, 'should get the toString() result'); }); test('toBuffer()', async (t) => { const FILE_PATH = 'tests/fixtures/data.bin'; const EXPECT_STRING = 'test'; const fileBox = FileBox.fromFile(FILE_PATH); const buffer = await fileBox.toBuffer(); t.equal(buffer.toString(), EXPECT_STRING, 'should get the toBuffer() result'); }); /** * Huan(202106): we keep this unit test for trying to figure out which operation system can support this long file name. * See: https://github.com/huan/file-box/issues/58 */ test('toFile() with long name', async (t) => { const IMAGE_URL = 'https://s3.cn-north-1.amazonaws.com.cn/xiaoju-material/public/5ffd393fc503f00039101dae_1620978346435_%E7%94%B5%E6%B1%A0%E5%9E%8B%E5%8F%B7%09%E8%BF%9B%E8%B4%A7%E4%BB%B7%E6%A0%BC%09%E5%8E%9F%E5%94%AE%E5%90%8E%E8%A1%A5%E6%AC%BE%E4%BB%B7%E6%A0%BC%09%E7%8E%B0%E5%85%AC%E5%8F%B8%E6%89%BF%E6%8B%85%E4%B8%80%E5%8D%8A%E7%9A%84%E4%BB%B7%E6%A0%BC%0AZD-20-100%09420%09270%09135%0AZD-Q85-D23L%09360%09270%09135%0AZD-H6-L3%09420%09315%09157.5%0A%E6%80%BB%E8%AE%A1%EF%BC%9A%091200%09855%09427.5%0A'; const linux = { code: 'ENAMETOOLONG', errno: -36, syscall: 'open', }; const darwin = { code: 'ENAMETOOLONG', errno: -63, syscall: 'open', }; const win32 = { code: 'ENOENT', errno: -4058, syscall: 'open', }; const FIXTURE_ERROR = { darwin, linux, win32, }; const fileBox = FileBox.fromUrl(IMAGE_URL); await t.rejects(fileBox.toFile(), FIXTURE_ERROR[process.platform], `should reject toFile() with ${JSON.stringify(FIXTURE_ERROR[process.platform])}`); }); test('metadata', async (t) => { const FILE_PATH = 'tests/fixtures/data.bin'; const EXPECTED_NAME = 'myname'; const EXPECTED_AGE = 'myage'; const EXPECTED_MOL = 42; // interface MetadataType { // metaname : string, // metaage : number, // metaobj: { // mol: number, // } // } const EXPECTED_METADATA = { metaage: EXPECTED_AGE, metaname: EXPECTED_NAME, metaobj: { mol: EXPECTED_MOL, }, }; const fileBox = FileBox.fromFile(FILE_PATH); t.same(fileBox.metadata, {}, 'should get a empty {} if not set'); t.doesNotThrow(() => { fileBox.metadata = EXPECTED_METADATA; }, 'should not throw for set metadata for the first time'); t.throws(() => { fileBox.metadata = EXPECTED_METADATA; }, 'should throw for set metadata again'); t.throws(() => { fileBox.metadata['mol'] = EXPECTED_MOL; }, 'should throw for change value of a property on metadata'); t.same(fileBox.metadata, EXPECTED_METADATA, 'should get the metadata'); }); test('fromQRCode()', async (t) => { const QRCODE_VALUE = 'hello, world!'; const EXPECTED_QRCODE_IMAGE_BASE64 = [ 'iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0CAYAAABUmhYnAAAAAklEQVR4AewaftIAAAKcSURBVO3BQY7', 'cQAwEwSxC//9yeo88NSBIsx7TjIg/WGMUa5RijVKsUYo1SrFGKdYoxRqlWKMUa5RijVKsUYo1SrFGKd', 'YoxRrl4qEk/CaVkyTcoXKShN+k8kSxRinWKMUa5eJlKm9Kwh0qn6TypiS8qVijFGuUYo1y8WFJuEPlj', 'iR0Kl0SOpUuCZ3KHUm4Q+WTijVKsUYp1igXw6n8T4o1SrFGKdYoF8MloVOZrFijFGuUYo1y8WEq3yQJ', 'ncoTKt+kWKMUa5RijXLxsiR8M5UuCZ3KSRK+WbFGKdYoxRrl4iGVf5nKicq/pFijFGuUYo0Sf/BAEjq', 'VLglvUnkiCZ3KSRLepPJJxRqlWKMUa5SLlyXhROU3JaFT6ZJwonKShCeS0Kk8UaxRijVKsUaJP3hREj', 'qVO5JwotIloVO5Iwl3qJwk4Q6VNxVrlGKNUqxRLv6yJJyodEl4Igl3qHRJ6FTuUPmkYo1SrFGKNcrFQ', '0k4ScITSehUuiScJKFT6ZLQqXRJuEPljiR0Kk8Ua5RijVKsUS4eUvkmSbhDpUvCHUk4UflNxRqlWKMU', 'a5SLh5Lwm1Q6lS4JdyThCZWTJJyovKlYoxRrlGKNcvEylTcl4SQJnUqXhCdUuiScJOFvKtYoxRqlWKN', 'cfFgS7lB5k0qXhDuS0Kl0SehUTpLQJaFTeaJYoxRrlGKNcjFcEjqVJ5JwkoROpVP5pGKNUqxRijXKxX', '8mCScqXRJOVE6S0Kl8UrFGKdYoxRrl4sNUPknlDpUuCV0SOpWTJHyTYo1SrFGKNcrFy5Lwm5LQqXQqX', 'RI6lZMkdConKidJ6FTeVKxRijVKsUaJP1hjFGuUYo1SrFGKNUqxRinWKMUapVijFGuUYo1SrFGKNUqx', 'RinWKMUa5Q8Ztu740xD9iQAAAABJRU5ErkJggg==', ].join(''); const fileBox = FileBox.fromQRCode(QRCODE_VALUE); const base64Text = await fileBox.toBase64(); t.equal(base64Text, EXPECTED_QRCODE_IMAGE_BASE64, 'should encode QR Code value to expected image'); }); test('toQRCode()', async (t) => { const QRCODE_IMAGE_BASE64 = [ 'iVBORw0KGgoAAAANSUhEUgAAAMgAAADIAQMAAACXljzdAAAABlBMVEX///8AAABVwtN+AAAA', 'CXBIWXMAAA7EAAAOxAGVKw4bAAAA7klEQVRYw+2WsQ3EIAxFjShSMgKjZLRktIzCCJQpIv7Z', 'hCiXO/qzT/wCWXo0X3wbEw0NWVaEKM187KHW2QLZ+AhpXovfQ+J6skEWHELqBa5NEeCwR7iS', 'V7BDzuzAiZ9eqn5IWjfWXHf7VCO5tPAM6U9AjSRideyHFn4FiuvDqV5CM9rZXuF2pZmIAjZy', 'x4S0MDdBxEmu3TrliPf7iglPvuLlRydfU3P70UweCSK+ZYK0mUg1O4AVcv0/8itGkC7SdiTH', '0+Mz19oJZ4NkhhSPbIhQkQGI8u1HJzmzs7p7pzNAru2pJb6z8ykkQ0P/pheK6vjurjf7+wAA', 'AABJRU5ErkJggg==', ].join(''); const EXPECTED_QRCODE_TEXT = 'hello, world!'; const fileBox = FileBox.fromBase64(QRCODE_IMAGE_BASE64, 'qrcode.png'); const qrCodeValue = await fileBox.toQRCode(); t.equal(qrCodeValue, EXPECTED_QRCODE_TEXT, 'should decode qrcode image base64 to qr code value'); }); test('toJSON()', async (t) => { // const BASE64_DECODED = 'FileBoxBase64\n' const BASE64_ENCODED = 'RmlsZUJveEJhc2U2NAo='; const BASE64_FILENAME = 'test.txt'; /** * Huan(202111): we have both `type` and `boxType` is because the compatible issue #73 * @see https://github.com/huan/file-box/issues/73 */ const EXPECTED_JSON_TEXT = '{"metadata":{},"name":"test.txt","size":14,"base64":"RmlsZUJveEJhc2U2NAo=","type":1,"boxType":1}'; const fileBox = FileBox.fromBase64(BASE64_ENCODED, BASE64_FILENAME); const jsonText = JSON.stringify(fileBox); t.equal(jsonText, EXPECTED_JSON_TEXT, 'should get expected json text'); const newFileBox = FileBox.fromJSON(jsonText); const newBase64 = await newFileBox.toBase64(); t.equal(newBase64, BASE64_ENCODED, 'should get base64 back'); }); test('toJSON() for not supported type', async (t) => { const BASE64_ENCODED = 'RmlsZUJveEJhc2U2NAo='; const buffer = Buffer.from(BASE64_ENCODED, 'base64'); const fileBox = FileBox.fromBuffer(buffer, 'test.txt'); t.equal(fileBox.type, FileBoxType.Buffer, 'should get type() as Buffer'); t.throws(() => JSON.stringify(fileBox), 'should throw for buffer type of FileBox'); }); /** * Issue #50: Stream can not be consumed twice * https://github.com/huan/file-box/issues/50 */ test('toStream() twice for a stream', async (t) => { const stream = new PassThrough(); const box = FileBox.fromStream(stream, 'hello.dat'); stream.end('hello, world!'); // consume it await t.resolves(box.toBase64(), 'should successful to read the stream for the first time'); // consume it twice await t.rejects(box.toBuffer(), 'should throw when the file-box be consumed twice'); }); test('toUuid()', async (t) => { const BASE64_ENCODED = 'RmlsZUJveEJhc2U2NAo='; const UUID = '12345678-1234-1234-1234-123456789012'; class FileBoxTest extends FileBox { } const buffer = Buffer.from(BASE64_ENCODED, 'base64'); const fileBox = FileBoxTest.fromBuffer(buffer, 'test.txt'); await t.rejects(fileBox.toUuid(), 'should reject without `FileBox.setUuidSaver()` call`'); FileBoxTest.setUuidSaver(() => Promise.resolve(UUID)); t.equal(await fileBox.toUuid(), UUID, `should get UUID: ${UUID}`); }); test('fromUuid()', async (t) => { const UUID = '12345678-1234-1234-1234-123456789012'; const TEXT = 'hello, world!'; class FileBoxTest extends FileBox { } const stream = new PassThrough(); stream.end(TEXT); const uuidBox = FileBoxTest.fromUuid(UUID, 'test.txt'); await t.rejects(uuidBox.toBase64(), 'should reject without `FileBox.setUuidLoader()` call`'); FileBoxTest.setUuidLoader((_) => Promise.resolve(stream)); t.equal((await uuidBox.toBuffer()).toString(), TEXT, `should get BASE64: ${TEXT}`); }); test('setUuidLoader()', async (t) => { class FileBoxTest1 extends FileBox { } class FileBoxTest2 extends FileBox { } t.doesNotThrow(() => FileBoxTest1.setUuidLoader((_) => ({})), 'should not throw for set loader for the first time'); t.throws(() => FileBoxTest1.setUuidLoader((_) => ({})), 'should throw for set loader twice'); t.doesNotThrow(() => FileBoxTest2.setUuidLoader((_) => ({})), 'should not throw for set loader for the first time'); t.throws(() => FileBoxTest2.setUuidLoader((_) => ({})), 'should throw for set loader twice'); }); test('setUuidSaver()', async (t) => { class FileBoxTest1 extends FileBox { } class FileBoxTest2 extends FileBox { } t.doesNotThrow(() => FileBoxTest1.setUuidSaver((_) => Promise.resolve('uuid')), 'should not throw for set loader for the first time'); t.throws(() => FileBoxTest1.setUuidSaver((_) => Promise.resolve('uuid')), 'should throw for set loader twice'); t.doesNotThrow(() => FileBoxTest2.setUuidSaver((_) => Promise.resolve('uuid')), 'should not throw for set loader for the first time'); t.throws(() => FileBoxTest2.setUuidSaver((_) => Promise.resolve('uuid')), 'should throw for set loader twice'); }); test('setUuidLoader() & setUuidSsaver() with `this`', async (t) => { const sandbox = sinon.createSandbox(); const loader = sandbox.stub() .returns(await FileBox.fromQRCode('qr').toStream()); const saver = sandbox.stub() .returns('uuid'); class FileBoxTest extends FileBox { } FileBoxTest.setUuidLoader(loader); FileBoxTest.setUuidSaver(saver); const fileBox = FileBoxTest.fromUuid('uuid', 'test.txt'); await fileBox.toBuffer(); t.equal(loader.thisValues[0], fileBox, 'should call loader with `this`'); const fileBox2 = FileBoxTest.fromBuffer(Buffer.from('test'), 'test.txt'); await fileBox2.toUuid(); t.equal(saver.thisValues[0], fileBox2, 'should call saver with `this`'); }); test('FileBox.validInterface()', async (t) => { const fileBox = FileBox.fromQRCode('test'); /** * 2 OK */ t.ok(FileBox.validInstance(fileBox), 'should satisfy instance validation for a FileBox instance'); t.ok(FileBox.validInterface(fileBox), 'should satisfy interface validation for a FileBox instance'); t.ok(FileBox.valid(fileBox), 'should satisfy interface validation'); t.ok(fileBox instanceof FileBox, 'should be instance of FileBox'); const copy = {}; Object.getOwnPropertyNames(Object.getPrototypeOf(fileBox)).forEach(prop => { copy[prop] = fileBox[prop]; }); function NOT_FILE_BOX_CONSTRUCTOR() { } const target = { ...copy, constructor: NOT_FILE_BOX_CONSTRUCTOR, }; /** * 1 OK, 1 NG */ t.ok(FileBox.validInstance(target), 'should pass instance validation instance test'); t.ok(FileBox.validInterface(target), 'should pass interface validation for an object with FileBox properties'); t.ok(FileBox.valid(target), 'should satisfy interface validation'); t.ok(target instanceof FileBox, 'should be instance of FileBox'); /** * 2 NG */ delete target.size; t.notOk(FileBox.validInterface(target), 'should not be a valid interface if it lack any property'); t.notOk(FileBox.valid(target), 'should not satisfy interface validation'); }); //# sourceMappingURL=file-box.spec.js.map