From 843748fcd720194d4acba7d49bb4da9178818635 Mon Sep 17 00:00:00 2001 From: Rafael Oleza Date: Fri, 26 Oct 2018 16:26:26 -0700 Subject: [PATCH] Add support for storing/retrieving binary data in metro-cache Reviewed By: mjesun Differential Revision: D10868923 fbshipit-source-id: 9e849b483ade5d97a4194155615f05cd70559593 --- packages/metro-cache/src/stores/FileStore.js | 22 +++++++- packages/metro-cache/src/stores/HttpStore.js | 23 ++++++-- .../src/stores/__tests__/FileStore-test.js | 9 +++ .../src/stores/__tests__/HttpStore-test.js | 56 +++++++++++++++++++ 4 files changed, 104 insertions(+), 6 deletions(-) diff --git a/packages/metro-cache/src/stores/FileStore.js b/packages/metro-cache/src/stores/FileStore.js index b99f3dd6cb..9fb86b8d80 100644 --- a/packages/metro-cache/src/stores/FileStore.js +++ b/packages/metro-cache/src/stores/FileStore.js @@ -15,6 +15,9 @@ const mkdirp = require('mkdirp'); const path = require('path'); const rimraf = require('rimraf'); +const NULL_BYTE = 0x00; +const NULL_BYTE_BUFFER = new Buffer([NULL_BYTE]); + export type Options = {| root: string, |}; @@ -29,7 +32,13 @@ class FileStore { get(key: Buffer): ?T { try { - return JSON.parse(fs.readFileSync(this._getFilePath(key), 'utf8')); + const data = fs.readFileSync(this._getFilePath(key)); + + if (data[0] === NULL_BYTE) { + return (data.slice(1): any); + } else { + return JSON.parse(data.toString('utf8')); + } } catch (err) { if (err.code === 'ENOENT') { return null; @@ -40,7 +49,16 @@ class FileStore { } set(key: Buffer, value: T): void { - fs.writeFileSync(this._getFilePath(key), JSON.stringify(value)); + if (value instanceof Buffer) { + const fd = fs.openSync(this._getFilePath(key), 'w'); + + fs.writeSync(fd, NULL_BYTE_BUFFER); + fs.writeSync(fd, value); + + fs.closeSync(fd); + } else { + fs.writeFileSync(this._getFilePath(key), JSON.stringify(value)); + } } clear() { diff --git a/packages/metro-cache/src/stores/HttpStore.js b/packages/metro-cache/src/stores/HttpStore.js index 5cdb665e6d..6596d715a1 100644 --- a/packages/metro-cache/src/stores/HttpStore.js +++ b/packages/metro-cache/src/stores/HttpStore.js @@ -28,6 +28,9 @@ const ZLIB_OPTIONS = { level: 9, }; +const NULL_BYTE = 0x00; +const NULL_BYTE_BUFFER = new Buffer([NULL_BYTE]); + class HttpStore { static HttpError = HttpError; static NetworkError = NetworkError; @@ -82,7 +85,7 @@ class HttpStore { const req = this._module.request(options, res => { const code = res.statusCode; - let data = ''; + const data = []; if (code === 404) { res.resume(); @@ -99,7 +102,7 @@ class HttpStore { const gunzipped = res.pipe(zlib.createGunzip()); gunzipped.on('data', chunk => { - data += chunk.toString(); + data.push(chunk); }); gunzipped.on('error', err => { @@ -108,7 +111,13 @@ class HttpStore { gunzipped.on('end', () => { try { - resolve(JSON.parse(data)); + const buffer = Buffer.concat(data); + + if (buffer.length > 0 && buffer[0] === NULL_BYTE) { + resolve((buffer.slice(1): any)); + } else { + resolve(JSON.parse(buffer.toString('utf8'))); + } } catch (err) { reject(err); } @@ -152,7 +161,13 @@ class HttpStore { }); gzip.pipe(req); - gzip.end(JSON.stringify(value) || 'null'); + + if (value instanceof Buffer) { + gzip.write(NULL_BYTE_BUFFER); + gzip.end(value); + } else { + gzip.end(JSON.stringify(value) || 'null'); + } }); } diff --git a/packages/metro-cache/src/stores/__tests__/FileStore-test.js b/packages/metro-cache/src/stores/__tests__/FileStore-test.js index 9082d4fff0..84e4aca71c 100644 --- a/packages/metro-cache/src/stores/__tests__/FileStore-test.js +++ b/packages/metro-cache/src/stores/__tests__/FileStore-test.js @@ -40,4 +40,13 @@ describe('FileStore', () => { expect(fileStore.get(cache)).toEqual(null); }); + + it('reads and writes binary data', () => { + const fileStore = new FileStore({root: '/root'}); + const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); + const data = Buffer.from([0xca, 0xc4, 0xe5]); + + fileStore.set(cache, data); + expect(fileStore.get(cache)).toEqual(data); + }); }); diff --git a/packages/metro-cache/src/stores/__tests__/HttpStore-test.js b/packages/metro-cache/src/stores/__tests__/HttpStore-test.js index 0ebfa5d95a..24c5165c1c 100644 --- a/packages/metro-cache/src/stores/__tests__/HttpStore-test.js +++ b/packages/metro-cache/src/stores/__tests__/HttpStore-test.js @@ -187,4 +187,60 @@ describe('HttpStore', () => { done(); }); }); + + it('gets the same value that was set', async () => { + const store = new HttpStore({endpoint: 'http://www.example.com/endpoint'}); + const chunks = []; + let storedValue; + + httpPassThrough.on('data', chunk => { + chunks.push(chunk); + }); + + httpPassThrough.on('end', () => { + storedValue = zlib.gunzipSync(Buffer.concat(chunks)); + + const callbackSet = require('http').request.mock.calls[0][1]; + + callbackSet(responseHttpOk('')); + }); + + await store.set(Buffer.from('key-set'), {foo: 42}); + + const promiseGet = store.get(Buffer.from('key-set')); + const callbackGet = require('http').request.mock.calls[1][1]; + + callbackGet(responseHttpOk(storedValue)); + + expect(await promiseGet).toEqual({foo: 42}); + }); + + it('gets the same value that was set when storing buffers', async () => { + const store = new HttpStore({endpoint: 'http://www.example.com/endpoint'}); + const chunks = []; + let storedValue; + + httpPassThrough.on('data', chunk => { + chunks.push(chunk); + }); + + httpPassThrough.on('end', () => { + storedValue = zlib.gunzipSync(Buffer.concat(chunks)); + + const callbackSet = require('http').request.mock.calls[0][1]; + + callbackSet(responseHttpOk('')); + }); + + const bufferValue = new Buffer([0xfb, 0xca, 0xc4]); + + await store.set(Buffer.from('key-set'), bufferValue); + + const promiseGet = store.get(Buffer.from('key-set')); + const callbackGet = require('http').request.mock.calls[1][1]; + + callbackGet(responseHttpOk(storedValue)); + + expect(await promiseGet).toEqual(bufferValue); + }); });