Keep a job from running twice at once, across processes on one machine, with a lock:
ts
import { acquireLock } from 'jg-fs';const lock = await acquireLock('.locks/nightly-report'); // LOCK_BUSY while another process holds ittry { await buildNightlyReport();} finally { await lock.release();}
The lock is a directory holding an owner.json that names the process. When its owner’s process is gone, as after a crash, the next acquireLock on the same host takes it over. Use it on local filesystems only: a lock is never taken over from another host.
Keep keys, tokens, and sessions private to the user who owns them:
ts
import { createPrivateDirectory, readPrivateFile, writePrivateFile } from 'jg-fs';await createPrivateDirectory(configDirectory); // 0o700; DIRECTORY_NOT_PRIVATE if it exists and isn'tawait writePrivateFile(`${configDirectory}/token.json`, token, { rootPath: configDirectory }); // atomic, 0o600const saved = await readPrivateFile(`${configDirectory}/token.json`, { maxBytes: 16 * 1024, rootPath: configDirectory,});
A private file is a regular file, never a symbolic link, with no symbolic link anywhere in its path (PATH_NOT_CANONICAL), and without group or other permissions (FILE_NOT_PRIVATE). The checks run on the open file, so the content read is the content checked. A shared file is never overwritten, and permissions are never changed for you: fix them, or remove the file. writePrivateFile with isExclusive creates the file and throws FILE_ALREADY_EXISTS when anything is already at the path.
On Windows, Node.js reports only a file’s read-only attribute as its mode, so the permission checks are skipped there. A private file on Windows depends on its directory’s access control list, such as a directory inside the user’s profile.
Failures throw Exceptions from jg-exception, with the codes in ERRORS.
Removing a stuck lock
A lock left in a state no process can safely take over stays busy, and LOCK_BUSY’s data.reason says why:
held: the owner named in owner.json is still running. Wait, or stop that process.
other-host: the owner runs on another host, which this host can’t check.
ownerless: owner.json is missing or invalid, such as after a crash right after the directory was created. A lock being created or released at that moment looks the same, so try again first.
recovery-in-progress: a recovery directory is inside, so another process is taking the lock over, or crashed while doing so.
unexpected-entries: the directory holds something besides owner.json, such as a .tmp file left by an interrupted write.
Before deleting a stuck lock’s directory by hand, confirm that no process is using it: check the pid and hostname in owner.json, when there is one. Then delete the whole directory.
Generated from TSDoc
API reference
Every export of [email protected], 84 in all, with its signature, its errors, and an example.
Acquires a lock that only one process at a time can hold, such as one guarding a script that mustn't run twice at once. The lock is a directory at path, created with mode 0o700, holding an owner.json, mode 0o600, that names the process.
When the lock already exists, it's taken over only from an owner on this host whose process is gone, and only when the directory holds nothing but owner.json. Every other case fails with LOCK_BUSY, whose data.reason says why: held, other-host, ownerless, recovery-in-progress, or unexpected-entries. A lock left behind by a crash at the wrong moment stays busy until it's removed by hand, as the README describes.
Use it on local filesystems only. POSIX modes don't apply on Windows, and a lock is never taken over from another host.
Signature
acquireLock(path: string): Promise<IFileLock>
Parameters
pathstring
The lock's directory, whose parent must exist.
Returns
Promise<IFileLock>
A promise that resolves to the lock, released with release().
Throws
LOCK_BUSYThe lock '<path>' is busy.
NOT_A_DIRECTORYThe path '<path>' is not a directory.
LOCK_ACQUISITION_FAILEDThe lock '<path>' could not be acquired.
Checks that a directory is private to its owner: a directory that isn't a symbolic link, with no symbolic link anywhere in its path, and, on POSIX, without group or other permissions. On Windows, the permission check is skipped, as for files.
Checks that a file is private to its owner, such as a key or a session file before it's trusted: a regular file that isn't a symbolic link, with no symbolic link anywhere in its path, and, on POSIX, without group or other permissions. With rootPath, it must be strictly inside that directory; with maxBytes, no larger. The checks run on the open file, so a file swapped in between them is caught.
On Windows, Node.js reports only a file's read-only attribute as its mode, so the permission check is skipped there: a private file on Windows depends on its directory's access control list.
Creates a directory, and any missing parents, with 0o700 permissions, and checks that it's private, as assertPrivateDirectory does. An existing directory that isn't private throws: its permissions are never changed silently.
Lists a directory's elements grouped by kind: directories, files (filtered by extension), and symbolic links, each group sorted. Entries removed while the directory is read are left out, and at most 32 entries are read at once.
A promise that resolves to the path's details, or null when it doesn't exist.
Example
import { getPathElement } from 'jg-fs';const element = await getPathElement('logo.png');element?.size; // the size in bytes, or undefined when the path doesn't exist
Checks whether a path is a root path or nested inside it. The check is lexical: both paths are resolved without touching the filesystem, and symbolic links aren't followed. Sibling paths that share a prefix, such as /data/app and /data/application, aren't nested, and neither is a path on another drive on Windows.
Reads a regular file of at most maxBytes bytes. It never follows a symbolic link in the path's last component, though links in parent directories are followed, so callers that need containment combine it with isPathWithin. It reads at most maxBytes + 1 bytes, even when the file grows while it's read.
Whether to include the contents of subdirectories.
Defaults to false.
Returns
Promise<string[]>
A promise that resolves to the content paths.
Throws
NOT_A_DIRECTORYif the path doesn't exist or is not a directory
Example
import { readDirectory } from 'jg-fs';await readDirectory('dist'); // ['dist/index.js', 'dist/assets']await readDirectory('dist', true); // includes the contents of dist/assets
Reads and parses a JSON file, and validates it with the schema when there is one. Without a schema, the value is unknown, or the type you pass. With maxBytes, it reads the file as readBoundedFile does, for a file whose size isn't yours to decide.
Reads a file after checking that it's private, as assertPrivateFile does, through the same open file, so the content read is the content checked. It reads at most maxBytes bytes, even when the file grows while it's read.
Writes a file atomically: readers see either the previous content or the complete new content, never a partial write. The data goes to a temporary file in the same directory, which is flushed to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins; without one, a replaced file keeps its permission bits and a new file gets the platform default. The temporary file is never more widely readable than the final mode. When writing fails before the rename, the temporary file is removed and the destination is untouched. While Windows refuses the rename because another process, such as an antivirus scanner, holds the file, it tries again for about 4 seconds. A symbolic link at the path is replaced by the file.
The permission bits of the written file, such as 0o600.
Returns
Promise<void>
A promise that resolves once the file is written and flushed.
Example
import { writeFileAtomic } from 'jg-fs';const state = { step: 2 };await writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial fileawait writeFileAtomic('secrets.env', 'TOKEN=abc\n', 0o600); // only the owner can read it
Writes a JSON file atomically, as writeFileAtomic does, so a crash or a full disk never leaves a truncated file that won't parse. A symbolic link at the path keeps pointing at the file, whose target is replaced. A string is written as given; anything else is stringified.
Writes a file that only its owner can read and write, such as a token or a session. The file's directory must have no symbolic link in its path, and with rootPath, the file must be strictly inside that directory.
By default, it replaces the file atomically with 0o600 permissions, through a temporary file that's never more widely readable; a file already at the path must be private, as assertPrivateFile checks, so a symbolic link or a shared file is never overwritten. With isExclusive, it creates the file and fails when anything is already at the path.
type IReadJsonFileOptions<T> = { schema?: ISchema<T>; maxBytes?: number;};
Members
schemaproperty
ISchema<T>
validates the parsed content, such as a Zod schema. Its errors propagate unchanged
maxBytesproperty
number
the largest file accepted, in bytes, for a file whose size isn't yours to decide, such as one a user uploaded. With it, the file is read as readBoundedFile reads it
Acquires a lock that only one process at a time can hold, such as one guarding a script that mustn't run twice at once. The lock is a directory at path, created with mode 0o700, holding an owner.json, mode 0o600, that names the process.
When the lock already exists, it's taken over only from an owner on this host whose process is gone, and only when the directory holds nothing but owner.json. Every other case fails with LOCK_BUSY, whose data.reason says why: held, other-host, ownerless, recovery-in-progress, or unexpected-entries. A lock left behind by a crash at the wrong moment stays busy until it's removed by hand, as the README describes.
Use it on local filesystems only. POSIX modes don't apply on Windows, and a lock is never taken over from another host.
Signature
acquireLock(path: string): IFileLock
Parameters
pathstring
The lock's directory, whose parent must exist.
Returns
IFileLock
The lock, released with release().
Throws
LOCK_BUSYThe lock '<path>' is busy.
NOT_A_DIRECTORYThe path '<path>' is not a directory.
LOCK_ACQUISITION_FAILEDThe lock '<path>' could not be acquired.
Checks that a directory is private to its owner: a directory that isn't a symbolic link, with no symbolic link anywhere in its path, and, on POSIX, without group or other permissions. On Windows, the permission check is skipped, as for files.
Signature
assertPrivateDirectory(path: string): void
Parameters
pathstring
The directory's path.
Throws
NOT_A_DIRECTORYif the path doesn't exist, is a symbolic link, or isn't a directory
PATH_NOT_CANONICALif the path goes through a symbolic link
DIRECTORY_NOT_PRIVATEif other users can access the directory
Example
import { assertPrivateDirectory } from 'jg-fs/sync';assertPrivateDirectory('/home/ada/.config/app');
Checks that a file is private to its owner, such as a key or a session file before it's trusted: a regular file that isn't a symbolic link, with no symbolic link anywhere in its path, and, on POSIX, without group or other permissions. With rootPath, it must be strictly inside that directory; with maxBytes, no larger. The checks run on the open file, so a file swapped in between them is caught.
On Windows, Node.js reports only a file's read-only attribute as its mode, so the permission check is skipped there: a private file on Windows depends on its directory's access control list.
Whether to delete an existing directory and its contents first.
Throws
DIRECTORY_ALREADY_EXISTSif something other than a directory exists at the path
Example
import { createDirectory } from 'jg-fs/sync';createDirectory('dist/assets'); // creates missing parents toocreateDirectory('dist', true); // replaces it with an empty directory
Creates a directory, and any missing parents, with 0o700 permissions, and checks that it's private, as assertPrivateDirectory does. An existing directory that isn't private throws: its permissions are never changed silently.
Signature
createPrivateDirectory(path: string): void
Parameters
pathstring
The directory's path.
Throws
NOT_A_DIRECTORYif the path exists and isn't a directory
PATH_NOT_CANONICALif the path goes through a symbolic link
DIRECTORY_NOT_PRIVATEif other users can access the directory
Example
import { createPrivateDirectory } from 'jg-fs/sync';createPrivateDirectory('/home/ada/.config/app/sessions');
Lists a directory's elements grouped by kind: directories, files (filtered by extension), and symbolic links, each group sorted. Entries removed while the directory is read are left out.
Reads a path's details without following a symbolic link at the path.
Signature
getPathElement(path: string): IPathElement | null
Parameters
pathstring
The path.
Returns
IPathElement | null
The path's details, or null when it doesn't exist.
Example
import { getPathElement } from 'jg-fs/sync';const element = getPathElement('logo.png');element?.size; // the size in bytes, or undefined when the path doesn't exist
Checks whether a path is a root path or nested inside it. The check is lexical: both paths are resolved without touching the filesystem, and symbolic links aren't followed. Sibling paths that share a prefix, such as /data/app and /data/application, aren't nested, and neither is a path on another drive on Windows.
Reads a regular file of at most maxBytes bytes. It never follows a symbolic link in the path's last component, though links in parent directories are followed, so callers that need containment combine it with isPathWithin. It reads at most maxBytes + 1 bytes, even when the file grows while it's read.
Whether to include the contents of subdirectories.
Defaults to false.
Returns
string[]
The content paths.
Throws
NOT_A_DIRECTORYif the path doesn't exist or is not a directory
Example
import { readDirectory } from 'jg-fs/sync';readDirectory('dist'); // ['dist/index.js', 'dist/assets']readDirectory('dist', true); // includes the contents of dist/assets
Reads and parses a JSON file, and validates it with the schema when there is one. Without a schema, the value is unknown, or the type you pass. With maxBytes, it reads the file as readBoundedFile does, for a file whose size isn't yours to decide.
Signature
readJsonFile<T = unknown>(path: string, options?: IReadJsonFileOptions<T>): T
Parameters
pathstring
The file's path.
options?IReadJsonFileOptions<T>
The schema, such as a Zod schema, whose errors propagate unchanged, and the largest file accepted, in bytes.
Defaults to {}.
Returns
T
The parsed value.
Throws
NOT_A_FILEif the path doesn't exist or is not a file
FILE_TOO_LARGEif the file holds more than maxBytes bytes
FILE_CONTENT_IS_EMPTY_OR_INVALIDif the file's JSON content cannot be parsed
Example 1
import { readJsonFile } from 'jg-fs/sync';const config = readJsonFile('config.json', { schema: Config }); // Config's type
Example 2
import { readJsonFile } from 'jg-fs/sync';const value = readJsonFile('data.json'); // unknown
Reads a file after checking that it's private, as assertPrivateFile does, through the same open file, so the content read is the content checked. It reads at most maxBytes bytes, even when the file grows while it's read.
Writes a file atomically: readers see either the previous content or the complete new content, never a partial write. The data goes to a temporary file in the same directory, which is flushed to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins; without one, a replaced file keeps its permission bits and a new file gets the platform default. The temporary file is never more widely readable than the final mode. When writing fails before the rename, the temporary file is removed and the destination is untouched. While Windows refuses the rename because another process, such as an antivirus scanner, holds the file, it tries again for about 4 seconds. A symbolic link at the path is replaced by the file.
The permission bits of the written file, such as 0o600.
Example
import { writeFileAtomic } from 'jg-fs/sync';const state = { step: 2 };writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial filewriteFileAtomic('secrets.env', 'TOKEN=abc\n', 0o600); // only the owner can read it
Writes a JSON file atomically, as writeFileAtomic does, so a crash or a full disk never leaves a truncated file that won't parse. A symbolic link at the path keeps pointing at the file, whose target is replaced. A string is written as given; anything else is stringified.
Writes a file that only its owner can read and write, such as a token or a session. The file's directory must have no symbolic link in its path, and with rootPath, the file must be strictly inside that directory.
By default, it replaces the file atomically with 0o600 permissions, through a temporary file that's never more widely readable; a file already at the path must be private, as assertPrivateFile checks, so a symbolic link or a shared file is never overwritten. With isExclusive, it creates the file and fails when anything is already at the path.
type IReadJsonFileOptions<T> = { schema?: ISchema<T>; maxBytes?: number;};
Members
schemaproperty
ISchema<T>
validates the parsed content, such as a Zod schema. Its errors propagate unchanged
maxBytesproperty
number
the largest file accepted, in bytes, for a file whose size isn't yours to decide, such as one a user uploaded. With it, the file is read as readBoundedFile reads it