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
JavaScript
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