Compare commits

...
20 Commits
Author SHA1 Message Date
Jimmy Wärting 5b2bab5365 Update CHANGELOG.md 2020-07-28 17:48:58 +02:00
Jimmy Wärting b75fb395b4 bumped minor version to 1.2.0 2020-07-28 17:44:52 +02:00
Jimmy Wärting 3217da9bc7 Create blob backed up by filesystem (#55)
* create a blob backed up by filesystem
* added code coverage test for filesystem blobs
* updated readme for using filesystem
* install peer dep
* added from to files array in package.json
2020-07-28 17:42:31 +02:00
Jimmy Wärting ae6b565ae6 fix slice test
`added` bytes was calculated wrong
2020-07-24 01:56:18 +02:00
Irakli Gozalishvili 26ca0e39b4 create test illustrating a bug 2020-07-24 01:56:18 +02:00
Antoni Kepinski 99388a79bb update changelog 2020-06-11 16:31:21 +02:00
Antoni Kepinski cf379d10e0 2.0.1 2020-06-11 16:30:40 +02:00
Pim 1d1faebe18 fix: remove upper bound for node engine semver (#49) 2020-06-11 16:29:53 +02:00
Konstantin Vyatkin 903a4c1bb0 use Symbol.hasInstance (#46) 2020-06-11 10:00:58 -04:00
Konstantin Vyatkin 126b4ec697 Switch to GitHub Actions (#47)
* remove travis

* add codecov partial line coverage

* switch coverage to c8

* add GitHub Actions

* update badge

* import TextDecoder from util

* lint
2020-06-11 10:01:22 +02:00
Antoni Kepinski 38639ec4aa release 2.0.0 2020-06-10 21:09:47 +02:00
Antoni Kepinski 5608cef75c update changelog 2020-06-10 21:08:54 +02:00
Antoni Kepinski 62fc8eab22 1.0.7 2020-06-10 16:14:25 +02:00
Antoni Kepinski 1a3c424a07 update nyc 2020-06-10 16:13:07 +02:00
Antoni Kepinski f4fa7e5825 Update CHANGELOG.md 2020-06-10 16:12:41 +02:00
Konstantin Vyatkin a1fef8cd0b Create TypeScript declarations from JSDoc (#45) 2020-06-10 16:09:02 +02:00
dependabot-preview[bot]anddependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> 1ec92a63b2 Bump xo from 0.30.0 to 0.32.0 (#39)
Bumps [xo](https://github.com/xojs/xo) from 0.30.0 to 0.32.0.
- [Release notes](https://github.com/xojs/xo/releases)
- [Commits](https://github.com/xojs/xo/compare/v0.30.0...v0.32.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-09 20:22:03 +02:00
Jimmy Wärting fd4f45e70f Operate on blob parts (byte sequence) (#44)
* operate on blob parts

* jsdoc

* specify parts type

* Back to 100% (discovered a small bug)
2020-06-07 23:42:42 +02:00
Richie Bendall 9dc07473c6 Update formatting 2020-06-08 01:12:25 +12:00
Jimmy Wärting 870be1e2d5 Use a WeakMap for private properties (#42)
* options.buffer isn't a valid option

* truly private internal properties

* 100% test coverage
2020-06-06 00:09:32 -04:00
13 changed files with 465 additions and 172 deletions
+6
View File
@@ -0,0 +1,6 @@
coverage:
round: up
precision: 2
parsers:
javascript:
enable_partials: yes
+57
View File
@@ -0,0 +1,57 @@
name: CI
on:
push:
branches: [master]
pull_request:
paths:
- "**.js"
- "package.json"
- ".github/workflows/ci.yml"
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
node: ["14", "12", engines]
exclude:
# On Windows, run tests with only the LTS environments.
- os: windows-latest
node: engines
- os: windows-latest
node: "14"
# On macOS, run tests with only the LTS environments.
- os: macOS-latest
node: engines
- os: macOS-latest
node: "14"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Get Node.JS version from package.json
if: matrix.node == 'engines'
id: get-version
run: echo ::set-output name=node::$(npx --q minimum-node-version)
- uses: actions/setup-node@v2-beta
if: matrix.node != 'engines'
with:
node-version: ${{ matrix.node }}
- uses: actions/setup-node@v2-beta
if: matrix.node == 'engines'
with:
node-version: ${{steps.get-version.outputs.node}}
- run: npm install
- run: npm install domexception
- run: npm run report -- --colors
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
file: ./coverage/coverage-final.json
+20
View File
@@ -0,0 +1,20 @@
name: CI
on:
pull_request:
paths:
- "**.js"
- "**eslint**"
- "package.json"
- ".github/workflows/lint.yml"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2-beta
with:
node-version: 14
- run: npm install
- run: npm run lint
+1 -2
View File
@@ -62,5 +62,4 @@ typings/
# dotenv environment variables file
.env
# next.js build output
.next
index.d.ts
+2 -1
View File
@@ -1 +1,2 @@
package-lock=false
package-lock=false
save-exact=false
-13
View File
@@ -1,13 +0,0 @@
language: node_js
node_js:
- "lts/*"
- "node"
script:
- npm run lint
- npm run coverage
cache:
directories:
- node_modules
+19
View File
@@ -1,6 +1,25 @@
Changelog
=========
## v2.1.0
- Fix: .slice has an implementation bug (#54).
- Added blob backed up by filesystem (#55)
## v2.0.1
- Fix: remove upper bound for node engine semver (#49).
## v2.0.0
> Note: This release was previously published as `1.0.7`, but as it contains breaking changes, we renamed it to `2.0.0`.
- **Breaking:** minimum supported Node.js version is now 10.17.
- **Breaking:** `buffer` option has been removed.
- Enhance: create TypeScript declarations from JSDoc (#45).
- Enhance: operate on blob parts (byte sequence) (#44).
- Enhance: use a `WeakMap` for private properties (#42) .
- Other: update formatting.
## v1.0.6
- Enhance: use upstream Blob directly in typings (#38)
+23 -5
View File
@@ -1,7 +1,7 @@
# fetch-blob
[![npm version][npm-image]][npm-url]
[![build status][travis-image]][travis-url]
[![build status][ci-image]][ci-url]
[![coverage status][codecov-image]][codecov-url]
[![install size][install-size-image]][install-size-url]
@@ -10,7 +10,7 @@ A Blob implementation in Node.js, originally from [node-fetch](https://github.co
## Installation
```sh
$ npm install fetch-blob
npm install fetch-blob
```
## Usage
@@ -21,18 +21,36 @@ const fetch = require('node-fetch');
fetch('https://httpbin.org/post', {
method: 'POST',
body: new Blob(['hello'], { type: 'text/plain' })
body: new Blob(['Hello World'], { type: 'text/plain' })
})
.then(res => res.json());
.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
[npm-url]: https://www.npmjs.com/package/fetch-blob
[travis-image]: https://flat.badgen.net/travis/node-fetch/fetch-blob
[travis-url]: https://travis-ci.org/node-fetch/fetch-blob
[ci-image]: https://github.com/node-fetch/fetch-blob/workflows/CI/badge.svg
[ci-url]: https://github.com/node-fetch/fetch-blob/actions
[codecov-image]: https://flat.badgen.net/codecov/c/github/node-fetch/fetch-blob/master
[codecov-url]: https://codecov.io/gh/node-fetch/fetch-blob
[install-size-image]: https://flat.badgen.net/packagephobia/install/fetch-blob
+57
View File
@@ -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;
Vendored
-4
View File
@@ -1,4 +0,0 @@
/// <reference lib="dom"/>
/* global Blob */
export = Blob;
+138 -92
View File
@@ -1,119 +1,172 @@
// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js
// (MIT licensed)
const {Readable} = require('stream');
const {Readable: ReadableStream} = require('stream');
/**
* @type {WeakMap<Blob, {type: string, size: number, parts: (Blob | Buffer)[] }>}
*/
const wm = new WeakMap();
const BUFFER = Symbol('buffer');
const TYPE = Symbol('type');
async function * read(parts) {
for (const part of parts) {
if ('stream' in part) {
yield * part.stream();
} else {
yield part;
}
}
}
class Blob {
constructor(...args) {
this[TYPE] = '';
const blobParts = args[0];
const options = args[1];
const buffers = [];
/* eslint-disable-next-line no-unused-vars */
/**
* The Blob() constructor returns a new Blob object. The content
* of the blob consists of the concatenation of the values given
* in the parameter array.
*
* @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts
* @param {{ type?: string }} [options]
*/
constructor(blobParts = [], options = {type: ''}) {
let size = 0;
if (blobParts) {
blobParts.forEach(element => {
let buffer;
if (element instanceof Buffer) {
buffer = element;
} else if (ArrayBuffer.isView(element)) {
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
} else if (element instanceof ArrayBuffer) {
buffer = Buffer.from(element);
} else if (element instanceof Blob) {
buffer = element[BUFFER];
} else {
buffer = Buffer.from(typeof element === 'string' ? element : String(element));
}
const parts = blobParts.map(element => {
let buffer;
if (element instanceof Buffer) {
buffer = element;
} else if (ArrayBuffer.isView(element)) {
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
} else if (element instanceof ArrayBuffer) {
buffer = Buffer.from(element);
} else if (element instanceof Blob) {
buffer = element;
} else {
buffer = Buffer.from(typeof element === 'string' ? element : String(element));
}
size += buffer.length;
buffers.push(buffer);
});
}
size += buffer.length || buffer.size || 0;
return buffer;
});
this[BUFFER] = Buffer.concat(buffers);
const type = options.type === undefined ? '' : String(options.type).toLowerCase();
const type = options && options.type !== undefined && String(options.type).toLowerCase();
if (type && !/[^\u0020-\u007E]/.test(type)) {
this[TYPE] = type;
}
if (options && Buffer.isBuffer(options.buffer)) {
this[BUFFER] = options.buffer;
}
wm.set(this, {
type: /[^\u0020-\u007E]/.test(type) ? '' : type,
size,
parts
});
}
/**
* The Blob interface's size property returns the
* size of the Blob in bytes.
*/
get size() {
return this[BUFFER].length;
return wm.get(this).size;
}
/**
* The type property of a Blob object returns the MIME type of the file.
*/
get type() {
return this[TYPE];
return wm.get(this).type;
}
text() {
return Promise.resolve(this[BUFFER].toString());
/**
* The text() method in the Blob interface returns a Promise
* that resolves with a string containing the contents of
* the blob, interpreted as UTF-8.
*
* @return {Promise<string>}
*/
async text() {
return Buffer.from(await this.arrayBuffer()).toString();
}
arrayBuffer() {
const buf = this[BUFFER];
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
return Promise.resolve(ab);
/**
* The arrayBuffer() method in the Blob interface returns a
* Promise that resolves with the contents of the blob as
* binary data contained in an ArrayBuffer.
*
* @return {Promise<ArrayBuffer>}
*/
async arrayBuffer() {
const data = new Uint8Array(this.size);
let offset = 0;
for await (const chunk of this.stream()) {
data.set(chunk, offset);
offset += chunk.length;
}
return data.buffer;
}
/**
* The Blob interface's stream() method is difference from native
* and uses node streams instead of whatwg streams.
*
* @returns {Readable} Node readable stream
*/
stream() {
const readable = new ReadableStream();
readable._read = () => { };
readable.push(this[BUFFER]);
readable.push(null);
return readable;
return Readable.from(read(wm.get(this).parts));
}
toString() {
return '[object Blob]';
}
slice(...args) {
/**
* The Blob interface's slice() method creates and returns a
* new Blob object which contains data from a subset of the
* blob on which it's called.
*
* @param {number} [start]
* @param {number} [end]
* @param {string} [type]
*/
slice(start = 0, end = this.size, type = '') {
const {size} = this;
const start = args[0];
const end = args[1];
let relativeStart;
let relativeEnd;
if (start === undefined) {
relativeStart = 0;
} else if (start < 0) {
relativeStart = Math.max(size + start, 0);
} else {
relativeStart = Math.min(start, size);
}
if (end === undefined) {
relativeEnd = size;
} else if (end < 0) {
relativeEnd = Math.max(size + end, 0);
} else {
relativeEnd = Math.min(end, size);
}
let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size);
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
const span = Math.max(relativeEnd - relativeStart, 0);
const parts = wm.get(this).parts.values();
const blobParts = [];
let added = 0;
for (const part of parts) {
const size = ArrayBuffer.isView(part) ? part.byteLength : part.size;
if (relativeStart && size <= relativeStart) {
// Skip the beginning and change the relative
// start & end position as we skip the unwanted parts
relativeStart -= size;
relativeEnd -= size;
} else {
const chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
blobParts.push(chunk);
added += ArrayBuffer.isView(chunk) ? chunk.byteLength : chunk.size;
relativeStart = 0; // All next sequental parts should start at 0
// don't add the overflow to new blobParts
if (added >= span) {
break;
}
}
}
const blob = new Blob([], {type});
Object.assign(wm.get(blob), {size: span, parts: blobParts});
const buffer = this[BUFFER];
const slicedBuffer = buffer.slice(
relativeStart,
relativeStart + span
);
const blob = new Blob([], {type: args[2]});
blob[BUFFER] = slicedBuffer;
return blob;
}
get [Symbol.toStringTag]() {
return 'Blob';
}
static [Symbol.hasInstance](object) {
return (
typeof object === 'object' &&
typeof object.stream === 'function' &&
object.stream.length === 0 &&
typeof object.constructor === 'function' &&
/^(Blob|File)$/.test(object[Symbol.toStringTag])
);
}
}
Object.defineProperties(Blob.prototype, {
@@ -122,11 +175,4 @@ Object.defineProperties(Blob.prototype, {
slice: {enumerable: true}
});
Object.defineProperty(Blob.prototype, Symbol.toStringTag, {
value: 'Blob',
writable: false,
enumerable: false,
configurable: true
});
module.exports = Blob;
+53 -47
View File
@@ -1,49 +1,55 @@
{
"name": "fetch-blob",
"version": "1.0.6",
"description": "A Blob implementation in Node.js, originally from node-fetch.",
"main": "index.js",
"files": [
"index.js",
"index.d.ts"
],
"scripts": {
"lint": "xo",
"test": "xo && ava",
"report": "nyc ava",
"coverage": "nyc --reporter json --reporter text ava && codecov -f coverage/coverage-final.json"
},
"repository": "https://github.com/node-fetch/fetch-blob.git",
"keywords": [
"blob",
"node-fetch"
],
"engines": {
"node": ">=6"
},
"author": "David Frank",
"license": "MIT",
"bugs": {
"url": "https://github.com/node-fetch/fetch-blob/issues"
},
"homepage": "https://github.com/node-fetch/fetch-blob#readme",
"devDependencies": {
"ava": "^3.8.2",
"codecov": "^3.7.0",
"get-stream": "^5.1.0",
"node-fetch": "^2.6.0",
"nyc": "^15.0.1",
"xo": "^0.30.0"
},
"xo": {
"overrides": [
{
"files": "test.js",
"rules": {
"node/no-unsupported-features/es-syntax": 0,
"node/no-unsupported-features/node-builtins": 0
}
}
]
}
"name": "fetch-blob",
"version": "2.1.0",
"description": "A Blob implementation in Node.js, originally from node-fetch.",
"main": "index.js",
"files": [
"from.js",
"index.js",
"index.d.ts"
],
"scripts": {
"lint": "xo",
"test": "xo && ava",
"report": "c8 --reporter json --reporter text ava",
"coverage": "c8 --reporter json --reporter text ava && codecov -f coverage/coverage-final.json",
"prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js"
},
"repository": "https://github.com/node-fetch/fetch-blob.git",
"keywords": [
"blob",
"node-fetch"
],
"engines": {
"node": "^10.17.0 || >=12.3.0"
},
"author": "David Frank",
"license": "MIT",
"bugs": {
"url": "https://github.com/node-fetch/fetch-blob/issues"
},
"homepage": "https://github.com/node-fetch/fetch-blob#readme",
"devDependencies": {
"ava": "^3.8.2",
"c8": "^7.2.0",
"codecov": "^3.7.0",
"get-stream": "^5.1.0",
"node-fetch": "^2.6.0",
"typescript": "^3.9.5",
"xo": "^0.32.0"
},
"xo": {
"overrides": [
{
"files": "test.js",
"rules": {
"node/no-unsupported-features/es-syntax": 0,
"node/no-unsupported-features/node-builtins": 0
}
}
]
},
"peerDependencies": {
"domexception": "^2.0.1"
}
}
+89 -8
View File
@@ -1,14 +1,37 @@
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');
test('Blob ctor', t => {
test('new Blob()', t => {
const blob = new Blob(); // eslint-disable-line no-unused-vars
t.pass();
});
test('new Blob(parts)', t => {
const data = 'a=1';
const blob = new Blob([data]); // eslint-disable-line no-unused-vars
t.pass();
});
test('Blob ctor parts', async t => {
const parts = [
'a',
new Uint8Array([98]),
new Uint16Array([25699]),
new Uint8Array([101]).buffer,
Buffer.from('f'),
new Blob(['g']),
{}
];
const blob = new Blob(parts);
t.is(await blob.text(), 'abcdefg[object Object]');
});
test('Blob size', t => {
const data = 'a=1';
const blob = new Blob([data]);
@@ -16,12 +39,27 @@ test('Blob size', t => {
});
test('Blob type', t => {
const data = 'a=1';
const type = 'text/plain';
const blob = new Blob([data], {type});
const blob = new Blob([], {type});
t.is(blob.type, type);
});
test('Blob slice type', t => {
const type = 'text/plain';
const blob = new Blob().slice(0, 0, type);
t.is(blob.type, type);
});
test('invalid Blob type', t => {
const blob = new Blob([], {type: '\u001Ftext/plain'});
t.is(blob.type, '');
});
test('invalid Blob slice type', t => {
const blob = new Blob().slice(0, 0, '\u001Ftext/plain');
t.is(blob.type, '');
});
test('Blob text()', async t => {
const data = 'a=1';
const type = 'text/plain';
@@ -55,11 +93,38 @@ test('Blob toString()', t => {
});
test('Blob slice()', async t => {
const data = 'a=1';
const type = 'text/plain';
const blob = new Blob([data], {type});
const blob2 = blob.slice(0, 1);
t.is(await blob2.text(), data.slice(0, 1));
const data = 'abcdefgh';
const blob = new Blob([data]).slice();
t.is(await blob.text(), data);
});
test('Blob slice(0, 1)', async t => {
const data = 'abcdefgh';
const blob = new Blob([data]).slice(0, 1);
t.is(await blob.text(), 'a');
});
test('Blob slice(-1)', async t => {
const data = 'abcdefgh';
const blob = new Blob([data]).slice(-1);
t.is(await blob.text(), 'h');
});
test('Blob slice(0, -1)', async t => {
const data = 'abcdefgh';
const blob = new Blob([data]).slice(0, -1);
t.is(await blob.text(), 'abcdefg');
});
test('Blob(["hello ", "world"]).slice(5)', async t => {
const parts = ['hello ', 'world'];
const blob = new Blob(parts);
t.is(await blob.slice(5).text(), ' world');
});
test('throw away unwanted parts', async t => {
const blob = new Blob(['a', 'b', 'c']).slice(1, 2);
t.is(await blob.text(), 'b');
});
test('Blob works with node-fetch Response.blob()', async t => {
@@ -79,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');
});