Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -6541,6 +6541,20 @@ with the [`using`][] syntax.
The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use.

### `fs.openAsBlobSync(path[, options])`

<!-- YAML
added: REPLACEME
-->

* `path` {string|Buffer|URL}
* `options` {Object}
* `type` {string} An optional mime type for the blob.
* Returns: {Blob}

For detailed information, see the documentation of the Promise-returning
version of this API: [`fs.openAsBlob()`][].

### `fs.opendirSync(path[, options])`

<!-- YAML
Expand Down Expand Up @@ -9450,6 +9464,7 @@ the file contents.
[`fs.mkdir()`]: #fsmkdirpath-options-callback
[`fs.mkdtemp()`]: #fsmkdtempprefix-options-callback
[`fs.open()`]: #fsopenpath-flags-mode-callback
[`fs.openAsBlob()`]: #fsopenasblobpath-options
[`fs.opendir()`]: #fsopendirpath-options-callback
[`fs.opendirSync()`]: #fsopendirsyncpath-options
[`fs.read()`]: #fsreadfd-buffer-offset-length-position-callback
Expand Down
24 changes: 24 additions & 0 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,29 @@ function openAsBlob(path, options = kEmptyObject) {
return PromiseResolve(createBlobFromFilePath(path, { type }));
}

/**
* @param {string | Buffer | URL} path
* @param {{
* type?: string;
* }} [options]
* @returns {Blob}
*/
function openAsBlobSync(path, options = kEmptyObject) {
validateObject(options, 'options');
const type = options.type || '';
validateString(type, 'options.type');
path = getValidatedPath(path);

const h = vfsState.handlers;
if (h !== null) {
const result = h.openAsBlobSync(path, options);
if (result !== undefined) return result;
}

const { createBlobFromFilePath } = require('internal/blob');
return createBlobFromFilePath(path, { type });
}

/**
* Reads file from the specified `fd` (file descriptor).
* @param {number} fd
Expand Down Expand Up @@ -3965,6 +3988,7 @@ module.exports = fs = {
open,
openSync,
openAsBlob,
openAsBlobSync,
readdir,
readdirSync,
read,
Expand Down
6 changes: 3 additions & 3 deletions lib/internal/bootstrap/switches/is_main_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,9 @@ if (isBuildingSnapshot()) {
// loaded on first use instead: the ESM loader (with its translators,
// resolver and their dependencies) by run_main/import(), the public util
// and url modules by whoever requires them, data: URL and TypeScript
// support by the module loaders, internal/blob by fs.openAsBlob(), and the
// DNS helpers by node:dns or an explicit --dns-result-order (see
// pre_execution).
// support by the module loaders, internal/blob by fs.openAsBlob() or
// fs.openAsBlobSync(), and the DNS helpers by node:dns or an explicit
// --dns-result-order (see pre_execution).
require('util');
require('url'); // eslint-disable-line no-restricted-modules
require('internal/modules/esm/loader');
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/vfs/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ function createVfsHandlers() {
}
return undefined;
},
openAsBlobSync: (path, options) =>
vfsRead(path, 'stat', (vfs, n) => vfs.openAsBlob(n, options)),

// ==================== Sync FD-based ops ====================

Expand Down
27 changes: 26 additions & 1 deletion test/fixtures/permission/fs-read.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,31 @@ const regularFile = __filename;
}));
}

// fs.openAsBlobSync
{
assert.throws(() => {
fs.openAsBlobSync(blockedFile);
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: path.toNamespacedPath(blockedFile),
}));
assert.throws(() => {
fs.openAsBlobSync(bufferBlockedFile);
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: path.toNamespacedPath(blockedFile),
}));
assert.throws(() => {
fs.openAsBlobSync(blockedFileURL);
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: path.toNamespacedPath(blockedFile),
}));
}

// fs.exists
{
// It will return false (without performing IO) when permissions is not met
Expand Down Expand Up @@ -710,4 +735,4 @@ const regularFile = __filename;
fs.realpath.native(regularFile, (err) => {
assert.ifError(err);
});
}
}
58 changes: 58 additions & 0 deletions test/parallel/test-fs-openAsBlobSync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const { Blob, Buffer } = require('buffer');
const fs = require('fs');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const filename = 'open-as-blob-sync.txt';
const testfile = tmpdir.resolve(filename);
const mutationFile = tmpdir.resolve('open-as-blob-sync-mutation.txt');
const missing = tmpdir.resolve('does-not-exist.txt');
const data = 'hello openAsBlobSync';

fs.writeFileSync(testfile, data);
fs.writeFileSync(mutationFile, data);

assert.throws(() => fs.openAsBlobSync(1), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => fs.openAsBlobSync(testfile, null), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => fs.openAsBlobSync(testfile, { type: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => fs.openAsBlobSync(missing), {
code: 'ENOENT',
syscall: 'stat',
path: missing,
});

(async () => {
for (const path of [
testfile,
Buffer.from(testfile),
tmpdir.fileURL(filename),
]) {
const blob = fs.openAsBlobSync(path, { type: 'text/plain' });

assert.ok(blob instanceof Blob);
assert.strictEqual(blob.size, Buffer.byteLength(data));
assert.strictEqual(blob.type, 'text/plain');
assert.strictEqual(await blob.text(), data);
}

const promise = fs.openAsBlob(testfile);
assert.ok(promise instanceof Promise);
await promise;

const blob = fs.openAsBlobSync(mutationFile);
fs.writeFileSync(mutationFile, `${data}!`);
await assert.rejects(blob.text(), {
name: 'NotReadableError',
});
})().then(common.mustCall());
2 changes: 1 addition & 1 deletion test/parallel/test-permission-fs-supported.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const supportedApis = [
...syncAndAsyncAPI('mkdir'),
...syncAndAsyncAPI('mkdtemp'),
...syncAndAsyncAPI('open'),
'openAsBlob',
...syncAndAsyncAPI('openAsBlob'),
...syncAndAsyncAPI('mkdtemp'),
'mkdtempDisposableSync',
...syncAndAsyncAPI('readdir'),
Expand Down
42 changes: 36 additions & 6 deletions test/parallel/test-vfs-fs-openAsBlob.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,55 @@
// Flags: --experimental-vfs
'use strict';

// fs.openAsBlob dispatches to VFS and returns a Blob over the virtual file.
// fs.openAsBlob and fs.openAsBlobSync dispatch to VFS and return Blobs over the virtual file.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');
const vfs = require('node:vfs');

const mountPoint = path.resolve('/tmp/vfs-openAsBlob-' + process.pid);
tmpdir.refresh();

const mountPoint = tmpdir.resolve('vfs-openAsBlob');
const hostOnlyPath = path.join(mountPoint, 'host-only.txt');
const nulName = 'nul\0file.txt';
const nulPath = path.join(mountPoint, 'src', nulName);
fs.mkdirSync(mountPoint);
fs.writeFileSync(hostOnlyPath, 'host content');

const myVfs = vfs.create();
myVfs.mkdirSync('/src', { recursive: true });
myVfs.writeFileSync('/src/hello.txt', 'hello world');
myVfs.writeFileSync(`/src/${nulName}`, 'nul content');
myVfs.mount(mountPoint);

fs.openAsBlob(path.join(mountPoint, 'src/hello.txt'))
.then(async (blob) => {
const filePath = path.join(mountPoint, 'src/hello.txt');

(async () => {
try {
const syncBlob = fs.openAsBlobSync(filePath, { type: 'text/plain' });
assert.ok(syncBlob instanceof Blob);
assert.strictEqual(syncBlob.size, 11);
assert.strictEqual(syncBlob.type, 'text/plain');
assert.strictEqual(await syncBlob.text(), 'hello world');
assert.throws(() => fs.openAsBlobSync(hostOnlyPath), {
code: 'ENOENT',
syscall: 'stat',
path: hostOnlyPath,
});
for (const input of [nulPath, Buffer.from(nulPath)]) {
assert.throws(() => fs.openAsBlobSync(input), {
code: 'ERR_INVALID_ARG_VALUE',
});
}

const blob = await fs.openAsBlob(filePath);
assert.ok(blob instanceof Blob);
assert.strictEqual(blob.size, 11);
assert.strictEqual(await blob.text(), 'hello world');
} finally {
myVfs.unmount();
})
.then(common.mustCall());
}
})().then(common.mustCall());
Loading