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)
474 lines (367 loc) • 16 kB
text/typescript
import 'reflect-metadata'
import assert from 'assert'
import {
PassThrough,
Readable,
} 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: Function) => {
console.info(constructor.name)
console.info(constructor.prototype.name)
}
},
methodFixture () {
return (
..._: any[]
// target : Object,
// propertyKey : string,
// descriptor : PropertyDescriptor,
) => {
console.info('@fixture()')
}
},
parameterFixture () {
return (target: object, propertyKey: string | symbol, parameterIndex: number) => {
console.info(propertyKey)
const existingRequiredParameters: number[] = 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')
})
.classFixture()
export class FixtureFileBox {
.methodFixture()
public static localFileFixture () {
return {
content: 'T',
name: 'test.txt',
size: '1',
type: 'plain/text',
}
}
}
// tslint:disable:max-classes-per-file
export class TestFileBox {
public static testFileCreateLocal (
.parameterFixture() localFileFixture: any,
) {
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')
})
}
}
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 override fromUrl (...args: any[]): FileBoxTest { return (super.fromUrl as any)(...args) }
override _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: {
[key in typeof process.platform]?: Object
} = {
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 as FileBox).metadata, {}, 'should get a empty {} if not set')
t.doesNotThrow(
() => {
(fileBox as FileBox).metadata = EXPECTED_METADATA
},
'should not throw for set metadata for the first time',
)
t.throws(
() => {
(fileBox as FileBox).metadata = EXPECTED_METADATA
},
'should throw for set metadata again',
)
t.throws(
() => {
(fileBox as FileBox).metadata['mol'] = EXPECTED_MOL
},
'should throw for change value of a property on metadata',
)
t.same((fileBox as 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((_: string) => 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((_: string) => ({} as any)), 'should not throw for set loader for the first time')
t.throws(() => FileBoxTest1.setUuidLoader((_: string) => ({} as any)), 'should throw for set loader twice')
t.doesNotThrow(() => FileBoxTest2.setUuidLoader((_: string) => ({} as any)), 'should not throw for set loader for the first time')
t.throws(() => FileBoxTest2.setUuidLoader((_: string) => ({} as any)), 'should throw for set loader twice')
})
test('setUuidSaver()', async t => {
class FileBoxTest1 extends FileBox {}
class FileBoxTest2 extends FileBox {}
t.doesNotThrow(() => FileBoxTest1.setUuidSaver((_: Readable) => Promise.resolve('uuid')), 'should not throw for set loader for the first time')
t.throws(() => FileBoxTest1.setUuidSaver((_: Readable) => Promise.resolve('uuid')), 'should throw for set loader twice')
t.doesNotThrow(() => FileBoxTest2.setUuidSaver((_: Readable) => Promise.resolve('uuid')), 'should not throw for set loader for the first time')
t.throws(() => FileBoxTest2.setUuidSaver((_: Readable) => 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 = {} as any
Object.getOwnPropertyNames(
Object.getPrototypeOf(fileBox),
).forEach(prop => {
copy[prop] = (fileBox as FileBox)[prop as keyof FileBox]
})
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')
})