@socketsupply/socket
Version:
A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.
1,627 lines (1,370 loc) • 41.7 kB
JavaScript
/**
* @module fs
*
* This module enables interacting with the file system in a way modeled on
* standard POSIX functions.
*
* The Application Sandbox restricts access to the file system.
*
* iOS Application Sandboxing has a set of rules that limits access to the file
* system. Apps can only access files in their own sandboxed home directory.
*
* | Directory | Description |
* | --- | --- |
* | `Documents` | The app’s sandboxed documents directory. The contents of this directory are backed up by iTunes and may be set as accessible to the user via iTunes when `UIFileSharingEnabled` is set to `true` in the application's `info.plist`. |
* | `Library` | The app’s sandboxed library directory. The contents of this directory are synchronized via iTunes (except the `Library/Caches` subdirectory, see below), but never exposed to the user. |
* | `Library/Caches` | The app’s sandboxed caches directory. The contents of this directory are not synchronized via iTunes and may be deleted by the system at any time. It's a good place to store data which provides a good offline-first experience for the user. |
* | `Library/Preferences` | The app’s sandboxed preferences directory. The contents of this directory are synchronized via iTunes. Its purpose is to be used by the Settings app. Avoid creating your own files in this directory. |
* | `tmp` | The app’s sandboxed temporary directory. The contents of this directory are not synchronized via iTunes and may be deleted by the system at any time. Although, it's recommended that you delete data that is not necessary anymore manually to minimize the space your app takes up on the file system. Use this directory to store data that is only useful during the app runtime. |
*
* Example usage:
* ```js
* import * as fs from 'socket:fs';
* ```
*/
import { isBufferLike, isFunction, noop } from '../util.js'
import { rand64 } from '../crypto.js'
import { Buffer } from '../buffer.js'
import ipc from '../ipc.js'
import gc from '../gc.js'
import { Dir, Dirent, sortDirectoryEntries } from './dir.js'
import { DirectoryHandle, FileHandle } from './handle.js'
import { ReadStream, WriteStream } from './stream.js'
import { normalizeFlags } from './flags.js'
import * as constants from './constants.js'
import * as promises from './promises.js'
import { Watcher } from './watcher.js'
import { Stats } from './stats.js'
import bookmarks from './bookmarks.js'
import fds from './fds.js'
import * as exports from './index.js'
const kFileDescriptor = Symbol.for('socket.runtune.fs.web.FileDescriptor')
/**
* @typedef {import('../buffer.js').Buffer} Buffer
* @typedef {Uint8Array|Int8Array} TypedArray
* @ignore
*/
function defaultCallback (err) {
if (err) throw err
}
function normalizePath (path) {
if (path instanceof URL) {
if (path.origin === globalThis.location.origin) {
return normalizePath(path.href)
}
return null
}
if (URL.canParse(path)) {
const url = new URL(path)
if (url.origin === globalThis.location.origin) {
path = `./${url.pathname.slice(1)}`
}
}
return path
}
async function visit (path, options = null, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
path = normalizePath(path)
const { flags, flag, mode } = options || {}
let handle = null
try {
handle = await FileHandle.open(path, flags || flag, mode, options)
} catch (err) {
return callback(err)
}
if (handle) {
await callback(null, handle)
try {
await handle.close(options)
} catch (err) {
console.warn(err.message || err)
}
}
}
/**
* Asynchronously check access a file for a given mode calling `callback`
* upon success or error.
* @see {@link https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback}
* @param {string | Buffer | URL} path
* @param {string?|function(Error?)?} [mode = F_OK(0)]
* @param {function(Error?)?} [callback]
*/
export function access (path, mode, callback) {
if (typeof mode === 'function') {
callback = mode
mode = FileHandle.DEFAULT_ACCESS_MODE
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
if (
path instanceof globalThis.FileSystemFileHandle ||
path instanceof globalThis.FileSystemDirectoryHandle
) {
return queueMicrotask(() => callback(null, mode))
}
path = normalizePath(path)
FileHandle
.access(path, mode)
.then((mode) => callback(null, mode))
.catch((err) => callback(err))
}
/**
* Synchronously check access a file for a given mode calling `callback`
* upon success or error.
* @see {@link https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback}
* @param {string | Buffer | URL} path
* @param {string?} [mode = F_OK(0)]
*/
export function accessSync (path, mode = constants.F_OK) {
path = normalizePath(path)
const result = ipc.sendSync('fs.access', { path, mode })
if (result.err) {
throw result.err
}
// F_OK means access in any way
return mode === constants.F_OK ? true : (result.data?.mode && mode) > 0
}
/**
* Checks if a path exists
* @param {string | Buffer | URL} path
* @param {function(Boolean)?} [callback]
*/
export function exists (path, callback) {
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
path = normalizePath(path)
access(path, (err) => {
// eslint-disable-next-line
callback(err !== null)
})
}
/**
* Checks if a path exists
* @param {string | Buffer | URL} path
* @param {function(Boolean)?} [callback]
*/
export function existsSync (path) {
path = normalizePath(path)
try {
accessSync(path)
return true
} catch {
return false
}
}
/**
* Asynchronously changes the permissions of a file.
* No arguments other than a possible exception are given to the completion callback
*
* @see {@link https://nodejs.org/api/fs.html#fschmodpath-mode-callback}
*
* @param {string | Buffer | URL} path
* @param {number} mode
* @param {function(Error?)} callback
*/
export function chmod (path, mode, callback) {
if (typeof mode !== 'number') {
throw new TypeError(`The argument 'mode' must be a 32-bit unsigned integer or an octal string. Received ${mode}`)
}
if (mode < 0 || !Number.isInteger(mode)) {
throw new RangeError(`The value of "mode" is out of range. It must be an integer. Received ${mode}`)
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
if (
path instanceof globalThis.FileSystemFileHandle ||
path instanceof globalThis.FileSystemDirectoryHandle
) {
return new TypeError(
'FileSystemHandle is not writable'
)
}
path = normalizePath(path)
ipc.request('fs.chmod', { mode, path }).then((result) => {
result?.err ? callback(result.err) : callback(null)
})
}
/**
* Synchronously changes the permissions of a file.
*
* @see {@link https://nodejs.org/api/fs.html#fschmodpath-mode-callback}
* @param {string | Buffer | URL} path
* @param {number} mode
*/
export function chmodSync (path, mode) {
if (typeof mode !== 'number') {
throw new TypeError(`The argument 'mode' must be a 32-bit unsigned integer or an octal string. Received ${mode}`)
}
if (mode < 0 || !Number.isInteger(mode)) {
throw new RangeError(`The value of "mode" is out of range. It must be an integer. Received ${mode}`)
}
path = normalizePath(path)
const result = ipc.sendSync('fs.chmod', { mode, path })
if (result.err) {
throw result.err
}
}
/**
* Changes ownership of file or directory at `path` with `uid` and `gid`.
* @param {string} path
* @param {number} uid
* @param {number} gid
* @param {function} callback
*/
export function chown (path, uid, gid, callback) {
path = normalizePath(path)
if (typeof path !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
if (!Number.isInteger(uid)) {
throw new TypeError('The argument \'uid\' must be an integer')
}
if (!Number.isInteger(gid)) {
throw new TypeError('The argument \'gid\' must be an integer')
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
if (
path instanceof globalThis.FileSystemFileHandle ||
path instanceof globalThis.FileSystemDirectoryHandle
) {
return new TypeError(
'FileSystemHandle is not writable'
)
}
ipc.request('fs.chown', { path, uid, gid }).then((result) => {
result?.err ? callback(result.err) : callback(null)
}).catch(callback)
}
/**
* Changes ownership of file or directory at `path` with `uid` and `gid`.
* @param {string} path
* @param {number} uid
* @param {number} gid
*/
export function chownSync (path, uid, gid) {
path = normalizePath(path)
if (typeof path !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
if (!Number.isInteger(uid)) {
throw new TypeError('The argument \'uid\' must be an integer')
}
if (!Number.isInteger(gid)) {
throw new TypeError('The argument \'gid\' must be an integer')
}
const result = ipc.sendSync('fs.chown', { path, uid, gid })
if (result.err) {
throw result.err
}
}
/**
* Asynchronously close a file descriptor calling `callback` upon success or error.
* @see {@link https://nodejs.org/api/fs.html#fsclosefd-callback}
* @param {number} fd
* @param {function(Error?)?} [callback]
*/
export function close (fd, callback) {
if (typeof callback !== 'function') {
callback = defaultCallback
}
try {
FileHandle
.from(fd)
.close()
.then(() => callback(null))
.catch((err) => callback(err))
} catch (err) {
callback(err)
}
}
/**
* Synchronously close a file descriptor.
* @param {number} fd - fd
*/
export function closeSync (fd) {
const id = fds.get(fd) || fd
const result = ipc.sendSync('fs.close', { id })
if (result.err) {
throw result.err
}
fds.release(id)
}
/**
* Asynchronously copies `src` to `dest` calling `callback` upon success or error.
* @param {string} src - The source file path.
* @param {string} dest - The destination file path.
* @param {number} flags - Modifiers for copy operation.
* @param {function(Error=)=} [callback] - The function to call after completion.
* @see {@link https://nodejs.org/api/fs.html#fscopyfilesrc-dest-mode-callback}
*/
export function copyFile (src, dest, flags = 0, callback) {
src = normalizePath(src)
dest = normalizePath(dest)
if (typeof src !== 'string') {
throw new TypeError('The argument \'src\' must be a string')
}
if (typeof dest !== 'string') {
throw new TypeError('The argument \'dest\' must be a string')
}
if (flags && !Number.isInteger(flags)) {
throw new TypeError('The argument \'flags\' must be an integer')
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
if (src instanceof globalThis.FileSystemFileHandle) {
if (src.getFile()[kFileDescriptor]) {
src = src.getFile()[kFileDescriptor].path
} else {
src.getFile().arrayBuffer.then((arrayBuffer) => {
writeFile(dest, arrayBuffer, { flags }, callback)
})
return
}
}
ipc.request('fs.copyFile', { src, dest, flags }).then((result) => {
result?.err ? callback(result.err) : callback(null)
}).catch(callback)
}
/**
* Synchronously copies `src` to `dest` calling `callback` upon success or error.
* @param {string} src - The source file path.
* @param {string} dest - The destination file path.
* @param {number} flags - Modifiers for copy operation.
* @see {@link https://nodejs.org/api/fs.html#fscopyfilesrc-dest-mode-callback}
*/
export function copyFileSync (src, dest, flags = 0) {
src = normalizePath(src)
dest = normalizePath(dest)
if (typeof src !== 'string') {
throw new TypeError('The argument \'src\' must be a string')
}
if (typeof dest !== 'string') {
throw new TypeError('The argument \'dest\' must be a string')
}
if (!Number.isInteger(flags)) {
throw new TypeError('The argument \'flags\' must be an integer')
}
const result = ipc.sendSync('fs.copyFile', { src, dest, flags })
if (result.err) {
throw result.err
}
}
/**
* @see {@link https://nodejs.org/api/fs.html#fscreatewritestreampath-options}
* @param {string | Buffer | URL} path
* @param {object?} [options]
* @returns {ReadStream}
*/
export function createReadStream (path, options) {
if (path?.fd) {
options = path
path = options?.path || null
}
path = normalizePath(path)
let handle = null
const stream = new ReadStream({
autoClose: typeof options?.fd !== 'number',
...options
})
if (options?.fd) {
handle = FileHandle.from(options.fd)
} else {
// @ts-ignore
handle = new FileHandle({ flags: 'r', path, ...options })
// @ts-ignore
handle.open(options).catch((err) => stream.emit('error', err))
}
stream.once('end', async () => {
if (options?.autoClose !== false) {
try {
await handle.close(options)
} catch (err) {
// @ts-ignore
stream.emit('error', err)
}
}
})
stream.setHandle(handle)
return stream
}
/**
* @see {@link https://nodejs.org/api/fs.html#fscreatewritestreampath-options}
* @param {string | Buffer | URL} path
* @param {object?} [options]
* @returns {WriteStream}
*/
export function createWriteStream (path, options) {
if (path?.fd) {
options = path
path = options?.path || null
}
if (path instanceof globalThis.FileSystemHandle) {
return new TypeError(
'FileSystemHandle is not writable'
)
}
path = normalizePath(path)
let handle = null
const stream = new WriteStream({
autoClose: typeof options?.fd !== 'number',
...options
})
if (typeof options?.fd === 'number') {
handle = FileHandle.from(options.fd)
} else {
handle = new FileHandle({ flags: 'w', path, ...options })
// @ts-ignore
handle.open(options).catch((err) => stream.emit('error', err))
}
stream.once('finish', async () => {
if (options?.autoClose !== false) {
try {
await handle.close(options)
} catch (err) {
// @ts-ignore
stream.emit('error', err)
}
}
})
stream.setHandle(handle)
return stream
}
/**
* Invokes the callback with the <fs.Stats> for the file descriptor. See
* the POSIX fstat(2) documentation for more detail.
*
* @see {@link https://nodejs.org/api/fs.html#fsfstatfd-options-callback}
*
* @param {number} fd - A file descriptor.
* @param {object?|function?} [options] - An options object.
* @param {function?} callback - The function to call after completion.
*/
export function fstat (fd, options, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
try {
FileHandle
.from(fd)
.stat(options)
.then(() => callback(null))
.catch((err) => callback(err))
} catch (err) {
callback(err)
}
}
/**
* Request that all data for the open file descriptor is flushed
* to the storage device.
* @param {number} fd - A file descriptor.
* @param {function} callback - The function to call after completion.
*/
export function fsync (fd, callback) {
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
try {
FileHandle
.from(fd)
.sync()
.then(() => callback(null))
.catch((err) => callback(err))
} catch (err) {
callback(err)
}
}
/**
* Truncates the file up to `offset` bytes.
* @param {number} fd - A file descriptor.
* @param {number=|function} [offset = 0]
* @param {function?} callback - The function to call after completion.
*/
export function ftruncate (fd, offset, callback) {
if (typeof offset === 'function') {
callback = offset
offset = {}
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
try {
FileHandle
.from(fd)
.truncate(offset)
.then(() => callback(null))
.catch((err) => callback(err))
} catch (err) {
callback(err)
}
}
/**
* Chages ownership of link at `path` with `uid` and `gid.
* @param {string} path
* @param {number} uid
* @param {number} gid
* @param {function} callback
*/
export function lchown (path, uid, gid, callback) {
if (
path instanceof globalThis.FileSystemFileHandle ||
path instanceof globalThis.FileSystemDirectoryHandle
) {
return new TypeError(
'FileSystemHandle is not writable'
)
}
path = normalizePath(path)
if (typeof path !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
if (!Number.isInteger(uid)) {
throw new TypeError('The argument \'uid\' must be an integer')
}
if (!Number.isInteger(gid)) {
throw new TypeError('The argument \'gid\' must be an integer')
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
ipc.request('fs.lchown', { path, uid, gid }).then((result) => {
result?.err ? callback(result.err) : callback(null)
}).catch(callback)
}
/**
* Creates a link to `dest` from `src`.
* @param {string} src
* @param {string} dest
* @param {function}
*/
export function link (src, dest, callback) {
src = normalizePath(src)
dest = normalizePath(dest)
if (typeof src !== 'string') {
throw new TypeError('The argument \'src\' must be a string')
}
if (typeof dest !== 'string') {
throw new TypeError('The argument \'dest\' must be a string')
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
ipc.request('fs.link', { src, dest }).then((result) => {
result?.err ? callback(result.err) : callback(null)
}).catch(callback)
}
/**
* @ignore
*/
export function mkdir (path, options, callback) {
path = normalizePath(path)
if (typeof options === 'function') {
callback = options
options = null
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
const mode = options.mode || 0o777
const recursive = Boolean(options.recursive) // default to false
if (typeof mode !== 'number') {
throw new TypeError('mode must be a number.')
}
if (mode < 0 || !Number.isInteger(mode)) {
throw new RangeError('mode must be a positive finite number.')
}
ipc
.request('fs.mkdir', { mode, path, recursive })
.then(result => result?.err ? callback(result.err) : callback(null))
.catch(err => callback(err))
}
/**
* @ignore
* @param {string|URL} path
* @param {object=} [options]
*/
export function mkdirSync (path, options = null) {
path = normalizePath(path)
const mode = options?.mode || 0o777
const recursive = Boolean(options?.recursive) // default to false
if (typeof mode !== 'number') {
throw new TypeError('mode must be a number.')
}
if (mode < 0 || !Number.isInteger(mode)) {
throw new RangeError('mode must be a positive finite number.')
}
const result = ipc.sendSync('fs.mkdir', { mode, path, recursive })
if (result.err) {
throw result.err
}
}
/**
* Asynchronously open a file calling `callback` upon success or error.
* @see {@link https://nodejs.org/api/fs.html#fsopenpath-flags-mode-callback}
* @param {string | Buffer | URL} path
* @param {string?} [flags = 'r']
* @param {string?} [mode = 0o666]
* @param {object?|function?} [options]
* @param {function(Error?, number?)?} [callback]
*/
export function open (path, flags = 'r', mode = 0o666, options = null, callback) {
if (typeof flags === 'object') {
callback = mode
options = flags
flags = FileHandle.DEFAULT_OPEN_FLAGS
mode = FileHandle.DEFAULT_OPEN_MODE
}
if (typeof mode === 'object') {
callback = options
options = mode
flags = FileHandle.DEFAULT_OPEN_FLAGS
mode = FileHandle.DEFAULT_OPEN_MODE
}
if (typeof options === 'function') {
callback = options
options = {}
}
if (typeof flags === 'function') {
callback = flags
flags = FileHandle.DEFAULT_OPEN_FLAGS
mode = FileHandle.DEFAULT_OPEN_MODE
}
if (typeof mode === 'function') {
callback = mode
mode = FileHandle.DEFAULT_OPEN_MODE
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
path = normalizePath(path)
FileHandle
.open(path, flags, mode, options)
.then((handle) => {
gc.unref(handle)
callback(null, handle.fd)
})
.catch((err) => callback(err))
}
/**
* Synchronously open a file.
* @param {string | Buffer | URL} path
* @param {string?} [flags = 'r']
* @param {string?} [mode = 0o666]
* @param {object?|function?} [options]
*/
export function openSync (path, flags = 'r', mode = 0o666, options = null) {
if (typeof flags === 'object') {
options = flags
flags = FileHandle.DEFAULT_OPEN_FLAGS
mode = FileHandle.DEFAULT_OPEN_MODE
}
if (typeof mode === 'object') {
options = mode
flags = FileHandle.DEFAULT_OPEN_FLAGS
mode = FileHandle.DEFAULT_OPEN_MODE
}
path = normalizePath(path)
const id = String(options?.id || rand64())
const result = ipc.sendSync('fs.open', {
id,
mode,
path,
flags
}, {
...options
})
if (result.err) {
throw result.err
}
if (result.data?.fd) {
fds.set(id, result.data.fd, 'file')
} else {
fds.set(id, id, 'file')
}
return result.data?.fd || id
}
/**
* Asynchronously open a directory calling `callback` upon success or error.
* @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback}
* @param {string | Buffer | URL} path
* @param {object?|function(Error?, Dir?)} [options]
* @param {string?} [options.encoding = 'utf8']
* @param {boolean?} [options.withFileTypes = false]
* @param {function(Error?, Dir?)?} callback
*/
export function opendir (path, options = {}, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
path = normalizePath(path)
DirectoryHandle
.open(path, options)
.then((handle) => callback(null, new Dir(handle, options)))
.catch((err) => callback(err))
}
/**
* Synchronously open a directory.
* @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback}
* @param {string | Buffer | URL} path
* @param {object?|function(Error?, Dir?)} [options]
* @param {string?} [options.encoding = 'utf8']
* @param {boolean?} [options.withFileTypes = false]
* @return {Dir}
*/
export function opendirSync (path, options = {}) {
path = normalizePath(path)
// @ts-ignore
const id = String(options?.id || rand64())
const result = ipc.sendSync('fs.opendir', { id, path }, options)
if (result.err) {
throw result.err
}
fds.set(id, id, 'directory')
// @ts-ignore
const handle = new DirectoryHandle({ id, path })
return new Dir(handle, options)
}
/**
* Asynchronously read from an open file descriptor.
* @see {@link https://nodejs.org/api/fs.html#fsreadfd-buffer-offset-length-position-callback}
* @param {number} fd
* @param {object | Buffer | TypedArray} buffer - The buffer that the data will be written to.
* @param {number} offset - The position in buffer to write the data to.
* @param {number} length - The number of bytes to read.
* @param {number | BigInt | null} position - Specifies where to begin reading from in the file. If position is null or -1 , data will be read from the current file position, and the file position will be updated. If position is an integer, the file position will be unchanged.
* @param {function(Error?, number?, Buffer?)} callback
*/
export function read (fd, buffer, offset, length, position, options, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
if (typeof buffer === 'object' && !isBufferLike(buffer)) {
options = buffer
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
try {
FileHandle
.from(fd)
.read({ ...options, buffer, offset, length, position })
.then(({ bytesRead, buffer }) => callback(null, bytesRead, buffer))
.catch((err) => callback(err))
} catch (err) {
callback(err)
}
}
/**
* Asynchronously write to an open file descriptor.
* @see {@link https://nodejs.org/api/fs.html#fswritefd-buffer-offset-length-position-callback}
* @param {number} fd
* @param {object | Buffer | TypedArray} buffer - The buffer that the data will be written to.
* @param {number} offset - The position in buffer to write the data to.
* @param {number} length - The number of bytes to read.
* @param {number | BigInt | null} position - Specifies where to begin reading from in the file. If position is null or -1 , data will be read from the current file position, and the file position will be updated. If position is an integer, the file position will be unchanged.
* @param {function(Error?, number?, Buffer?)} callback
*/
export function write (fd, buffer, offset, length, position, options, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
if (typeof buffer === 'object' && !isBufferLike(buffer)) {
options = buffer
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
try {
FileHandle
.from(fd)
.write({ ...options, buffer, offset, length, position })
.then(({ bytesWritten, buffer }) => callback(null, bytesWritten, buffer))
.catch((err) => callback(err))
} catch (err) {
callback(err)
}
}
/**
* Asynchronously read all entries in a directory.
* @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback}
* @param {string | Buffer | URL } path
* @param {object?|function(Error?, object[])} [options]
* @param {string?} [options.encoding ? 'utf8']
* @param {boolean?} [options.withFileTypes ? false]
* @param {function(Error?, object[])} callback
*/
export function readdir (path, options = {}, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
if (!options || typeof options !== 'object') {
options = {}
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
path = normalizePath(path)
options = {
entries: DirectoryHandle.MAX_ENTRIES,
withFileTypes: false,
...options
}
DirectoryHandle
.open(path, options)
.then(async (handle) => {
const entries = []
const dir = new Dir(handle, options)
try {
for await (const entry of dir.entries(options)) {
entries.push(entry)
}
} catch (err) {
return callback(err)
} finally {
if (!dir.closing && !dir.closed) {
dir.close().catch(noop)
}
}
callback(null, entries.sort(sortDirectoryEntries))
})
.catch((err) => callback(err))
}
/**
* Synchronously read all entries in a directory.
* @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback}
* @param {string | Buffer | URL } path
* @param {object?|function(Error?, object[])} [options]
* @param {string?} [options.encoding ? 'utf8']
* @param {boolean?} [options.withFileTypes ? false]
*/
export function readdirSync (path, options = {}) {
options = {
entries: DirectoryHandle.MAX_ENTRIES,
withFileTypes: false,
...options
}
const dir = opendirSync(path, options)
const entries = []
do {
const entry = dir.readSync(options)
if (!entry || entry.length === 0) {
break
}
entries.push(...[].concat(entry))
} while (true)
dir.closeSync()
return entries
}
/**
* @param {string | Buffer | URL | number } path
* @param {object?|function(Error?, Buffer?)} [options]
* @param {string?} [options.encoding ? 'utf8']
* @param {string?} [options.flag ? 'r']
* @param {AbortSignal?} [options.signal]
* @param {function(Error?, Buffer?)} callback
*/
export function readFile (path, options = {}, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
if (typeof options === 'string') {
options = { encoding: options }
}
path = normalizePath(path)
options = { flags: 'r', ...options }
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
visit(path, options, async (err, handle) => {
let buffer = null
if (err) {
callback(err)
return
}
try {
buffer = await handle.readFile(options)
} catch (err) {
callback(err)
return
}
callback(null, buffer)
})
}
/**
* @param {string|Buffer|URL|number} path
* @param {{ encoding?: string = 'utf8', flags?: string = 'r'}} [options]
* @param {object?|function(Error?, Buffer?)} [options]
* @param {AbortSignal?} [options.signal]
*/
export function readFileSync (path, options = null) {
if (typeof options === 'string') {
options = { encoding: options }
}
path = normalizePath(path)
options = {
flags: 'r',
encoding: options?.encoding,
...options
}
let result = null
const stats = statSync(path)
const flags = normalizeFlags(options.flags)
const mode = FileHandle.DEFAULT_OPEN_MODE
// @ts-ignore
const id = String(options?.id || rand64())
result = ipc.sendSync('fs.open', {
mode,
flags,
id,
path
}, options)
if (result.err) {
throw result.err
}
result = ipc.sendSync('fs.read', {
id,
size: stats.size,
offset: 0
}, { responseType: 'arraybuffer' })
if (result.err) {
throw result.err
}
const data = result.data
result = ipc.sendSync('fs.close', { id }, options)
if (result.err) {
throw result.err
}
const buffer = data ? Buffer.from(data) : Buffer.alloc(0)
if (typeof options?.encoding === 'string') {
return buffer.toString(options.encoding)
}
return buffer
}
/**
* Reads link at `path`
* @param {string} path
* @param {function(err, string)} callback
*/
export function readlink (path, callback) {
if (typeof path !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
path = normalizePath(path)
ipc.request('fs.readlink', { path }).then((result) => {
result?.err ? callback(result.err) : callback(null, result.data.path)
}).catch(callback)
}
/**
* Computes real path for `path`
* @param {string} path
* @param {function(err, string)} callback
*/
export function realpath (path, callback) {
if (typeof path !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
ipc.request('fs.realpath', { path }).then((result) => {
result?.err ? callback(result.err) : callback(result.data.path)
}).catch(callback)
}
/**
* Computes real path for `path`
* @param {string} path
*/
export function realpathSync (path) {
if (typeof path !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
const result = ipc.sendSync('fs.realpath', { path })
if (result.err) {
throw result.err
}
}
/**
* Renames file or directory at `src` to `dest`.
* @param {string} src
* @param {string} dest
* @param {function} callback
*/
export function rename (src, dest, callback) {
src = normalizePath(src)
dest = normalizePath(dest)
if (typeof src !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
if (typeof dest !== 'string') {
throw new TypeError('The argument \'dest\' must be a string')
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
ipc.request('fs.rename', { src, dest }).then((result) => {
result?.err ? callback(result.err) : callback(null)
})
}
/**
* Renames file or directory at `src` to `dest`, synchronously.
* @param {string} src
* @param {string} dest
*/
export function renameSync (src, dest) {
src = normalizePath(src)
dest = normalizePath(dest)
if (typeof src !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
if (typeof dest !== 'string') {
throw new TypeError('The argument \'dest\' must be a string')
}
const result = ipc.sendSync('fs.rename', { src, dest })
if (result.err) {
throw result.err
}
}
/**
* Removes directory at `path`.
* @param {string} path
* @param {function} callback
*/
export function rmdir (path, callback) {
path = normalizePath(path)
if (typeof path !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
ipc.request('fs.rmdir', { path }).then((result) => {
result?.err ? callback(result.err) : callback(null)
})
}
/**
* Removes directory at `path`, synchronously.
* @param {string} path
*/
export function rmdirSync (path) {
path = normalizePath(path)
if (typeof path !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
const result = ipc.sendSync('fs.rmdir', { path })
if (result.err) {
throw result.err
}
}
/**
* Synchronously get the stats of a file
* @param {string | Buffer | URL | number } path - filename or file descriptor
* @param {object?} options
* @param {string?} [options.encoding ? 'utf8']
* @param {string?} [options.flag ? 'r']
*/
export function statSync (path, options = null) {
path = normalizePath(path)
const result = ipc.sendSync('fs.stat', { path })
if (result.err) {
throw result.err
}
return Stats.from(result.data, Boolean(options?.bigint))
}
/**
* Get the stats of a file
* @param {string | Buffer | URL | number } path - filename or file descriptor
* @param {object?} options
* @param {string?} [options.encoding ? 'utf8']
* @param {string?} [options.flag ? 'r']
* @param {AbortSignal?} [options.signal]
* @param {function(Error?, Stats?)} callback
*/
export function stat (path, options, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
if (
path instanceof globalThis.FileSystemFileHandle ||
path instanceof globalThis.FileSystemDirectoryHandle
) {
if (path.getFile()[kFileDescriptor]) {
try {
path.getFile()[kFileDescriptor].stat(options).then(
(stats) => callback(null, stats),
(err) => callback(err)
)
} catch (err) {
callback(err)
}
return
} else {
queueMicrotask(() => {
const info = {
st_mode: path instanceof globalThis.FileSystemDirectoryHandle
? constants.S_IFDIR
: constants.S_IFREG,
st_size: path instanceof globalThis.FileSystemFileHandle
? path.size
: 0
}
const stats = Stats.from(info, Boolean(options?.bigint))
callback(null, stats)
})
return
}
}
visit(path, {}, async (err, handle) => {
let stats = null
if (err) {
callback(err)
return
}
try {
stats = await handle.stat(options)
} catch (err) {
callback(err)
return
}
callback(null, stats)
})
}
/**
* Get the stats of a symbolic link
* @param {string | Buffer | URL | number } path - filename or file descriptor
* @param {object?} options
* @param {string?} [options.encoding ? 'utf8']
* @param {string?} [options.flag ? 'r']
* @param {AbortSignal?} [options.signal]
* @param {function(Error?, Stats?)} callback
*/
export function lstat (path, options, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
if (
path instanceof globalThis.FileSystemFileHandle ||
path instanceof globalThis.FileSystemDirectoryHandle
) {
return stat(path, options, callback)
}
visit(path, {}, async (err, handle) => {
let stats = null
if (err) {
callback(err)
return
}
try {
stats = await handle.lstat(options)
} catch (err) {
callback(err)
return
}
callback(null, stats)
})
}
/**
* Creates a symlink of `src` at `dest`.
* @param {string} src
* @param {string} dest
*/
export function symlink (src, dest, type = null, callback) {
let flags = 0
src = normalizePath(src)
dest = normalizePath(dest)
if (typeof src !== 'string') {
throw new TypeError('The argument \'src\' must be a string')
}
if (typeof dest !== 'string') {
throw new TypeError('The argument \'dest\' must be a string')
}
if (typeof type === 'function') {
callback = type
}
if (!type) {
type = 'file'
}
if (type === 'file') {
flags = 0
} else if (type === 'dir') {
flags = constants.UV_FS_SYMLINK_DIR
} else if (type === 'junction') {
flags = constants.UV_FS_SYMLINK_JUNCTION
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
ipc.request('fs.symlink', { src, dest, flags }).then((result) => {
result?.err ? callback(result.err) : callback(null)
}).catch(callback)
}
/**
* Unlinks (removes) file at `path`.
* @param {string} path
* @param {function} callback
*/
export function unlink (path, callback) {
path = normalizePath(path)
if (typeof path !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
ipc.request('fs.unlink', { path }).then((result) => {
result?.err ? callback(result.err) : callback(null)
}).catch(callback)
}
/**
* Unlinks (removes) file at `path`, synchronously.
* @param {string} path
*/
export function unlinkSync (path) {
path = normalizePath(path)
if (typeof path !== 'string') {
throw new TypeError('The argument \'path\' must be a string')
}
const result = ipc.sendSync('fs.unlink', { path })
if (result.err) {
throw result.err
}
}
/**
* @see {@url https://nodejs.org/api/fs.html#fswritefilefile-data-options-callback}
* @param {string | Buffer | URL | number } path - filename or file descriptor
* @param {string | Buffer | TypedArray | DataView | object } data
* @param {object?} options
* @param {string?} [options.encoding ? 'utf8']
* @param {string?} [options.mode ? 0o666]
* @param {string?} [options.flag ? 'w']
* @param {AbortSignal?} [options.signal]
* @param {function(Error?)} callback
*/
export function writeFile (path, data, options, callback) {
if (typeof options === 'function') {
callback = options
options = { encoding: 'utf8' }
}
if (typeof options === 'string') {
options = { encoding: options }
}
path = normalizePath(path)
options = { mode: 0o666, flag: 'w', ...options }
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function.')
}
visit(path, options, async (err, handle) => {
if (err) {
callback(err)
return
}
try {
await handle.writeFile(data, options)
} catch (err) {
callback(err)
return
}
callback(null)
})
}
/**
* Writes data to a file synchronously.
* @param {string | Buffer | URL | number } path - filename or file descriptor
* @param {string | Buffer | TypedArray | DataView | object } data
* @param {object?} options
* @param {string?} [options.encoding ? 'utf8']
* @param {string?} [options.mode ? 0o666]
* @param {string?} [options.flag ? 'w']
* @param {AbortSignal?} [options.signal]
* @see {@link https://nodejs.org/api/fs.html#fswritefilesyncfile-data-options}
*/
export function writeFileSync (path, data, options) {
const id = String(options?.id || rand64())
let result = ipc.sendSync('fs.open', {
id,
mode: options?.mode || 0o666,
path,
flags: options?.flags ? normalizeFlags(options.flags) : 'w'
}, options)
if (result.err) {
throw result.err
}
result = ipc.sendSync('fs.write', { id, offset: 0 }, null, data)
if (result.err) {
throw result.err
}
result = ipc.sendSync('fs.close', { id }, options)
if (result.err) {
throw result.err
}
}
/**
* Watch for changes at `path` calling `callback`
* @param {string}
* @param {function|object=} [options]
* @param {string=} [options.encoding = 'utf8']
* @param {?function} [callback]
* @return {Watcher}
*/
export function watch (path, options, callback = null) {
if (typeof options === 'function') {
callback = options
}
path = normalizePath(path)
const watcher = new Watcher(path, options)
watcher.on('change', callback)
return watcher
}
// re-exports
export {
bookmarks,
constants,
Dir,
DirectoryHandle,
Dirent,
fds,
FileHandle,
promises,
ReadStream,
Stats,
Watcher,
WriteStream
}
export default exports
for (const key in exports) {
const value = exports[key]
if (key in promises && isFunction(value) && isFunction(promises[key])) {
value[Symbol.for('nodejs.util.promisify.custom')] = promises[key]
value[Symbol.for('socket.runtime.util.promisify.custom')] = promises[key]
}
}