Skip to content

Commit b815b5e

Browse files
Add support for reading from a web stream (#635)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 00e051b commit b815b5e

File tree

11 files changed

+204
-189
lines changed

11 files changed

+204
-189
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ jobs:
1010
fail-fast: false
1111
matrix:
1212
node-version:
13+
- 22
1314
- 20
1415
- 18
1516
steps:

browser.d.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

browser.js

Lines changed: 0 additions & 15 deletions
This file was deleted.

core.d.ts

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1-
import type {Readable as ReadableStream} from 'node:stream';
1+
/**
2+
Typings for primary entry point, Node.js specific typings can be found in index.d.ts
3+
*/
4+
5+
import type {ReadableStream as WebReadableStream} from 'node:stream/web';
26
import type {ITokenizer} from 'strtok3';
37

8+
/**
9+
Either the Node.js ReadableStream or the `lib.dom.d.ts` ReadableStream.
10+
Related issue: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60377
11+
*/
12+
export type AnyWebReadableStream<G> = WebReadableStream<G> | ReadableStream<G>;
13+
414
export type FileExtension =
515
| 'jpg'
616
| 'png'
@@ -318,10 +328,6 @@ export type FileTypeResult = {
318328
readonly mime: MimeType;
319329
};
320330

321-
export type ReadableStreamWithFileType = ReadableStream & {
322-
readonly fileType?: FileTypeResult;
323-
};
324-
325331
/**
326332
Detect the file type of a `Uint8Array`, or `ArrayBuffer`.
327333
@@ -339,10 +345,10 @@ Detect the file type of a Node.js [readable stream](https://nodejs.org/api/strea
339345
340346
The file type is detected by checking the [magic number](https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files) of the buffer.
341347
342-
@param stream - A readable stream representing file data.
348+
@param stream - A Node.js Readable stream or Web API Readable Stream representing file data. The Web Readable stream **must be a byte stream**.
343349
@returns The detected file type, or `undefined` when there is no match.
344350
*/
345-
export function fileTypeFromStream(stream: ReadableStream): Promise<FileTypeResult | undefined>;
351+
export function fileTypeFromStream(stream: AnyWebReadableStream<Uint8Array>): Promise<FileTypeResult | undefined>;
346352

347353
/**
348354
Detect the file type from an [`ITokenizer`](https://github.com/Borewit/strtok3#tokenizer) source.
@@ -391,37 +397,6 @@ export type StreamOptions = {
391397
readonly sampleSize?: number;
392398
};
393399

394-
/**
395-
Returns a `Promise` which resolves to the original readable stream argument, but with an added `fileType` property, which is an object like the one returned from `fileTypeFromFile()`.
396-
397-
This method can be handy to put in between a stream, but it comes with a price.
398-
Internally `stream()` builds up a buffer of `sampleSize` bytes, used as a sample, to determine the file type.
399-
The sample size impacts the file detection resolution.
400-
A smaller sample size will result in lower probability of the best file type detection.
401-
402-
**Note:** This method is only available when using Node.js.
403-
**Note:** Requires Node.js 14 or later.
404-
405-
@param readableStream - A [readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable) containing a file to examine.
406-
@returns A `Promise` which resolves to the original readable stream argument, but with an added `fileType` property, which is an object like the one returned from `fileTypeFromFile()`.
407-
408-
@example
409-
```
410-
import got from 'got';
411-
import {fileTypeStream} from 'file-type';
412-
413-
const url = 'https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg';
414-
415-
const stream1 = got.stream(url);
416-
const stream2 = await fileTypeStream(stream1, {sampleSize: 1024});
417-
418-
if (stream2.fileType?.mime === 'image/jpeg') {
419-
// stream2 can be used to stream the JPEG image (from the very beginning of the stream)
420-
}
421-
```
422-
*/
423-
export function fileTypeStream(readableStream: ReadableStream, options?: StreamOptions): Promise<ReadableStreamWithFileType>;
424-
425400
/**
426401
Detect the file type of a [`Blob`](https://nodejs.org/api/buffer.html#class-blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File).
427402
@@ -508,11 +483,6 @@ export declare class FileTypeParser {
508483
*/
509484
fromBuffer(buffer: Uint8Array | ArrayBuffer): Promise<FileTypeResult | undefined>;
510485

511-
/**
512-
Works the same way as {@link fileTypeFromStream}, additionally taking into account custom detectors (if any were provided to the constructor).
513-
*/
514-
fromStream(stream: ReadableStream): Promise<FileTypeResult | undefined>;
515-
516486
/**
517487
Works the same way as {@link fileTypeFromTokenizer}, additionally taking into account custom detectors (if any were provided to the constructor).
518488
*/
@@ -522,9 +492,4 @@ export declare class FileTypeParser {
522492
Works the same way as {@link fileTypeFromBlob}, additionally taking into account custom detectors (if any were provided to the constructor).
523493
*/
524494
fromBlob(blob: Blob): Promise<FileTypeResult | undefined>;
525-
526-
/**
527-
Works the same way as {@link fileTypeStream}, additionally taking into account custom detectors (if any were provided to the constructor).
528-
*/
529-
toDetectionStream(readableStream: ReadableStream, options?: StreamOptions): Promise<FileTypeResult | undefined>;
530495
}

core.js

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
Primary entry point, Node.js specific entry point is index.js
3+
*/
4+
15
import * as Token from 'token-types';
26
import * as strtok3 from 'strtok3/core';
37
import {includes, indexOf, getUintBE} from 'uint8array-extras';
@@ -8,7 +12,7 @@ import {
812
} from './util.js';
913
import {extensions, mimeTypes} from './supported.js';
1014

11-
const minimumBytes = 4100; // A fair amount of file-types are detectable within this range.
15+
export const reasonableDetectionSizeInBytes = 4100; // A fair amount of file-types are detectable within this range.
1216

1317
export async function fileTypeFromStream(stream) {
1418
return new FileTypeParser().fromStream(stream);
@@ -88,54 +92,18 @@ export class FileTypeParser {
8892
}
8993

9094
async fromBlob(blob) {
91-
const buffer = await blob.arrayBuffer();
92-
return this.fromBuffer(new Uint8Array(buffer));
95+
return this.fromStream(blob.stream());
9396
}
9497

9598
async fromStream(stream) {
96-
const tokenizer = await strtok3.fromStream(stream);
99+
const tokenizer = await strtok3.fromWebStream(stream);
97100
try {
98101
return await this.fromTokenizer(tokenizer);
99102
} finally {
100103
await tokenizer.close();
101104
}
102105
}
103106

104-
async toDetectionStream(readableStream, options = {}) {
105-
const {default: stream} = await import('node:stream');
106-
const {sampleSize = minimumBytes} = options;
107-
108-
return new Promise((resolve, reject) => {
109-
readableStream.on('error', reject);
110-
111-
readableStream.once('readable', () => {
112-
(async () => {
113-
try {
114-
// Set up output stream
115-
const pass = new stream.PassThrough();
116-
const outputStream = stream.pipeline ? stream.pipeline(readableStream, pass, () => {}) : readableStream.pipe(pass);
117-
118-
// Read the input stream and detect the filetype
119-
const chunk = readableStream.read(sampleSize) ?? readableStream.read() ?? new Uint8Array(0);
120-
try {
121-
pass.fileType = await this.fromBuffer(chunk);
122-
} catch (error) {
123-
if (error instanceof strtok3.EndOfStreamError) {
124-
pass.fileType = undefined;
125-
} else {
126-
reject(error);
127-
}
128-
}
129-
130-
resolve(outputStream);
131-
} catch (error) {
132-
reject(error);
133-
}
134-
})();
135-
});
136-
});
137-
}
138-
139107
check(header, options) {
140108
return _check(this.buffer, header, options);
141109
}
@@ -145,7 +113,7 @@ export class FileTypeParser {
145113
}
146114

147115
async parse(tokenizer) {
148-
this.buffer = new Uint8Array(minimumBytes);
116+
this.buffer = new Uint8Array(reasonableDetectionSizeInBytes);
149117

150118
// Keep reading until EOF if the file size is unknown.
151119
if (tokenizer.fileInfo.size === undefined) {
@@ -1690,9 +1658,5 @@ export class FileTypeParser {
16901658
}
16911659
}
16921660

1693-
export async function fileTypeStream(readableStream, options = {}) {
1694-
return new FileTypeParser().toDetectionStream(readableStream, options);
1695-
}
1696-
16971661
export const supportedExtensions = new Set(extensions);
16981662
export const supportedMimeTypes = new Set(mimeTypes);

index.d.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,69 @@
1-
import type {FileTypeResult} from './core.js';
1+
/**
2+
Typings for Node.js specific entry point.
3+
*/
4+
5+
import type {Readable as NodeReadableStream} from 'node:stream';
6+
import type {FileTypeResult, StreamOptions, AnyWebReadableStream} from './core.js';
7+
import {FileTypeParser} from './core.js';
8+
9+
export type ReadableStreamWithFileType = NodeReadableStream & {
10+
readonly fileType?: FileTypeResult;
11+
};
12+
13+
export declare class NodeFileTypeParser extends FileTypeParser {
14+
/**
15+
@param stream - Node.js `stream.Readable` or Web API `ReadableStream`.
16+
*/
17+
fromStream(stream: AnyWebReadableStream<Uint8Array> | NodeReadableStream): Promise<FileTypeResult | undefined>;
18+
19+
/**
20+
Works the same way as {@link fileTypeStream}, additionally taking into account custom detectors (if any were provided to the constructor).
21+
*/
22+
toDetectionStream(readableStream: NodeReadableStream, options?: StreamOptions): Promise<ReadableStreamWithFileType>;
23+
}
224

325
/**
426
Detect the file type of a file path.
527
628
The file type is detected by checking the [magic number](https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files) of the buffer.
729
8-
@param path - The file path to parse.
30+
@param path
931
@returns The detected file type and MIME type or `undefined` when there is no match.
1032
*/
1133
export function fileTypeFromFile(path: string): Promise<FileTypeResult | undefined>;
1234

35+
export function fileTypeFromStream(stream: AnyWebReadableStream<Uint8Array> | NodeReadableStream): Promise<FileTypeResult | undefined>;
36+
37+
/**
38+
Returns a `Promise` which resolves to the original readable stream argument, but with an added `fileType` property, which is an object like the one returned from `fileTypeFromFile()`.
39+
40+
This method can be handy to put in between a stream, but it comes with a price.
41+
Internally `stream()` builds up a buffer of `sampleSize` bytes, used as a sample, to determine the file type.
42+
The sample size impacts the file detection resolution.
43+
A smaller sample size will result in lower probability of the best file type detection.
44+
45+
**Note:** This method is only available when using Node.js.
46+
**Note:** Requires Node.js 14 or later.
47+
48+
@param readableStream - A [readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable) containing a file to examine.
49+
@param options - Maybe used to override the default sample-size.
50+
@returns A `Promise` which resolves to the original readable stream argument, but with an added `fileType` property, which is an object like the one returned from `fileTypeFromFile()`.
51+
52+
@example
53+
```
54+
import got from 'got';
55+
import {fileTypeStream} from 'file-type';
56+
57+
const url = 'https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg';
58+
59+
const stream1 = got.stream(url);
60+
const stream2 = await fileTypeStream(stream1, {sampleSize: 1024});
61+
62+
if (stream2.fileType?.mime === 'image/jpeg') {
63+
// stream2 can be used to stream the JPEG image (from the very beginning of the stream)
64+
}
65+
```
66+
*/
67+
export function fileTypeStream(readableStream: NodeReadableStream, options?: StreamOptions): Promise<ReadableStreamWithFileType>;
68+
1369
export * from './core.js';

index.js

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,56 @@
1+
/**
2+
Node.js specific entry point.
3+
*/
4+
5+
import {ReadableStream as WebReadableStream} from 'node:stream/web';
16
import * as strtok3 from 'strtok3';
2-
import {FileTypeParser} from './core.js';
7+
import {FileTypeParser, reasonableDetectionSizeInBytes} from './core.js';
8+
9+
export class NodeFileTypeParser extends FileTypeParser {
10+
async fromStream(stream) {
11+
const tokenizer = await (stream instanceof WebReadableStream ? strtok3.fromWebStream(stream) : strtok3.fromStream(stream));
12+
try {
13+
return super.fromTokenizer(tokenizer);
14+
} finally {
15+
await tokenizer.close();
16+
}
17+
}
18+
19+
async toDetectionStream(readableStream, options = {}) {
20+
const {default: stream} = await import('node:stream');
21+
const {sampleSize = reasonableDetectionSizeInBytes} = options;
22+
23+
return new Promise((resolve, reject) => {
24+
readableStream.on('error', reject);
25+
26+
readableStream.once('readable', () => {
27+
(async () => {
28+
try {
29+
// Set up output stream
30+
const pass = new stream.PassThrough();
31+
const outputStream = stream.pipeline ? stream.pipeline(readableStream, pass, () => {}) : readableStream.pipe(pass);
32+
33+
// Read the input stream and detect the filetype
34+
const chunk = readableStream.read(sampleSize) ?? readableStream.read() ?? new Uint8Array(0);
35+
try {
36+
pass.fileType = await this.fromBuffer(chunk);
37+
} catch (error) {
38+
if (error instanceof strtok3.EndOfStreamError) {
39+
pass.fileType = undefined;
40+
} else {
41+
reject(error);
42+
}
43+
}
44+
45+
resolve(outputStream);
46+
} catch (error) {
47+
reject(error);
48+
}
49+
})();
50+
});
51+
});
52+
}
53+
}
354

455
export async function fileTypeFromFile(path, fileTypeOptions) {
556
const tokenizer = await strtok3.fromFile(path);
@@ -11,4 +62,12 @@ export async function fileTypeFromFile(path, fileTypeOptions) {
1162
}
1263
}
1364

14-
export * from './core.js';
65+
export async function fileTypeFromStream(stream, fileTypeOptions) {
66+
return (new NodeFileTypeParser(fileTypeOptions)).fromStream(stream);
67+
}
68+
69+
export async function fileTypeStream(readableStream, options = {}) {
70+
return new NodeFileTypeParser().toDetectionStream(readableStream, options);
71+
}
72+
73+
export {fileTypeFromBuffer, fileTypeFromBlob, FileTypeParser, supportedMimeTypes, supportedExtensions} from './core.js';

0 commit comments

Comments
 (0)