diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c117b61..eb011f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: node-version: ${{steps.get-version.outputs.node}} - run: npm install + - run: npm install domexception - run: npm run report -- --colors diff --git a/README.md b/README.md index 1286c25..53c81f5 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,24 @@ fetch('https://httpbin.org/post', { .then(json => console.log(json)); ``` +### Blob part backed up by filesystem +To use, install [domexception](https://github.com/jsdom/domexception). + +```sh +npm install fetch-blob domexception +``` + +```js +const blobFrom = require('fetch-blob/from.js'); +const blob1 = blobFrom('./2-GiB-file.bin'); +const blob2 = blobFrom('./2-GiB-file.bin'); + +// Not a 4 GiB memory snapshot, just holds 3 references +// points to where data is located on the disk +const blob = new Blob([blob1, blob2]); +console.log(blob.size) // 4 GiB +``` + See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and [tests](https://github.com/node-fetch/fetch-blob/blob/master/test.js) for more details. [npm-image]: https://flat.badgen.net/npm/v/fetch-blob diff --git a/from.js b/from.js new file mode 100644 index 0000000..8d60de8 --- /dev/null +++ b/from.js @@ -0,0 +1,57 @@ +const {statSync, createReadStream} = require('fs'); +const Blob = require('.'); +const DOMException = require('domexception'); + +/** + * @param {string} path filepath on the disk + * @returns {Blob} + */ +function blobFrom(path) { + const {size, mtime} = statSync(path); + const blob = new BlobDataItem({path, size, mtime}); + + return new Blob([blob]); +} + +/** + * This is a blob backed up by a file on the disk + * with minium requirement + * + * @private + */ +class BlobDataItem { + constructor(options) { + this.size = options.size; + this.path = options.path; + this.start = options.start; + this.mtime = options.mtime; + } + + // Slicing arguments is validated and formated + // by Blob.prototype.slice + slice(start, end) { + return new BlobDataItem({ + path: this.path, + start, + mtime: this.mtime, + size: end - start + }); + } + + stream() { + if (statSync(this.path).mtime > this.mtime) { + throw new DOMException('The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.', 'NotReadableError'); + } + + return createReadStream(this.path, { + start: this.start, + end: this.start + this.size - 1 + }); + } + + get [Symbol.toStringTag]() { + return 'Blob'; + } +} + +module.exports = blobFrom; diff --git a/package.json b/package.json index 68a848c..9a7e74c 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "A Blob implementation in Node.js, originally from node-fetch.", "main": "index.js", "files": [ + "from.js", "index.js", "index.d.ts" ], @@ -48,5 +49,7 @@ } ] }, - "dependencies": {} + "peerDependencies": { + "domexception": "^2.0.1" + } } diff --git a/test.js b/test.js index ae73beb..3a7b23b 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,7 @@ +const fs = require('fs'); const test = require('ava'); const Blob = require('.'); +const blobFrom = require('./from'); const getStream = require('get-stream'); const {Response} = require('node-fetch'); const {TextDecoder} = require('util'); @@ -142,3 +144,19 @@ test('Blob works with node-fetch Response.text()', async t => { const text = await response.text(); t.is(text, data); }); + +test('blob part backed up by filesystem', async t => { + const blob = blobFrom('./LICENSE'); + t.is(await blob.slice(0, 3).text(), 'MIT'); + t.is(await blob.slice(4, 11).text(), 'License'); +}); + +test('Reading after modified should fail', async t => { + const blob = blobFrom('./LICENSE'); + await new Promise(resolve => setTimeout(resolve, 100)); + const now = new Date(); + // Change modified time + fs.utimesSync('./LICENSE', now, now); + const error = await blob.text().catch(error => error); + t.is(error.name, 'NotReadableError'); +});