Skip to content

Commit 8d14e88

Browse files
authored
Created a File Class (#97)
* Created a File Class * Test the untested parts * ignore any d.ts file
1 parent 8ef89ad commit 8d14e88

File tree

8 files changed

+198
-49
lines changed

8 files changed

+198
-49
lines changed

.gitignore

+1-2
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,4 @@ typings/
6262
# dotenv environment variables file
6363
.env
6464

65-
index.d.ts
66-
from.d.ts
65+
*.d.ts

CHANGELOG.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
Changelog
22
=========
33

4-
## v3.0.0-rc.0
4+
## v3.0.0
55
- Changed WeakMap for private field (require node 12)
66
- Switch to ESM
7-
- blob.stream() return a subset of whatwg stream which is the async iterable
7+
- blob.stream() return a subset of whatwg stream which is the async iterable part
88
(it no longer return a node stream)
99
- Reduced the dependency of Buffer by changing to global TextEncoder/Decoder (require node 11)
1010
- Disabled xo since it could understand private fields (#)
1111
- No longer transform the type to lowercase (https://github.com/w3c/FileAPI/issues/43)
1212
This is more loose than strict, keys should be lowercased, but values should not.
1313
It would require a more proper mime type parser - so we just made it loose.
14-
- index.js can now be imported by browser & deno since it no longer depends on any
15-
core node features (but why would you? other environment can benefit from it)
14+
- index.js and file.js can now be imported by browser & deno since it no longer depends on any
15+
core node features (but why would you?)
16+
- Implemented a File class
1617

1718
## v2.1.2
1819
- Fixed a bug where `start` in BlobDataItem was undefined (#85)

README.md

+22-12
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ npm install fetch-blob
8686
// Ways to import
8787
// (PS it's dependency free ESM package so regular http-import from CDN works too)
8888
import Blob from 'fetch-blob'
89+
import File from 'fetch-blob/file.js'
90+
8991
import {Blob} from 'fetch-blob'
92+
import {File} from 'fetch-blob/file.js'
93+
9094
const {Blob} = await import('fetch-blob')
9195

9296

@@ -105,27 +109,33 @@ globalThis.ReadableStream.from(blob.stream())
105109
```
106110
107111
### Blob part backed up by filesystem
108-
To use, install [domexception](https://github.com/jsdom/domexception).
109112
110-
```sh
111-
npm install fetch-blob domexception
112-
```
113+
`fetch-blob/from.js` comes packed with tools to convert any filepath into either a Blob or a File
114+
It will not read the content into memory. It will only stat the file for last modified date and file size.
113115
114116
```js
115-
// The default export is sync and use fs.stat to retrieve size & last modified
117+
// The default export is sync and use fs.stat to retrieve size & last modified as a blob
116118
import blobFromSync from 'fetch-blob/from.js'
117-
import {Blob, blobFrom, blobFromSync} from 'fetch-blob/from.js'
119+
import {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync} from 'fetch-blob/from.js'
118120

119-
const fsBlob1 = blobFromSync('./2-GiB-file.bin')
120-
const fsBlob2 = await blobFrom('./2-GiB-file.bin')
121+
const fsFile = fileFromSync('./2-GiB-file.bin', 'application/octet-stream')
122+
const fsBlob = await blobFrom('./2-GiB-file.mp4')
121123

122-
// Not a 4 GiB memory snapshot, just holds 3 references
124+
// Not a 4 GiB memory snapshot, just holds references
123125
// points to where data is located on the disk
124-
const blob = new Blob([fsBlob1, fsBlob2, 'memory'])
125-
console.log(blob.size) // 4 GiB
126+
const blob = new Blob([fsFile, fsBlob, 'memory', new Uint8Array(10)])
127+
console.log(blob.size) // ~4 GiB
126128
```
127129
128-
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.
130+
`blobFrom|blobFromSync|fileFrom|fileFromSync(path, [mimetype])`
131+
132+
### Creating Blobs backed up by other async sources
133+
Our Blob & File class are more generic then any other polyfills in the way that it can accept any blob look-a-like item
134+
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
135+
136+
An example of this could be to create a file or blob like item coming from a remote HTTP request. Or from a DataBase
137+
138+
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.
129139
130140
[npm-image]: https://flat.badgen.net/npm/v/fetch-blob
131141
[npm-url]: https://www.npmjs.com/package/fetch-blob

file.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Blob from './index.js';
2+
3+
export default class File extends Blob {
4+
#lastModified = 0;
5+
#name = '';
6+
7+
/**
8+
* @param {*[]} fileBits
9+
* @param {string} fileName
10+
* @param {{lastModified?: number, type?: string}} options
11+
*/ // @ts-ignore
12+
constructor(fileBits, fileName, options = {}) {
13+
if (arguments.length < 2) {
14+
throw new TypeError(`Failed to construct 'File': 2 arguments required, but only ${arguments.length} present.`);
15+
}
16+
super(fileBits, options);
17+
18+
const modified = Number(options.lastModified);
19+
this.#lastModified = Number.isNaN(this.#lastModified) ? modified : Date.now()
20+
this.#name = fileName;
21+
}
22+
23+
get name() {
24+
return this.#name;
25+
}
26+
27+
get lastModified() {
28+
return this.#lastModified;
29+
}
30+
31+
get [Symbol.toStringTag]() {
32+
return "File";
33+
}
34+
}
35+
36+
export { File };

from.js

+36-8
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,54 @@
11
import {statSync, createReadStream} from 'fs';
22
import {stat} from 'fs/promises';
3-
import DOMException from 'domexception';
3+
import {basename} from 'path';
4+
import File from './file.js';
45
import Blob from './index.js';
6+
import {MessageChannel} from 'worker_threads';
7+
8+
const DOMException = globalThis.DOMException || (() => {
9+
const port = new MessageChannel().port1
10+
const ab = new ArrayBuffer(0)
11+
try { port.postMessage(ab, [ab, ab]) }
12+
catch (err) { return err.constructor }
13+
})()
14+
15+
/**
16+
* @param {string} path filepath on the disk
17+
* @param {string} [type] mimetype to use
18+
*/
19+
const blobFromSync = (path, type) => fromBlob(statSync(path), path, type);
20+
21+
/**
22+
* @param {string} path filepath on the disk
23+
* @param {string} [type] mimetype to use
24+
*/
25+
const blobFrom = (path, type) => stat(path).then(stat => fromBlob(stat, path, type));
526

627
/**
728
* @param {string} path filepath on the disk
8-
* @returns {Blob}
29+
* @param {string} [type] mimetype to use
930
*/
10-
const blobFromSync = path => from(statSync(path), path);
31+
const fileFrom = (path, type) => stat(path).then(stat => fromFile(stat, path, type));
1132

1233
/**
1334
* @param {string} path filepath on the disk
14-
* @returns {Promise<Blob>}
35+
* @param {string} [type] mimetype to use
1536
*/
16-
const blobFrom = path => stat(path).then(stat => from(stat, path));
37+
const fileFromSync = (path, type) => fromFile(statSync(path), path, type);
38+
39+
const fromBlob = (stat, path, type = '') => new Blob([new BlobDataItem({
40+
path,
41+
size: stat.size,
42+
lastModified: stat.mtimeMs,
43+
start: 0
44+
})], {type});
1745

18-
const from = (stat, path) => new Blob([new BlobDataItem({
46+
const fromFile = (stat, path, type = '') => new File([new BlobDataItem({
1947
path,
2048
size: stat.size,
2149
lastModified: stat.mtimeMs,
2250
start: 0
23-
})]);
51+
})], basename(path), { type, lastModified: stat.mtimeMs });
2452

2553
/**
2654
* This is a blob backed up by a file on the disk
@@ -72,4 +100,4 @@ class BlobDataItem {
72100
}
73101

74102
export default blobFromSync;
75-
export {Blob, blobFrom, blobFromSync};
103+
export {File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync};

index.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export default class Blob {
173173
added += chunk.size
174174
}
175175
blobParts.push(chunk);
176-
relativeStart = 0; // All next sequental parts should start at 0
176+
relativeStart = 0; // All next sequential parts should start at 0
177177

178178
// don't add the overflow to new blobParts
179179
if (added >= span) {
@@ -195,9 +195,7 @@ export default class Blob {
195195

196196
static [Symbol.hasInstance](object) {
197197
return (
198-
object &&
199-
typeof object === 'object' &&
200-
typeof object.constructor === 'function' &&
198+
typeof object?.constructor === 'function' &&
201199
(
202200
typeof object.stream === 'function' ||
203201
typeof object.arrayBuffer === 'function'

package.json

+24-15
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
{
22
"name": "fetch-blob",
3-
"version": "3.0.0-rc.0",
4-
"description": "A Blob implementation in Node.js, originally from node-fetch.",
3+
"version": "3.0.0",
4+
"description": "Blob & File implementation in Node.js, originally from node-fetch.",
55
"main": "index.js",
66
"type": "module",
77
"files": [
88
"from.js",
9+
"file.js",
10+
"file.d.ts",
911
"index.js",
1012
"index.d.ts",
1113
"from.d.ts"
@@ -20,20 +22,23 @@
2022
"repository": "https://github.com/node-fetch/fetch-blob.git",
2123
"keywords": [
2224
"blob",
25+
"file",
2326
"node-fetch"
2427
],
2528
"engines": {
2629
"node": ">=14.0.0"
2730
},
28-
"author": "David Frank",
31+
"author": "Jimmy Wärting <[email protected]> (https://jimmy.warting.se)",
2932
"license": "MIT",
3033
"bugs": {
3134
"url": "https://github.com/node-fetch/fetch-blob/issues"
3235
},
3336
"homepage": "https://github.com/node-fetch/fetch-blob#readme",
3437
"xo": {
3538
"rules": {
36-
"unicorn/import-index": "off",
39+
"unicorn/prefer-node-protocol": "off",
40+
"unicorn/numeric-separators-style": "off",
41+
"unicorn/prefer-spread": "off",
3742
"import/extensions": [
3843
"error",
3944
"always",
@@ -52,18 +57,22 @@
5257
}
5358
]
5459
},
55-
"peerDependenciesMeta": {
56-
"domexception": {
57-
"optional": true
58-
}
59-
},
6060
"devDependencies": {
6161
"ava": "^3.15.0",
62-
"c8": "^7.7.1",
63-
"codecov": "^3.8.1",
64-
"domexception": "^2.0.1",
62+
"c8": "^7.7.2",
63+
"codecov": "^3.8.2",
6564
"node-fetch": "^3.0.0-beta.9",
66-
"typescript": "^4.2.4",
67-
"xo": "^0.38.2"
68-
}
65+
"typescript": "^4.3.2",
66+
"xo": "^0.40.1"
67+
},
68+
"funding": [
69+
{
70+
"type": "github",
71+
"url": "https://github.com/sponsors/jimmywarting"
72+
},
73+
{
74+
"type": "paypal",
75+
"url": "https://paypal.me/jimmywarting"
76+
}
77+
]
6978
}

test.js

+72-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import fs from 'fs';
2-
import test from 'ava';
3-
import {Response} from 'node-fetch';
42
import {Readable} from 'stream';
53
import buffer from 'buffer';
4+
import test from 'ava';
5+
import {Response} from 'node-fetch';
6+
import syncBlob, {blobFromSync, blobFrom, fileFromSync, fileFrom} from './from.js';
7+
import File from './file.js';
68
import Blob from './index.js';
7-
import syncBlob, {blobFromSync, blobFrom} from './from.js';
89

910
const license = fs.readFileSync('./LICENSE', 'utf-8');
1011

@@ -164,7 +165,22 @@ test('Reading after modified should fail', async t => {
164165
const now = new Date();
165166
// Change modified time
166167
fs.utimesSync('./LICENSE', now, now);
167-
const error = await blob.text().catch(error => error);
168+
const error = await t.throwsAsync(blob.text());
169+
t.is(error.constructor.name, 'DOMException');
170+
t.is(error instanceof Error, true);
171+
t.is(error.name, 'NotReadableError');
172+
});
173+
174+
test('Reading file after modified should fail', async t => {
175+
const file = fileFromSync('./LICENSE');
176+
await new Promise(resolve => {
177+
setTimeout(resolve, 100);
178+
});
179+
const now = new Date();
180+
// Change modified time
181+
fs.utimesSync('./LICENSE', now, now);
182+
const error = await t.throwsAsync(file.text());
183+
t.is(error.constructor.name, 'DOMException');
168184
t.is(error instanceof Error, true);
169185
t.is(error.name, 'NotReadableError');
170186
});
@@ -239,6 +255,7 @@ test('Large chunks are divided into smaller chunks', async t => {
239255
});
240256

241257
test('Can use named import - as well as default', async t => {
258+
// eslint-disable-next-line node/no-unsupported-features/es-syntax
242259
const {Blob, default: def} = await import('./index.js');
243260
t.is(Blob, def);
244261
});
@@ -254,3 +271,54 @@ if (buffer.Blob) {
254271
t.is(await blob2.text(), 'blob part');
255272
});
256273
}
274+
275+
test('File is a instance of blob', t => {
276+
t.true(new File([], '') instanceof Blob);
277+
});
278+
279+
test('fileFrom returns the name', async t => {
280+
t.is((await fileFrom('./LICENSE')).name, 'LICENSE');
281+
});
282+
283+
test('fileFromSync returns the name', t => {
284+
t.is(fileFromSync('./LICENSE').name, 'LICENSE');
285+
});
286+
287+
test('fileFromSync(path, type) sets the type', t => {
288+
t.is(fileFromSync('./LICENSE', 'text/plain').type, 'text/plain');
289+
});
290+
291+
test('blobFromSync(path, type) sets the type', t => {
292+
t.is(blobFromSync('./LICENSE', 'text/plain').type, 'text/plain');
293+
});
294+
295+
test('fileFrom(path, type) sets the type', async t => {
296+
const file = await fileFrom('./LICENSE', 'text/plain');
297+
t.is(file.type, 'text/plain');
298+
});
299+
300+
test('fileFrom(path, type) read/sets the lastModified ', async t => {
301+
const file = await fileFrom('./LICENSE', 'text/plain');
302+
// Earlier test updates the last modified date to now
303+
t.is(typeof file.lastModified, 'number');
304+
// The lastModifiedDate is deprecated and removed from spec
305+
t.false('lastModifiedDate' in file);
306+
t.is(file.lastModified > Date.now() - 60000, true);
307+
});
308+
309+
test('blobFrom(path, type) sets the type', async t => {
310+
const blob = await blobFrom('./LICENSE', 'text/plain');
311+
t.is(blob.type, 'text/plain');
312+
});
313+
314+
test('blobFrom(path) sets empty type', async t => {
315+
const blob = await blobFrom('./LICENSE');
316+
t.is(blob.type, '');
317+
});
318+
319+
test('new File() throws with too few args', t => {
320+
t.throws(() => new File(), {
321+
instanceOf: TypeError,
322+
message: 'Failed to construct \'File\': 2 arguments required, but only 0 present.'
323+
});
324+
});

0 commit comments

Comments
 (0)