Skip to content

Commit a85d5fc

Browse files
Copilotstreamich
andcommitted
feat: implement fs.openAsBlob method with comprehensive tests
Co-authored-by: streamich <[email protected]>
1 parent 2f394ec commit a85d5fc

File tree

3 files changed

+127
-2
lines changed

3 files changed

+127
-2
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { of } from '../../../thingies';
2+
import { memfs } from '../../../';
3+
4+
describe('.openAsBlob()', () => {
5+
it('can read a text file as blob', async () => {
6+
const { fs } = memfs({ '/dir/test.txt': 'Hello, World!' });
7+
const blob = await fs.openAsBlob('/dir/test.txt');
8+
expect(blob).toBeInstanceOf(Blob);
9+
expect(blob.size).toBe(13);
10+
expect(blob.type).toBe('');
11+
12+
const text = await blob.text();
13+
expect(text).toBe('Hello, World!');
14+
});
15+
16+
it('can read a binary file as blob', async () => {
17+
const binaryData = Buffer.from([0x89, 0x50, 0x4e, 0x47]); // PNG header
18+
const { fs } = memfs({ '/image.png': binaryData });
19+
const blob = await fs.openAsBlob('/image.png');
20+
expect(blob).toBeInstanceOf(Blob);
21+
expect(blob.size).toBe(4);
22+
23+
const arrayBuffer = await blob.arrayBuffer();
24+
const view = new Uint8Array(arrayBuffer);
25+
expect(Array.from(view)).toEqual([0x89, 0x50, 0x4e, 0x47]);
26+
});
27+
28+
it('can specify a mime type', async () => {
29+
const { fs } = memfs({ '/data.json': '{"key": "value"}' });
30+
const blob = await fs.openAsBlob('/data.json', { type: 'application/json' });
31+
expect(blob).toBeInstanceOf(Blob);
32+
expect(blob.type).toBe('application/json');
33+
34+
const text = await blob.text();
35+
expect(text).toBe('{"key": "value"}');
36+
});
37+
38+
it('handles empty files', async () => {
39+
const { fs } = memfs({ '/empty.txt': '' });
40+
const blob = await fs.openAsBlob('/empty.txt');
41+
expect(blob).toBeInstanceOf(Blob);
42+
expect(blob.size).toBe(0);
43+
44+
const text = await blob.text();
45+
expect(text).toBe('');
46+
});
47+
48+
it('throws if file does not exist', async () => {
49+
const { fs } = memfs({ '/dir/test.txt': 'content' });
50+
const [, err] = await of(fs.openAsBlob('/dir/test-NOT-FOUND.txt'));
51+
expect(err).toBeInstanceOf(Error);
52+
expect((<any>err).code).toBe('ENOENT');
53+
});
54+
55+
it('throws EISDIR if path is a directory', async () => {
56+
const { fs } = memfs({ '/dir/test.txt': 'content' });
57+
const [, err] = await of(fs.openAsBlob('/dir'));
58+
expect(err).toBeInstanceOf(Error);
59+
expect((<any>err).code).toBe('EISDIR');
60+
});
61+
62+
it('works with Buffer paths', async () => {
63+
const { fs } = memfs({ '/test.txt': 'buffer path test' });
64+
const pathBuffer = Buffer.from('/test.txt');
65+
const blob = await fs.openAsBlob(pathBuffer);
66+
expect(blob).toBeInstanceOf(Blob);
67+
68+
const text = await blob.text();
69+
expect(text).toBe('buffer path test');
70+
});
71+
72+
it('works with different path formats', async () => {
73+
const { fs } = memfs({ '/path-test.txt': 'path format test' });
74+
const blob = await fs.openAsBlob('/path-test.txt');
75+
expect(blob).toBeInstanceOf(Blob);
76+
77+
const text = await blob.text();
78+
expect(text).toBe('path format test');
79+
});
80+
81+
it('handles large files', async () => {
82+
const largeContent = 'x'.repeat(10000);
83+
const { fs } = memfs({ '/large.txt': largeContent });
84+
const blob = await fs.openAsBlob('/large.txt');
85+
expect(blob).toBeInstanceOf(Blob);
86+
expect(blob.size).toBe(10000);
87+
88+
const text = await blob.text();
89+
expect(text).toBe(largeContent);
90+
});
91+
92+
it('can read file through symlink', async () => {
93+
const { fs } = memfs({ '/original.txt': 'symlink test' });
94+
fs.symlinkSync('/original.txt', '/link.txt');
95+
96+
const blob = await fs.openAsBlob('/link.txt');
97+
expect(blob).toBeInstanceOf(Blob);
98+
99+
const text = await blob.text();
100+
expect(text).toBe('symlink test');
101+
});
102+
});

src/node/lists/fsCallbackApiList.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
77
'chown',
88
'close',
99
'copyFile',
10+
'cp',
1011
'createReadStream',
1112
'createWriteStream',
1213
'exists',
@@ -24,6 +25,7 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
2425
'mkdir',
2526
'mkdtemp',
2627
'open',
28+
'openAsBlob',
2729
'opendir',
2830
'read',
2931
'readv',
@@ -35,6 +37,7 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
3537
'rm',
3638
'rmdir',
3739
'stat',
40+
'statfs',
3841
'symlink',
3942
'truncate',
4043
'unlink',

src/node/volume.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,8 +1446,28 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
14461446
public statfsSync: FsSynchronousApi['statfsSync'] = notImplemented;
14471447
/** @todo Implement statfs */
14481448
public statfs: FsCallbackApi['statfs'] = notImplemented;
1449-
/** @todo Implement openAsBlob */
1450-
public openAsBlob: FsCallbackApi['openAsBlob'] = notImplemented;
1449+
private readonly _openAsBlob = (filename: string, options: opts.IOpenAsBlobOptions = {}): Blob => {
1450+
const link = this._core.getResolvedLinkOrThrow(filename, 'open');
1451+
const node = link.getNode();
1452+
if (node.isDirectory()) throw createError(ERROR_CODE.EISDIR, 'open', link.getPath());
1453+
1454+
const buffer = node.getBuffer();
1455+
const type = options.type || '';
1456+
1457+
return new Blob([buffer], { type });
1458+
};
1459+
1460+
public openAsBlob = (path: PathLike, options?: opts.IOpenAsBlobOptions): Promise<Blob> => {
1461+
return new Promise((resolve, reject) => {
1462+
try {
1463+
const filename = pathToFilename(path);
1464+
const blob = this._openAsBlob(filename, options);
1465+
resolve(blob);
1466+
} catch (err) {
1467+
reject(err);
1468+
}
1469+
});
1470+
};
14511471

14521472
private readonly _opendir = (filename: string, options: opts.IOpendirOptions): Dir => {
14531473
const link: Link = this._core.getResolvedLinkOrThrow(filename, 'scandir');

0 commit comments

Comments
 (0)