Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4747497b52 | ||
|
|
09fd40448a | ||
|
|
8d14e88040 | ||
|
|
8ef89adad4 | ||
|
|
a3b0d62b9d | ||
|
|
97a084b5e8 | ||
|
|
a8fed4e338 | ||
|
|
483973cf25 | ||
|
|
cc929f3714 | ||
|
|
5584886f0f | ||
|
|
0d2cd8b9b7 | ||
|
|
149030ddbe | ||
|
|
f08e6a6773 | ||
|
|
eed6a1dddd | ||
|
|
3b12a39ea2 | ||
|
|
591a28ed94 | ||
|
|
40f4568c95 | ||
|
|
a74fb90d95 | ||
|
|
689bee122b | ||
|
|
50b89fcdce | ||
|
|
40c6ff45df | ||
|
|
5b2bab5365 | ||
|
|
b75fb395b4 | ||
|
|
3217da9bc7 | ||
|
|
ae6b565ae6 | ||
|
|
26ca0e39b4 | ||
|
|
99388a79bb | ||
|
|
cf379d10e0 | ||
|
|
1d1faebe18 | ||
|
|
903a4c1bb0 | ||
|
|
126b4ec697 | ||
|
|
38639ec4aa | ||
|
|
5608cef75c |
@@ -0,0 +1,6 @@
|
|||||||
|
coverage:
|
||||||
|
round: up
|
||||||
|
precision: 2
|
||||||
|
parsers:
|
||||||
|
javascript:
|
||||||
|
enable_partials: yes
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
ignore:
|
||||||
|
- dependency-name: get-stream
|
||||||
|
versions:
|
||||||
|
- 6.0.0
|
||||||
|
- 6.0.1
|
||||||
|
- dependency-name: typescript
|
||||||
|
versions:
|
||||||
|
- 4.1.3
|
||||||
|
- 4.1.4
|
||||||
|
- 4.1.5
|
||||||
|
- 4.2.2
|
||||||
|
- 4.2.3
|
||||||
|
- 4.2.4
|
||||||
|
- dependency-name: xo
|
||||||
|
versions:
|
||||||
|
- 0.37.1
|
||||||
|
- 0.38.1
|
||||||
|
- 0.38.2
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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: ["16", "15", "14", 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 run report -- --colors
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v1
|
||||||
|
with:
|
||||||
|
file: ./coverage/coverage-final.json
|
||||||
@@ -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
-1
@@ -62,4 +62,4 @@ typings/
|
|||||||
# dotenv environment variables file
|
# dotenv environment variables file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
index.d.ts
|
*.d.ts
|
||||||
|
|||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
|
|
||||||
node_js:
|
|
||||||
- "lts/*"
|
|
||||||
- "node"
|
|
||||||
|
|
||||||
script:
|
|
||||||
- npm run lint
|
|
||||||
- npm run coverage
|
|
||||||
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- node_modules
|
|
||||||
+39
-5
@@ -1,12 +1,46 @@
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
## v1.0.7
|
## v3.0.0
|
||||||
|
- Changed WeakMap for private field (require node 12)
|
||||||
|
- Switch to ESM
|
||||||
|
- blob.stream() return a subset of whatwg stream which is the async iterable part
|
||||||
|
(it no longer return a node stream)
|
||||||
|
- Reduced the dependency of Buffer by changing to global TextEncoder/Decoder (require node 11)
|
||||||
|
- Disabled xo since it could understand private fields (#)
|
||||||
|
- No longer transform the type to lowercase (https://github.com/w3c/FileAPI/issues/43)
|
||||||
|
This is more loose than strict, keys should be lowercased, but values should not.
|
||||||
|
It would require a more proper mime type parser - so we just made it loose.
|
||||||
|
- index.js and file.js can now be imported by browser & deno since it no longer depends on any
|
||||||
|
core node features (but why would you?)
|
||||||
|
- Implemented a File class
|
||||||
|
|
||||||
- Enhance: create TypeScript declarations from JSDoc (#45)
|
## v2.1.2
|
||||||
- Enhance: operate on blob parts (byte sequence) (#44)
|
- Fixed a bug where `start` in BlobDataItem was undefined (#85)
|
||||||
- Enhance: use a `WeakMap` for private properties (#42)
|
|
||||||
- Other: update formatting
|
## v2.1.1
|
||||||
|
- Add nullish values checking in Symbol.hasInstance (#82)
|
||||||
|
- Add generated typings for from.js file (#80)
|
||||||
|
- Updated dev dependencies
|
||||||
|
|
||||||
|
## 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
|
## v1.0.6
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# fetch-blob
|
# fetch-blob
|
||||||
|
|
||||||
[![npm version][npm-image]][npm-url]
|
[![npm version][npm-image]][npm-url]
|
||||||
[![build status][travis-image]][travis-url]
|
[![build status][ci-image]][ci-url]
|
||||||
[![coverage status][codecov-image]][codecov-url]
|
[![coverage status][codecov-image]][codecov-url]
|
||||||
[![install size][install-size-image]][install-size-url]
|
[![install size][install-size-image]][install-size-url]
|
||||||
|
|
||||||
@@ -13,27 +13,136 @@ A Blob implementation in Node.js, originally from [node-fetch](https://github.co
|
|||||||
npm install fetch-blob
|
npm install fetch-blob
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Upgrading from 2x to 3x</summary>
|
||||||
|
|
||||||
|
Updating from 2 to 3 should be a breeze since there is not many changes to the blob specification.
|
||||||
|
The major cause of a major release is coding standards.
|
||||||
|
- internal WeakMaps was replaced with private fields
|
||||||
|
- internal Buffer.from was replaced with TextEncoder/Decoder
|
||||||
|
- internal buffers was replaced with Uint8Arrays
|
||||||
|
- CommonJS was replaced with ESM
|
||||||
|
- The node stream returned by calling `blob.stream()` was replaced with a simple generator function that yields Uint8Array (Breaking change)
|
||||||
|
(Read "Differences from other blobs" for more info.)
|
||||||
|
|
||||||
|
All of this changes have made it dependency free of any core node modules, so it would be possible to just import it using http-import from a CDN without any bundling
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Differences from other Blobs</summary>
|
||||||
|
|
||||||
|
- Unlike NodeJS `buffer.Blob` (Added in: v15.7.0) and browser native Blob this polyfilled version can't be sent via PostMessage
|
||||||
|
- This blob version is more arbitrary, it can be constructed with blob parts that isn't a instance of itself
|
||||||
|
it has to look and behave as a blob to be accepted as a blob part.
|
||||||
|
- The benefit of this is that you can create other types of blobs that don't contain any internal data that has to be read in other ways, such as the `BlobDataItem` created in `from.js` that wraps a file path into a blob-like item and read lazily (nodejs plans to [implement this][fs-blobs] as well)
|
||||||
|
- The `blob.stream()` is the most noticeable differences. It returns a AsyncGeneratorFunction that yields Uint8Arrays
|
||||||
|
|
||||||
|
The reasoning behind `Blob.prototype.stream()` is that NodeJS readable stream
|
||||||
|
isn't spec compatible with whatwg streams and we didn't want to import the hole whatwg stream polyfill for node
|
||||||
|
or browserify NodeJS streams for the browsers and picking any flavor over the other. So we decided to opted out
|
||||||
|
of any stream and just implement the bear minium of what both streams have in common which is the asyncIterator
|
||||||
|
that both yields Uint8Array. this is the most isomorphic way with the use of `for-await-of` loops.
|
||||||
|
It would be redundant to convert anything to whatwg streams and than convert it back to
|
||||||
|
node streams since you work inside of Node.
|
||||||
|
It will probably stay like this until nodejs get native support for whatwg<sup>[1][https://github.com/nodejs/whatwg-stream]</sup> streams and whatwg stream add the node
|
||||||
|
equivalent for `Readable.from(iterable)`<sup>[2](https://github.com/whatwg/streams/issues/1018)</sup>
|
||||||
|
|
||||||
|
But for now if you really need a Node Stream then you can do so using this transformation
|
||||||
|
```js
|
||||||
|
import {Readable} from 'stream'
|
||||||
|
const stream = Readable.from(blob.stream())
|
||||||
|
```
|
||||||
|
But if you don't need it to be a stream then you can just use the asyncIterator part of it that is isomorphic.
|
||||||
|
```js
|
||||||
|
for await (const chunk of blob.stream()) {
|
||||||
|
console.log(chunk) // uInt8Array
|
||||||
|
}
|
||||||
|
```
|
||||||
|
If you need to make some feature detection to fix this different behavior
|
||||||
|
```js
|
||||||
|
if (Blob.prototype.stream?.constructor?.name === 'AsyncGeneratorFunction') {
|
||||||
|
// not spec compatible, monkey patch it...
|
||||||
|
// (Alternative you could extend the Blob and use super.stream())
|
||||||
|
let orig = Blob.prototype.stream
|
||||||
|
Blob.prototype.stream = function () {
|
||||||
|
const iterator = orig.call(this)
|
||||||
|
return new ReadableStream({
|
||||||
|
async pull (ctrl) {
|
||||||
|
const next = await iterator.next()
|
||||||
|
return next.done ? ctrl.close() : ctrl.enqueue(next.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Possible feature whatwg version: `ReadableStream.from(iterator)`
|
||||||
|
It's also possible to delete this method and instead use `.slice()` and `.arrayBuffer()` since it has both a public and private stream method
|
||||||
|
</details>
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const Blob = require('fetch-blob');
|
// Ways to import
|
||||||
const fetch = require('node-fetch');
|
// (PS it's dependency free ESM package so regular http-import from CDN works too)
|
||||||
|
import Blob from 'fetch-blob'
|
||||||
|
import File from 'fetch-blob/file.js'
|
||||||
|
|
||||||
fetch('https://httpbin.org/post', {
|
import {Blob} from 'fetch-blob'
|
||||||
method: 'POST',
|
import {File} from 'fetch-blob/file.js'
|
||||||
body: new Blob(['Hello World'], { type: 'text/plain' })
|
|
||||||
})
|
const {Blob} = await import('fetch-blob')
|
||||||
.then(res => res.json());
|
|
||||||
.then(json => console.log(json));
|
|
||||||
|
// Ways to read the blob:
|
||||||
|
const blob = new Blob(['hello, world'])
|
||||||
|
|
||||||
|
await blob.text()
|
||||||
|
await blob.arrayBuffer()
|
||||||
|
for await (let chunk of blob.stream()) { ... }
|
||||||
|
|
||||||
|
// turn the async iterator into a node stream
|
||||||
|
stream.Readable.from(blob.stream())
|
||||||
|
|
||||||
|
// turn the async iterator into a whatwg stream (feature)
|
||||||
|
globalThis.ReadableStream.from(blob.stream())
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
### Blob part backed up by filesystem
|
||||||
|
|
||||||
|
`fetch-blob/from.js` comes packed with tools to convert any filepath into either a Blob or a File
|
||||||
|
It will not read the content into memory. It will only stat the file for last modified date and file size.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// The default export is sync and use fs.stat to retrieve size & last modified as a blob
|
||||||
|
import blobFromSync from 'fetch-blob/from.js'
|
||||||
|
import {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync} from 'fetch-blob/from.js'
|
||||||
|
|
||||||
|
const fsFile = fileFromSync('./2-GiB-file.bin', 'application/octet-stream')
|
||||||
|
const fsBlob = await blobFrom('./2-GiB-file.mp4')
|
||||||
|
|
||||||
|
// Not a 4 GiB memory snapshot, just holds references
|
||||||
|
// points to where data is located on the disk
|
||||||
|
const blob = new Blob([fsFile, fsBlob, 'memory', new Uint8Array(10)])
|
||||||
|
console.log(blob.size) // ~4 GiB
|
||||||
|
```
|
||||||
|
|
||||||
|
`blobFrom|blobFromSync|fileFrom|fileFromSync(path, [mimetype])`
|
||||||
|
|
||||||
|
### Creating Blobs backed up by other async sources
|
||||||
|
Our Blob & File class are more generic then any other polyfills in the way that it can accept any blob look-a-like item
|
||||||
|
An example of this is that our blob implementation can be constructed with parts coming from [BlobDataItem](https://github.com/node-fetch/fetch-blob/blob/8ef89adad40d255a3bbd55cf38b88597c1cd5480/from.js#L32) (aka a filepath) or from [buffer.Blob](https://nodejs.org/api/buffer.html#buffer_new_buffer_blob_sources_options), It dose not have to implement all the methods - just enough that it can be read/understood by our Blob implementation. The minium requirements is that it has `Symbol.toStringTag`, `size`, `slice()` and either a `stream()` or a `arrayBuffer()` method. If you then wrap it in our Blob or File `new Blob([blobDataItem])` then you get all of the other methods that should be implemented in a blob or file
|
||||||
|
|
||||||
|
An example of this could be to create a file or blob like item coming from a remote HTTP request. Or from a DataBase
|
||||||
|
|
||||||
|
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 of how to use the Blob.
|
||||||
|
|
||||||
[npm-image]: https://flat.badgen.net/npm/v/fetch-blob
|
[npm-image]: https://flat.badgen.net/npm/v/fetch-blob
|
||||||
[npm-url]: https://www.npmjs.com/package/fetch-blob
|
[npm-url]: https://www.npmjs.com/package/fetch-blob
|
||||||
[travis-image]: https://flat.badgen.net/travis/node-fetch/fetch-blob
|
[ci-image]: https://github.com/node-fetch/fetch-blob/workflows/CI/badge.svg
|
||||||
[travis-url]: https://travis-ci.org/node-fetch/fetch-blob
|
[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-image]: https://flat.badgen.net/codecov/c/github/node-fetch/fetch-blob/master
|
||||||
[codecov-url]: https://codecov.io/gh/node-fetch/fetch-blob
|
[codecov-url]: https://codecov.io/gh/node-fetch/fetch-blob
|
||||||
[install-size-image]: https://flat.badgen.net/packagephobia/install/fetch-blob
|
[install-size-image]: https://flat.badgen.net/packagephobia/install/fetch-blob
|
||||||
[install-size-url]: https://packagephobia.now.sh/result?p=fetch-blob
|
[install-size-url]: https://packagephobia.now.sh/result?p=fetch-blob
|
||||||
|
[fs-blobs]: https://github.com/nodejs/node/issues/37340
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import Blob from './index.js';
|
||||||
|
|
||||||
|
export default class File extends Blob {
|
||||||
|
#lastModified = 0;
|
||||||
|
#name = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {*[]} fileBits
|
||||||
|
* @param {string} fileName
|
||||||
|
* @param {{lastModified?: number, type?: string}} options
|
||||||
|
*/ // @ts-ignore
|
||||||
|
constructor(fileBits, fileName, options = {}) {
|
||||||
|
if (arguments.length < 2) {
|
||||||
|
throw new TypeError(`Failed to construct 'File': 2 arguments required, but only ${arguments.length} present.`);
|
||||||
|
}
|
||||||
|
super(fileBits, options);
|
||||||
|
|
||||||
|
const modified = Number(options.lastModified);
|
||||||
|
this.#lastModified = Number.isNaN(modified) ? Date.now() : modified
|
||||||
|
this.#name = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.#name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastModified() {
|
||||||
|
return this.#lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return "File";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { File };
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import {statSync, createReadStream} from 'fs';
|
||||||
|
import {stat} from 'fs/promises';
|
||||||
|
import {basename} from 'path';
|
||||||
|
import File from './file.js';
|
||||||
|
import Blob from './index.js';
|
||||||
|
import {MessageChannel} from 'worker_threads';
|
||||||
|
|
||||||
|
const DOMException = globalThis.DOMException || (() => {
|
||||||
|
const port = new MessageChannel().port1
|
||||||
|
const ab = new ArrayBuffer(0)
|
||||||
|
try { port.postMessage(ab, [ab, ab]) }
|
||||||
|
catch (err) { return err.constructor }
|
||||||
|
})()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path filepath on the disk
|
||||||
|
* @param {string} [type] mimetype to use
|
||||||
|
*/
|
||||||
|
const blobFromSync = (path, type) => fromBlob(statSync(path), path, type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path filepath on the disk
|
||||||
|
* @param {string} [type] mimetype to use
|
||||||
|
*/
|
||||||
|
const blobFrom = (path, type) => stat(path).then(stat => fromBlob(stat, path, type));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path filepath on the disk
|
||||||
|
* @param {string} [type] mimetype to use
|
||||||
|
*/
|
||||||
|
const fileFrom = (path, type) => stat(path).then(stat => fromFile(stat, path, type));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path filepath on the disk
|
||||||
|
* @param {string} [type] mimetype to use
|
||||||
|
*/
|
||||||
|
const fileFromSync = (path, type) => fromFile(statSync(path), path, type);
|
||||||
|
|
||||||
|
const fromBlob = (stat, path, type = '') => new Blob([new BlobDataItem({
|
||||||
|
path,
|
||||||
|
size: stat.size,
|
||||||
|
lastModified: stat.mtimeMs,
|
||||||
|
start: 0
|
||||||
|
})], {type});
|
||||||
|
|
||||||
|
const fromFile = (stat, path, type = '') => new File([new BlobDataItem({
|
||||||
|
path,
|
||||||
|
size: stat.size,
|
||||||
|
lastModified: stat.mtimeMs,
|
||||||
|
start: 0
|
||||||
|
})], basename(path), { type, lastModified: stat.mtimeMs });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a blob backed up by a file on the disk
|
||||||
|
* with minium requirement. Its wrapped around a Blob as a blobPart
|
||||||
|
* so you have no direct access to this.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class BlobDataItem {
|
||||||
|
#path;
|
||||||
|
#start;
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
this.#path = options.path;
|
||||||
|
this.#start = options.start;
|
||||||
|
this.size = options.size;
|
||||||
|
this.lastModified = options.lastModified
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slicing arguments is first validated and formatted
|
||||||
|
* to not be out of range by Blob.prototype.slice
|
||||||
|
*/
|
||||||
|
slice(start, end) {
|
||||||
|
return new BlobDataItem({
|
||||||
|
path: this.#path,
|
||||||
|
lastModified: this.lastModified,
|
||||||
|
size: end - start,
|
||||||
|
start
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async * stream() {
|
||||||
|
const {mtimeMs} = await stat(this.#path)
|
||||||
|
if (mtimeMs > this.lastModified) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
if (this.size) {
|
||||||
|
yield * createReadStream(this.#path, {
|
||||||
|
start: this.#start,
|
||||||
|
end: this.#start + this.size - 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return 'Blob';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default blobFromSync;
|
||||||
|
export {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync};
|
||||||
@@ -1,71 +1,76 @@
|
|||||||
const {Readable} = require('stream');
|
// 64 KiB (same size chrome slice theirs blob into Uint8array's)
|
||||||
|
const POOL_SIZE = 65536;
|
||||||
|
|
||||||
/**
|
/** @param {(Blob | Uint8Array)[]} parts */
|
||||||
* @type {WeakMap<Blob, {type: string, size: number, parts: (Blob | Buffer)[] }>}
|
async function * toIterator (parts, clone = true) {
|
||||||
*/
|
for (let part of parts) {
|
||||||
const wm = new WeakMap();
|
|
||||||
|
|
||||||
async function * read(parts) {
|
|
||||||
for (const part of parts) {
|
|
||||||
if ('stream' in part) {
|
if ('stream' in part) {
|
||||||
yield * part.stream();
|
yield * part.stream();
|
||||||
|
} else if (ArrayBuffer.isView(part)) {
|
||||||
|
if (clone) {
|
||||||
|
let position = part.byteOffset;
|
||||||
|
let end = part.byteOffset + part.byteLength;
|
||||||
|
while (position !== end) {
|
||||||
|
const size = Math.min(end - position, POOL_SIZE);
|
||||||
|
const chunk = part.buffer.slice(position, position + size);
|
||||||
|
yield new Uint8Array(chunk);
|
||||||
|
position += chunk.byteLength;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
yield part;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
yield part;
|
// For blobs that have arrayBuffer but no stream method (nodes buffer.Blob)
|
||||||
|
let position = 0;
|
||||||
|
while (position !== part.size) {
|
||||||
|
const chunk = part.slice(position, Math.min(part.size, position + POOL_SIZE));
|
||||||
|
const buffer = await chunk.arrayBuffer();
|
||||||
|
position += buffer.byteLength;
|
||||||
|
yield new Uint8Array(buffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export default class Blob {
|
||||||
* @template T
|
|
||||||
* @param {T} object
|
/** @type {Array.<(Blob|Uint8Array)>} */
|
||||||
* @returns {T is Blob}
|
#parts = [];
|
||||||
*/
|
#type = '';
|
||||||
const isBlob = object => {
|
#size = 0;
|
||||||
return (
|
|
||||||
typeof object === 'object' &&
|
|
||||||
typeof object.stream === 'function' &&
|
|
||||||
typeof object.constructor === 'function' &&
|
|
||||||
/^(Blob|File)$/.test(object[Symbol.toStringTag])
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
class Blob {
|
|
||||||
/**
|
/**
|
||||||
* The Blob() constructor returns a new Blob object. The content
|
* The Blob() constructor returns a new Blob object. The content
|
||||||
* of the blob consists of the concatenation of the values given
|
* of the blob consists of the concatenation of the values given
|
||||||
* in the parameter array.
|
* in the parameter array.
|
||||||
*
|
*
|
||||||
* @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts
|
* @param {*} blobParts
|
||||||
* @param {{ type?: string }} [options]
|
* @param {{ type?: string }} [options]
|
||||||
*/
|
*/
|
||||||
constructor(blobParts = [], options = {type: ''}) {
|
constructor(blobParts = [], options = {}) {
|
||||||
let size = 0;
|
let size = 0;
|
||||||
|
|
||||||
const parts = blobParts.map(element => {
|
const parts = blobParts.map(element => {
|
||||||
let buffer;
|
let part;
|
||||||
if (Buffer.isBuffer(element)) {
|
if (ArrayBuffer.isView(element)) {
|
||||||
buffer = element;
|
part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength));
|
||||||
} else if (ArrayBuffer.isView(element)) {
|
|
||||||
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
|
|
||||||
} else if (element instanceof ArrayBuffer) {
|
} else if (element instanceof ArrayBuffer) {
|
||||||
buffer = Buffer.from(element);
|
part = new Uint8Array(element.slice(0));
|
||||||
} else if (isBlob(element)) {
|
} else if (element instanceof Blob) {
|
||||||
buffer = element;
|
part = element;
|
||||||
} else {
|
} else {
|
||||||
buffer = Buffer.from(typeof element === 'string' ? element : String(element));
|
part = new TextEncoder().encode(String(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
size += buffer.length || buffer.size || 0;
|
size += ArrayBuffer.isView(part) ? part.byteLength : part.size;
|
||||||
return buffer;
|
return part;
|
||||||
});
|
});
|
||||||
|
|
||||||
const type = options.type === undefined ? '' : String(options.type).toLowerCase();
|
const type = options.type === undefined ? '' : String(options.type);
|
||||||
|
|
||||||
wm.set(this, {
|
this.#type = /[^\u0020-\u007E]/.test(type) ? '' : type;
|
||||||
type: /[^\u0020-\u007E]/.test(type) ? '' : type,
|
this.#size = size;
|
||||||
size,
|
this.#parts = parts;
|
||||||
parts
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,14 +78,14 @@ class Blob {
|
|||||||
* size of the Blob in bytes.
|
* size of the Blob in bytes.
|
||||||
*/
|
*/
|
||||||
get size() {
|
get size() {
|
||||||
return wm.get(this).size;
|
return this.#size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type property of a Blob object returns the MIME type of the file.
|
* The type property of a Blob object returns the MIME type of the file.
|
||||||
*/
|
*/
|
||||||
get type() {
|
get type() {
|
||||||
return wm.get(this).type;
|
return this.#type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,7 +96,16 @@ class Blob {
|
|||||||
* @return {Promise<string>}
|
* @return {Promise<string>}
|
||||||
*/
|
*/
|
||||||
async text() {
|
async text() {
|
||||||
return Buffer.from(await this.arrayBuffer()).toString();
|
// More optimized than using this.arrayBuffer()
|
||||||
|
// that requires twice as much ram
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let str = '';
|
||||||
|
for await (let part of toIterator(this.#parts, false)) {
|
||||||
|
str += decoder.decode(part, { stream: true });
|
||||||
|
}
|
||||||
|
// Remaining
|
||||||
|
str += decoder.decode();
|
||||||
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,7 +118,7 @@ class Blob {
|
|||||||
async arrayBuffer() {
|
async arrayBuffer() {
|
||||||
const data = new Uint8Array(this.size);
|
const data = new Uint8Array(this.size);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
for await (const chunk of this.stream()) {
|
for await (const chunk of toIterator(this.#parts, false)) {
|
||||||
data.set(chunk, offset);
|
data.set(chunk, offset);
|
||||||
offset += chunk.length;
|
offset += chunk.length;
|
||||||
}
|
}
|
||||||
@@ -113,13 +127,13 @@ class Blob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Blob interface's stream() method is difference from native
|
* The Blob stream() implements partial support of the whatwg stream
|
||||||
* and uses node streams instead of whatwg streams.
|
* by only being async iterable.
|
||||||
*
|
*
|
||||||
* @returns {Readable} Node readable stream
|
* @returns {AsyncGenerator<Uint8Array>}
|
||||||
*/
|
*/
|
||||||
stream() {
|
async * stream() {
|
||||||
return Readable.from(read(wm.get(this).parts));
|
yield * toIterator(this.#parts, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,7 +152,7 @@ class Blob {
|
|||||||
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
|
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
|
||||||
|
|
||||||
const span = Math.max(relativeEnd - relativeStart, 0);
|
const span = Math.max(relativeEnd - relativeStart, 0);
|
||||||
const parts = wm.get(this).parts.values();
|
const parts = this.#parts;
|
||||||
const blobParts = [];
|
const blobParts = [];
|
||||||
let added = 0;
|
let added = 0;
|
||||||
|
|
||||||
@@ -150,10 +164,16 @@ class Blob {
|
|||||||
relativeStart -= size;
|
relativeStart -= size;
|
||||||
relativeEnd -= size;
|
relativeEnd -= size;
|
||||||
} else {
|
} else {
|
||||||
const chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
|
let chunk
|
||||||
|
if (ArrayBuffer.isView(part)) {
|
||||||
|
chunk = part.subarray(relativeStart, Math.min(size, relativeEnd));
|
||||||
|
added += chunk.byteLength
|
||||||
|
} else {
|
||||||
|
chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
|
||||||
|
added += chunk.size
|
||||||
|
}
|
||||||
blobParts.push(chunk);
|
blobParts.push(chunk);
|
||||||
added += size;
|
relativeStart = 0; // All next sequential parts should start at 0
|
||||||
relativeStart = 0; // All next sequental parts should start at 0
|
|
||||||
|
|
||||||
// don't add the overflow to new blobParts
|
// don't add the overflow to new blobParts
|
||||||
if (added >= span) {
|
if (added >= span) {
|
||||||
@@ -162,11 +182,27 @@ class Blob {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = new Blob([], {type});
|
const blob = new Blob([], {type: String(type).toLowerCase()});
|
||||||
Object.assign(wm.get(blob), {size: span, parts: blobParts});
|
blob.#size = span;
|
||||||
|
blob.#parts = blobParts;
|
||||||
|
|
||||||
return blob;
|
return blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return 'Blob';
|
||||||
|
}
|
||||||
|
|
||||||
|
static [Symbol.hasInstance](object) {
|
||||||
|
return (
|
||||||
|
typeof object?.constructor === 'function' &&
|
||||||
|
(
|
||||||
|
typeof object.stream === 'function' ||
|
||||||
|
typeof object.arrayBuffer === 'function'
|
||||||
|
) &&
|
||||||
|
/^(Blob|File)$/.test(object[Symbol.toStringTag])
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperties(Blob.prototype, {
|
Object.defineProperties(Blob.prototype, {
|
||||||
@@ -175,11 +211,4 @@ Object.defineProperties(Blob.prototype, {
|
|||||||
slice: {enumerable: true}
|
slice: {enumerable: true}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(Blob.prototype, Symbol.toStringTag, {
|
export { Blob };
|
||||||
value: 'Blob',
|
|
||||||
writable: false,
|
|
||||||
enumerable: false,
|
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Blob;
|
|
||||||
|
|||||||
+46
-20
@@ -1,43 +1,52 @@
|
|||||||
{
|
{
|
||||||
"name": "fetch-blob",
|
"name": "fetch-blob",
|
||||||
"version": "1.0.7",
|
"version": "3.0.0",
|
||||||
"description": "A Blob implementation in Node.js, originally from node-fetch.",
|
"description": "Blob & File implementation in Node.js, originally from node-fetch.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
|
"from.js",
|
||||||
|
"file.js",
|
||||||
|
"file.d.ts",
|
||||||
"index.js",
|
"index.js",
|
||||||
"index.d.ts"
|
"index.d.ts",
|
||||||
|
"from.d.ts"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "xo",
|
"lint": "xo test.js",
|
||||||
"test": "xo && ava",
|
"test": "npm run lint && ava",
|
||||||
"report": "nyc ava",
|
"report": "c8 --reporter json --reporter text ava",
|
||||||
"coverage": "nyc --reporter json --reporter text ava && codecov -f coverage/coverage-final.json",
|
"coverage": "c8 --reporter json --reporter text ava && codecov -f coverage/coverage-final.json",
|
||||||
"prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js"
|
"prepublishOnly": "tsc --declaration --emitDeclarationOnly --allowJs index.js from.js"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/node-fetch/fetch-blob.git",
|
"repository": "https://github.com/node-fetch/fetch-blob.git",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"blob",
|
"blob",
|
||||||
|
"file",
|
||||||
"node-fetch"
|
"node-fetch"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10.17.0"
|
"node": ">=14.0.0"
|
||||||
},
|
},
|
||||||
"author": "David Frank",
|
"author": "Jimmy Wärting <jimmy@warting.se> (https://jimmy.warting.se)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/node-fetch/fetch-blob/issues"
|
"url": "https://github.com/node-fetch/fetch-blob/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/node-fetch/fetch-blob#readme",
|
"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.1.0",
|
|
||||||
"typescript": "^3.9.5",
|
|
||||||
"xo": "^0.32.0"
|
|
||||||
},
|
|
||||||
"xo": {
|
"xo": {
|
||||||
|
"rules": {
|
||||||
|
"unicorn/prefer-node-protocol": "off",
|
||||||
|
"unicorn/numeric-separators-style": "off",
|
||||||
|
"unicorn/prefer-spread": "off",
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"ignorePackages": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": "test.js",
|
"files": "test.js",
|
||||||
@@ -48,5 +57,22 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"devDependencies": {
|
||||||
|
"ava": "^3.15.0",
|
||||||
|
"c8": "^7.7.2",
|
||||||
|
"codecov": "^3.8.2",
|
||||||
|
"node-fetch": "^3.0.0-beta.9",
|
||||||
|
"typescript": "^4.3.2",
|
||||||
|
"xo": "^0.40.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
const test = require('ava');
|
import fs from 'fs';
|
||||||
const Blob = require('.');
|
import {Readable} from 'stream';
|
||||||
const getStream = require('get-stream');
|
import buffer from 'buffer';
|
||||||
const {Response} = require('node-fetch');
|
import test from 'ava';
|
||||||
|
import {Response} from 'node-fetch';
|
||||||
|
import syncBlob, {blobFromSync, blobFrom, fileFromSync, fileFrom} from './from.js';
|
||||||
|
import File from './file.js';
|
||||||
|
import Blob from './index.js';
|
||||||
|
|
||||||
|
const license = fs.readFileSync('./LICENSE', 'utf-8');
|
||||||
|
|
||||||
test('new Blob()', t => {
|
test('new Blob()', t => {
|
||||||
const blob = new Blob(); // eslint-disable-line no-unused-vars
|
const blob = new Blob(); // eslint-disable-line no-unused-vars
|
||||||
@@ -22,11 +28,12 @@ test('Blob ctor parts', async t => {
|
|||||||
new Uint8Array([101]).buffer,
|
new Uint8Array([101]).buffer,
|
||||||
Buffer.from('f'),
|
Buffer.from('f'),
|
||||||
new Blob(['g']),
|
new Blob(['g']),
|
||||||
{}
|
{},
|
||||||
|
new URLSearchParams('foo')
|
||||||
];
|
];
|
||||||
|
|
||||||
const blob = new Blob(parts);
|
const blob = new Blob(parts);
|
||||||
t.is(await blob.text(), 'abcdefg[object Object]');
|
t.is(await blob.text(), 'abcdefg[object Object]foo=');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Blob size', t => {
|
test('Blob size', t => {
|
||||||
@@ -78,8 +85,10 @@ test('Blob stream()', async t => {
|
|||||||
const data = 'a=1';
|
const data = 'a=1';
|
||||||
const type = 'text/plain';
|
const type = 'text/plain';
|
||||||
const blob = new Blob([data], {type});
|
const blob = new Blob([data], {type});
|
||||||
const result = await getStream(blob.stream());
|
|
||||||
t.is(result, data);
|
for await (const chunk of blob.stream()) {
|
||||||
|
t.is(chunk.join(), [97, 61, 49].join());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Blob toString()', t => {
|
test('Blob toString()', t => {
|
||||||
@@ -113,6 +122,12 @@ test('Blob slice(0, -1)', async t => {
|
|||||||
t.is(await blob.text(), 'abcdefg');
|
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 => {
|
test('throw away unwanted parts', async t => {
|
||||||
const blob = new Blob(['a', 'b', 'c']).slice(1, 2);
|
const blob = new Blob(['a', 'b', 'c']).slice(1, 2);
|
||||||
t.is(await blob.text(), 'b');
|
t.is(await blob.text(), 'b');
|
||||||
@@ -122,7 +137,7 @@ test('Blob works with node-fetch Response.blob()', async t => {
|
|||||||
const data = 'a=1';
|
const data = 'a=1';
|
||||||
const type = 'text/plain';
|
const type = 'text/plain';
|
||||||
const blob = new Blob([data], {type});
|
const blob = new Blob([data], {type});
|
||||||
const response = new Response(blob);
|
const response = new Response(Readable.from(blob.stream()));
|
||||||
const blob2 = await response.blob();
|
const blob2 = await response.blob();
|
||||||
t.is(await blob2.text(), data);
|
t.is(await blob2.text(), data);
|
||||||
});
|
});
|
||||||
@@ -131,7 +146,179 @@ test('Blob works with node-fetch Response.text()', async t => {
|
|||||||
const data = 'a=1';
|
const data = 'a=1';
|
||||||
const type = 'text/plain';
|
const type = 'text/plain';
|
||||||
const blob = new Blob([data], {type});
|
const blob = new Blob([data], {type});
|
||||||
const response = new Response(blob);
|
const response = new Response(Readable.from(blob.stream()));
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
t.is(text, data);
|
t.is(text, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('blob part backed up by filesystem', async t => {
|
||||||
|
const blob = blobFromSync('./LICENSE');
|
||||||
|
t.is(await blob.slice(0, 3).text(), license.slice(0, 3));
|
||||||
|
t.is(await blob.slice(4, 11).text(), license.slice(4, 11));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reading after modified should fail', async t => {
|
||||||
|
const blob = blobFromSync('./LICENSE');
|
||||||
|
await new Promise(resolve => {
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
});
|
||||||
|
const now = new Date();
|
||||||
|
// Change modified time
|
||||||
|
fs.utimesSync('./LICENSE', now, now);
|
||||||
|
const error = await t.throwsAsync(blob.text());
|
||||||
|
t.is(error.constructor.name, 'DOMException');
|
||||||
|
t.is(error instanceof Error, true);
|
||||||
|
t.is(error.name, 'NotReadableError');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reading file after modified should fail', async t => {
|
||||||
|
const file = fileFromSync('./LICENSE');
|
||||||
|
await new Promise(resolve => {
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
});
|
||||||
|
const now = new Date();
|
||||||
|
// Change modified time
|
||||||
|
fs.utimesSync('./LICENSE', now, now);
|
||||||
|
const error = await t.throwsAsync(file.text());
|
||||||
|
t.is(error.constructor.name, 'DOMException');
|
||||||
|
t.is(error instanceof Error, true);
|
||||||
|
t.is(error.name, 'NotReadableError');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reading from the stream created by blobFrom', async t => {
|
||||||
|
const blob = blobFromSync('./LICENSE');
|
||||||
|
const actual = await blob.text();
|
||||||
|
t.is(actual, license);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create a blob from path asynchronous', async t => {
|
||||||
|
const blob = await blobFrom('./LICENSE');
|
||||||
|
const actual = await blob.text();
|
||||||
|
t.is(actual, license);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reading empty blobs', async t => {
|
||||||
|
const blob = blobFromSync('./LICENSE').slice(0, 0);
|
||||||
|
const actual = await blob.text();
|
||||||
|
t.is(actual, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Blob-ish class is an instance of Blob', t => {
|
||||||
|
class File {
|
||||||
|
stream() {}
|
||||||
|
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return 'File';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.true(new File() instanceof Blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Instanceof check returns false for nullish values', t => {
|
||||||
|
t.false(null instanceof Blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @see https://github.com/w3c/FileAPI/issues/43 - important to keep boundary value */
|
||||||
|
test('Dose not lowercase the blob values', t => {
|
||||||
|
const type = 'multipart/form-data; boundary=----WebKitFormBoundaryTKqdrVt01qOBltBd';
|
||||||
|
t.is(new Blob([], {type}).type, type);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parts are immutable', async t => {
|
||||||
|
const buf = new Uint8Array([97]);
|
||||||
|
const blob = new Blob([buf]);
|
||||||
|
buf[0] = 98;
|
||||||
|
t.is(await blob.text(), 'a');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Blobs are immutable', async t => {
|
||||||
|
const buf = new Uint8Array([97]);
|
||||||
|
const blob = new Blob([buf]);
|
||||||
|
const chunk = await blob.stream().next();
|
||||||
|
t.is(chunk.value[0], 97);
|
||||||
|
chunk.value[0] = 98;
|
||||||
|
t.is(await blob.text(), 'a');
|
||||||
|
});
|
||||||
|
|
||||||
|
// This was necessary to avoid large ArrayBuffer clones (slice)
|
||||||
|
test('Large chunks are divided into smaller chunks', async t => {
|
||||||
|
const buf = new Uint8Array(65590);
|
||||||
|
const blob = new Blob([buf]);
|
||||||
|
let i = 0;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
for await (const chunk of blob.stream()) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
t.is(i === 2, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Can use named import - as well as default', async t => {
|
||||||
|
// eslint-disable-next-line node/no-unsupported-features/es-syntax
|
||||||
|
const {Blob, default: def} = await import('./index.js');
|
||||||
|
t.is(Blob, def);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('default from.js exports blobFromSync', t => {
|
||||||
|
t.is(blobFromSync, syncBlob);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (buffer.Blob) {
|
||||||
|
test('Can wrap buffer.Blob to a fetch-blob', async t => {
|
||||||
|
const blob1 = new buffer.Blob(['blob part']);
|
||||||
|
const blob2 = new Blob([blob1]);
|
||||||
|
t.is(await blob2.text(), 'blob part');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('File is a instance of blob', t => {
|
||||||
|
t.true(new File([], '') instanceof Blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fileFrom returns the name', async t => {
|
||||||
|
t.is((await fileFrom('./LICENSE')).name, 'LICENSE');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fileFromSync returns the name', t => {
|
||||||
|
t.is(fileFromSync('./LICENSE').name, 'LICENSE');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fileFromSync(path, type) sets the type', t => {
|
||||||
|
t.is(fileFromSync('./LICENSE', 'text/plain').type, 'text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blobFromSync(path, type) sets the type', t => {
|
||||||
|
t.is(blobFromSync('./LICENSE', 'text/plain').type, 'text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fileFrom(path, type) sets the type', async t => {
|
||||||
|
const file = await fileFrom('./LICENSE', 'text/plain');
|
||||||
|
t.is(file.type, 'text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fileFrom(path, type) read/sets the lastModified ', async t => {
|
||||||
|
const file = await fileFrom('./LICENSE', 'text/plain');
|
||||||
|
// Earlier test updates the last modified date to now
|
||||||
|
t.is(typeof file.lastModified, 'number');
|
||||||
|
// The lastModifiedDate is deprecated and removed from spec
|
||||||
|
t.false('lastModifiedDate' in file);
|
||||||
|
t.is(file.lastModified > Date.now() - 60000, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blobFrom(path, type) sets the type', async t => {
|
||||||
|
const blob = await blobFrom('./LICENSE', 'text/plain');
|
||||||
|
t.is(blob.type, 'text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blobFrom(path) sets empty type', async t => {
|
||||||
|
const blob = await blobFrom('./LICENSE');
|
||||||
|
t.is(blob.type, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('new File() throws with too few args', t => {
|
||||||
|
t.throws(() => new File(), {
|
||||||
|
instanceOf: TypeError,
|
||||||
|
message: 'Failed to construct \'File\': 2 arguments required, but only 0 present.'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user