# jg-fs

> Filesystem helpers, async and sync: path checks, directories, text and JSON files, atomic writes, and bounded reads.

- **Version:** `1.4.0`
- **Runtimes:** Node.js
- **Install:** `npm install jg-fs`
- **On npm:** [jg-fs](https://www.npmjs.com/package/jg-fs)
- **Source:** [packages/jg-fs](https://github.com/jesusgraterol/jg-packages/tree/main/packages/jg-fs)
- **Recipes:** [jg-fs recipes](https://jg-packages.jesusgraterol.dev/packages/jg-fs/recipes/index.md)

## Usage

The root entry is async, and `jg-fs/sync` has the same functions, synchronous:

```ts
import { readJsonFile, writeFileAtomic, writeJsonFile, writeTextFile } from 'jg-fs';
import { readTextFile } from 'jg-fs/sync';
import { z } from 'zod';

const Config = z.object({ port: z.number() });

await writeTextFile('notes/today.md', '# Today'); // creates notes/ when it's missing
const config = await readJsonFile('config.json', { schema: Config }); // validated, and typed by the schema
await writeJsonFile('config.json', { ...config, port: 3000 }); // atomic: readers never see a partial file
await writeFileAtomic('state.txt', 'step 2'); // any content, atomically
const upload = await readJsonFile('uploads/import.json', { maxBytes: 1_048_576 }); // FILE_TOO_LARGE past 1 MB

const readme = readTextFile('README.md');
```

List, filter, and sort a directory:

```ts
import { getDirectoryElements } from 'jg-fs';

const { files, directories } = await getDirectoryElements('content', {
  includeExts: ['.md'],
  sortByKey: 'creation',
  sortOrder: 'desc',
});
```

Read files from untrusted paths with a size limit, without following a symbolic link:

```ts
import { isPathWithin, readBoundedFile } from 'jg-fs';

if (isPathWithin(uploadsDirectory, path)) {
  const upload = await readBoundedFile(path, 5 * 1024 * 1024);
}
```

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 it
try {
  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't
await writePrivateFile(`${configDirectory}/token.json`, token, { rootPath: configDirectory }); // atomic, 0o600
const 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 `Exception`s 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.

## <a id="api-reference"></a>jg-fs API reference

Every export of `jg-fs@1.4.0`, 84 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.

### <a id="api-entry-main"></a>`jg-fs`

- Functions: [`acquireLock`](#api-acquireLock), [`assertPrivateDirectory`](#api-assertPrivateDirectory), [`assertPrivateFile`](#api-assertPrivateFile), [`copyDirectory`](#api-copyDirectory), [`copyFile`](#api-copyFile), [`createDirectory`](#api-createDirectory), [`createDirectorySymLink`](#api-createDirectorySymLink), [`createFileSymLink`](#api-createFileSymLink), [`createPrivateDirectory`](#api-createPrivateDirectory), [`deleteDirectory`](#api-deleteDirectory), [`deleteFile`](#api-deleteFile), [`getDirectoryElements`](#api-getDirectoryElements), [`getPathElement`](#api-getPathElement), [`isDirectory`](#api-isDirectory), [`isFile`](#api-isFile), [`isPathWithin`](#api-isPathWithin), [`pathExists`](#api-pathExists), [`readBoundedFile`](#api-readBoundedFile), [`readBufferFile`](#api-readBufferFile), [`readDirectory`](#api-readDirectory), [`readFile`](#api-readFile), [`readJsonFile`](#api-readJsonFile), [`readPrivateFile`](#api-readPrivateFile), [`readTextFile`](#api-readTextFile), [`writeBufferFile`](#api-writeBufferFile), [`writeFile`](#api-writeFile), [`writeFileAtomic`](#api-writeFileAtomic), [`writeJsonFile`](#api-writeJsonFile), [`writePrivateFile`](#api-writePrivateFile), [`writeTextFile`](#api-writeTextFile)
- Constants: [`ERRORS`](#api-ERRORS)
- Types: [`IDirectoryElementsKeySort`](#api-IDirectoryElementsKeySort), [`IDirectoryElementsOptions`](#api-IDirectoryElementsOptions), [`IDirectoryPathElements`](#api-IDirectoryPathElements), [`IFileLock`](#api-IFileLock), [`IFileLockOwner`](#api-IFileLockOwner), [`IPathElement`](#api-IPathElement), [`IPrivateFileOptions`](#api-IPrivateFileOptions), [`IReadJsonFileOptions`](#api-IReadJsonFileOptions), [`IReadPrivateFileOptions`](#api-IReadPrivateFileOptions), [`ISchema`](#api-ISchema), [`IWritePrivateFileOptions`](#api-IWritePrivateFileOptions)

#### <a id="api-functions"></a>Functions in `jg-fs`

##### <a id="api-acquireLock"></a>`acquireLock`

*Function*

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.

```ts
acquireLock(path: string): Promise<IFileLock>
```

**Parameters**

- `path` `string`: The lock's directory, whose parent must exist.

**Returns** `Promise<IFileLock>`: A promise that resolves to the lock, released with `release()`.

**Throws**

- `LOCK_BUSY`: The lock '<path>' is busy.
- `NOT_A_DIRECTORY`: The path '<path>' is not a directory.
- `LOCK_ACQUISITION_FAILED`: The lock '<path>' could not be acquired.

**Example**

```ts
import { acquireLock, writeFileAtomic } from 'jg-fs';

const lock = await acquireLock('.locks/nightly-report');
try {
  await writeFileAtomic('reports/nightly.json', JSON.stringify({ generatedAt: Date.now() }));
} finally {
  await lock.release();
}
```

##### <a id="api-assertPrivateDirectory"></a>`assertPrivateDirectory`

*Function*

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.

```ts
assertPrivateDirectory(path: string): Promise<void>
```

**Parameters**

- `path` `string`: The directory's path.

**Returns** `Promise<void>`: A promise that resolves once the directory passes every check.

**Throws**

- `NOT_A_DIRECTORY`: if the path doesn't exist, is a symbolic link, or isn't a directory
- `PATH_NOT_CANONICAL`: if the path goes through a symbolic link
- `DIRECTORY_NOT_PRIVATE`: if other users can access the directory

**Example**

```ts
import { assertPrivateDirectory } from 'jg-fs';

await assertPrivateDirectory('/home/ada/.config/app');
```

##### <a id="api-assertPrivateFile"></a>`assertPrivateFile`

*Function*

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.

```ts
assertPrivateFile(path: string, options?: IPrivateFileOptions): Promise<void>
```

**Parameters**

- `path` `string`: The file's path.
- `options?` `IPrivateFileOptions`: The root it must be inside, and the largest size accepted, in bytes. Defaults to `{}`.

**Returns** `Promise<void>`: A promise that resolves once the file passes every check.

**Throws**

- `NON_EXISTENT_FILE`: if the file doesn't exist
- `NOT_A_FILE`: if the path's final component is a symbolic link, or it isn't a regular file
- `FILE_NOT_PRIVATE`: if other users can access the file
- `FILE_TOO_LARGE`: if the file holds more than maxBytes bytes
- `PATH_NOT_CANONICAL`: if the path or the root goes through a symbolic link
- `PATH_OUTSIDE_ROOT`: if the file isn't strictly inside the root
- `NOT_A_DIRECTORY`: if the root doesn't exist or isn't a directory

**Example**

```ts
import { assertPrivateFile } from 'jg-fs';

await assertPrivateFile('/home/ada/.config/app/token.json', { rootPath: '/home/ada/.config/app' });
```

##### <a id="api-copyDirectory"></a>`copyDirectory`

*Function*

Copies a directory and everything in it, replacing the destination completely.

```ts
copyDirectory(srcPath: string, destPath: string): Promise<void>
```

**Parameters**

- `srcPath` `string`: The directory to copy.
- `destPath` `string`: Where to copy it.

**Returns** `Promise<void>`: A promise that resolves once the copy is complete.

**Throws**

- `NOT_A_DIRECTORY`: if the srcPath is not a directory

**Example**

```ts
import { copyDirectory } from 'jg-fs';

await copyDirectory('templates', 'dist/templates');
```

##### <a id="api-copyFile"></a>`copyFile`

*Function*

Copies a file, replacing the destination when it exists.

```ts
copyFile(srcPath: string, destPath: string): Promise<void>
```

**Parameters**

- `srcPath` `string`: The file to copy.
- `destPath` `string`: Where to copy it.

**Returns** `Promise<void>`: A promise that resolves once the copy exists.

**Throws**

- `NOT_A_FILE`: if the srcPath doesn't exist or is not a file

**Example**

```ts
import { copyFile } from 'jg-fs';

await copyFile('config.example.json', 'config.json');
```

##### <a id="api-createDirectory"></a>`createDirectory`

*Function*

Creates a directory, with any missing parents. An existing directory is kept, or replaced with an empty one when `deleteIfExists` is set.

```ts
createDirectory(path: string, deleteIfExists?: boolean): Promise<void>
```

**Parameters**

- `path` `string`: The directory's path.
- `deleteIfExists?` `boolean`: Whether to delete an existing directory and its contents first.

**Returns** `Promise<void>`: A promise that resolves once the directory exists.

**Throws**

- `DIRECTORY_ALREADY_EXISTS`: if something other than a directory exists at the path

**Example**

```ts
import { createDirectory } from 'jg-fs';

await createDirectory('dist/assets'); // creates missing parents too
await createDirectory('dist', true); // replaces it with an empty directory
```

##### <a id="api-createDirectorySymLink"></a>`createDirectorySymLink`

*Function*

Creates a symbolic link to a directory.

```ts
createDirectorySymLink(target: string, path: string): Promise<void>
```

**Parameters**

- `target` `string`: The directory the link points to.
- `path` `string`: Where to create the link.

**Returns** `Promise<void>`: A promise that resolves once the link exists.

**Throws**

- `NON_EXISTENT_DIRECTORY`: if the target directory doesn't exist
- `NOT_A_DIRECTORY`: if the target directory is not considered a directory by the OS

**Example**

```ts
import { createDirectorySymLink } from 'jg-fs';

await createDirectorySymLink('releases/1.4.0', 'current');
```

##### <a id="api-createFileSymLink"></a>`createFileSymLink`

*Function*

Creates a symbolic link to a file.

```ts
createFileSymLink(target: string, path: string): Promise<void>
```

**Parameters**

- `target` `string`: The file the link points to.
- `path` `string`: Where to create the link.

**Returns** `Promise<void>`: A promise that resolves once the link exists.

**Throws**

- `NON_EXISTENT_FILE`: if the target file does not exist
- `NOT_A_FILE`: if the target is not a file

**Example**

```ts
import { createFileSymLink } from 'jg-fs';

await createFileSymLink('config.production.json', 'config.json');
```

##### <a id="api-createPrivateDirectory"></a>`createPrivateDirectory`

*Function*

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.

```ts
createPrivateDirectory(path: string): Promise<void>
```

**Parameters**

- `path` `string`: The directory's path.

**Returns** `Promise<void>`: A promise that resolves once the directory exists and is private.

**Throws**

- `NOT_A_DIRECTORY`: if the path exists and isn't a directory
- `PATH_NOT_CANONICAL`: if the path goes through a symbolic link
- `DIRECTORY_NOT_PRIVATE`: if other users can access the directory

**Example**

```ts
import { createPrivateDirectory } from 'jg-fs';

await createPrivateDirectory('/home/ada/.config/app/sessions');
```

##### <a id="api-deleteDirectory"></a>`deleteDirectory`

*Function*

Deletes a directory and everything in it. A missing directory is not an error.

```ts
deleteDirectory(path: string): Promise<void>
```

**Parameters**

- `path` `string`: The directory's path.

**Returns** `Promise<void>`: A promise that resolves once the directory is gone.

**Example**

```ts
import { deleteDirectory } from 'jg-fs';

await deleteDirectory('dist');
```

##### <a id="api-deleteFile"></a>`deleteFile`

*Function*

Deletes a file.

```ts
deleteFile(path: string): Promise<void>
```

**Parameters**

- `path` `string`: The file's path.

**Returns** `Promise<void>`: A promise that resolves once the file is gone.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

**Example**

```ts
import { deleteFile } from 'jg-fs';

await deleteFile('cache.json');
```

##### <a id="api-getDirectoryElements"></a>`getDirectoryElements`

*Function*

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.

```ts
getDirectoryElements(
  path: string,
  options?: Partial<IDirectoryElementsOptions>,
): Promise<IDirectoryPathElements>
```

**Parameters**

- `path` `string`: The directory's path.
- `options?` `Partial<IDirectoryElementsOptions>`: The extensions to include and how to sort.

**Returns** `Promise<IDirectoryPathElements>`: A promise that resolves to the grouped elements.

**Throws**

- `NOT_A_DIRECTORY`: if the path doesn't exist or is not a directory

**Example**

```ts
import { getDirectoryElements } from 'jg-fs';

const { files } = await getDirectoryElements('content', { includeExts: ['.md'], sortByKey: 'creation', sortOrder: 'desc' });
```

##### <a id="api-getPathElement"></a>`getPathElement`

*Function*

Reads a path's details without following a symbolic link at the path.

```ts
getPathElement(path: string): Promise<IPathElement | null>
```

**Parameters**

- `path` `string`: The path.

**Returns** `Promise<IPathElement | null>`: A promise that resolves to the path's details, or `null` when it doesn't exist.

**Example**

```ts
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
```

##### <a id="api-isDirectory"></a>`isDirectory`

*Function*

Checks whether a path exists and is a directory.

```ts
isDirectory(path: string): Promise<boolean>
```

**Parameters**

- `path` `string`: The path.

**Returns** `Promise<boolean>`: A promise that resolves to whether the path is a directory.

**Example**

```ts
import { isDirectory } from 'jg-fs';

await isDirectory('dist'); // true
```

##### <a id="api-isFile"></a>`isFile`

*Function*

Checks whether a path exists and is a regular file.

```ts
isFile(path: string): Promise<boolean>
```

**Parameters**

- `path` `string`: The path.

**Returns** `Promise<boolean>`: A promise that resolves to whether the path is a file.

**Example**

```ts
import { isFile } from 'jg-fs';

await isFile('package.json'); // true
```

##### <a id="api-isPathWithin"></a>`isPathWithin`

*Function*

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.

```ts
isPathWithin(rootPath: string, targetPath: string): boolean
```

**Parameters**

- `rootPath` `string`: The root path.
- `targetPath` `string`: The path to check.

**Returns** `boolean`: Whether the target is the root or inside it.

**Example**

```ts
import { isPathWithin } from 'jg-fs';

isPathWithin('/data/app', '/data/app/logs/today.log'); // true
isPathWithin('/data/app', '/data/app/../secrets'); // false
isPathWithin('/data/app', '/data/application'); // false
```

##### <a id="api-pathExists"></a>`pathExists`

*Function*

Checks whether a path exists, whether it's a file, a directory, or anything else.

```ts
pathExists(path: string): Promise<boolean>
```

**Parameters**

- `path` `string`: The path.

**Returns** `Promise<boolean>`: A promise that resolves to whether the path exists.

**Example**

```ts
import { pathExists } from 'jg-fs';

await pathExists('config.json'); // true when the file exists
```

##### <a id="api-readBoundedFile"></a>`readBoundedFile`

*Function*

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.

```ts
readBoundedFile(
  path: string,
  maxBytes: number,
): Promise<Buffer<ArrayBufferLike>>
```

**Parameters**

- `path` `string`: The file's path.
- `maxBytes` `number`: The largest size accepted, in bytes.

**Returns** `Promise<Buffer<ArrayBufferLike>>`: A promise that resolves to the content.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist, its final component is a symbolic link, or it isn't a regular file
- `FILE_TOO_LARGE`: if the file holds more than maxBytes bytes

**Example**

```ts
import { join } from 'node:path';
import { readBoundedFile } from 'jg-fs';

const upload = await readBoundedFile(join('uploads', 'avatar.png'), 5 * 1024 * 1024); // at most 5 MB
```

##### <a id="api-readBufferFile"></a>`readBufferFile`

*Function*

Reads a binary file. An empty file reads as an empty Buffer.

```ts
readBufferFile(path: string): Promise<Buffer<ArrayBufferLike>>
```

**Parameters**

- `path` `string`: The file's path.

**Returns** `Promise<Buffer<ArrayBufferLike>>`: A promise that resolves to the content.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

**Example**

```ts
import { readBufferFile } from 'jg-fs';

const image = await readBufferFile('avatar.png');
```

##### <a id="api-readDirectory"></a>`readDirectory`

*Function*

Lists a directory's contents as paths joined to the directory's path.

```ts
readDirectory(path: string, recursive?: boolean): Promise<string[]>
```

**Parameters**

- `path` `string`: The directory's path.
- `recursive?` `boolean`: Whether to include the contents of subdirectories. Defaults to `false`.

**Returns** `Promise<string[]>`: A promise that resolves to the content paths.

**Throws**

- `NOT_A_DIRECTORY`: if the path doesn't exist or is not a directory

**Example**

```ts
import { readDirectory } from 'jg-fs';

await readDirectory('dist'); // ['dist/index.js', 'dist/assets']
await readDirectory('dist', true); // includes the contents of dist/assets
```

##### <a id="api-readFile"></a>`readFile`

*Function*

Reads a file as a string when an encoding is given, or as a Buffer otherwise.

```ts
readFile(path: string, options: IReadStringFileOptions): Promise<string>
```

**Parameters**

- `path` `string`: The file's path.
- `options` `IReadStringFileOptions`: Node.js's readFile options, such as the encoding.

**Returns** `Promise<string>`: A promise that resolves to the content.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

```ts
readFile(
  path: string,
  options?: IReadBufferFileOptions,
): Promise<Buffer<ArrayBufferLike>>
```

**Parameters**

- `path` `string`: The file's path.
- `options?` `IReadBufferFileOptions`: Node.js's readFile options, such as the encoding.

**Returns** `Promise<Buffer<ArrayBufferLike>>`: A promise that resolves to the content.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

```ts
readFile(
  path: string,
  options?: IReadFileOptions,
): Promise<string | Buffer<ArrayBufferLike>>
```

**Parameters**

- `path` `string`: The file's path.
- `options?` `IReadFileOptions`: Node.js's readFile options, such as the encoding.

**Returns** `Promise<string | Buffer<ArrayBufferLike>>`: A promise that resolves to the content.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

**Example**

```ts
import { readFile } from 'jg-fs';

await readFile('notes.md', 'utf-8'); // a string
await readFile('avatar.png'); // a Buffer
```

##### <a id="api-readJsonFile"></a>`readJsonFile`

*Function*

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.

```ts
readJsonFile<T = unknown>(
  path: string,
  options?: IReadJsonFileOptions<T>,
): Promise<T>
```

**Parameters**

- `path` `string`: 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** `Promise<T>`: A promise that resolves to the parsed value.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file
- `FILE_TOO_LARGE`: if the file holds more than maxBytes bytes
- `FILE_CONTENT_IS_EMPTY_OR_INVALID`: if the file's JSON content cannot be parsed

**Example 1**

```ts
import { readJsonFile } from 'jg-fs';

const config = await readJsonFile('config.json', { schema: Config }); // Config's type
```

**Example 2**

```ts
import { readJsonFile } from 'jg-fs';

const value = await readJsonFile('data.json'); // unknown
```

##### <a id="api-readPrivateFile"></a>`readPrivateFile`

*Function*

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.

```ts
readPrivateFile(
  path: string,
  options: IReadPrivateFileOptions,
): Promise<Buffer<ArrayBufferLike>>
```

**Parameters**

- `path` `string`: The file's path.
- `options` `IReadPrivateFileOptions`: The largest size accepted, in bytes, and the root the file must be inside.

**Returns** `Promise<Buffer<ArrayBufferLike>>`: A promise that resolves to the content.

**Throws**

- `NON_EXISTENT_FILE`: if the file doesn't exist
- `NOT_A_FILE`: if the path's final component is a symbolic link, or it isn't a regular file
- `FILE_NOT_PRIVATE`: if other users can access the file
- `FILE_TOO_LARGE`: if the file holds more than maxBytes bytes
- `PATH_NOT_CANONICAL`: if the path or the root goes through a symbolic link
- `PATH_OUTSIDE_ROOT`: if the file isn't strictly inside the root
- `NOT_A_DIRECTORY`: if the root doesn't exist or isn't a directory

**Example**

```ts
import { readPrivateFile } from 'jg-fs';

const key = await readPrivateFile('/home/ada/.ssh/id_ed25519', { maxBytes: 16 * 1024 });
```

##### <a id="api-readTextFile"></a>`readTextFile`

*Function*

Reads a UTF-8 text file. An empty file reads as an empty string.

```ts
readTextFile(path: string): Promise<string>
```

**Parameters**

- `path` `string`: The file's path.

**Returns** `Promise<string>`: A promise that resolves to the text.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

**Example**

```ts
import { readTextFile } from 'jg-fs';

const markdown = await readTextFile('README.md');
```

##### <a id="api-writeBufferFile"></a>`writeBufferFile`

*Function*

Writes a binary file. An empty Buffer writes an empty file.

```ts
writeBufferFile(path: string, data: Buffer): Promise<void>
```

**Parameters**

- `path` `string`: The file's path.
- `data` `Buffer`: The content.

**Returns** `Promise<void>`: A promise that resolves once the file is written.

**Throws**

- `FILE_CONTENT_IS_EMPTY_OR_INVALID`: if the data isn't a Buffer

**Example**

```ts
import { writeBufferFile } from 'jg-fs';

const imageBuffer = Buffer.from('iVBORw0KGgo=', 'base64');
await writeBufferFile('avatar.png', imageBuffer);
```

##### <a id="api-writeFile"></a>`writeFile`

*Function*

Writes a file, creating its directory first when it doesn't exist.

```ts
writeFile(
  path: string,
  data: string | ArrayBufferView<ArrayBufferLike>,
  options?: WriteFileOptions,
): Promise<void>
```

**Parameters**

- `path` `string`: The file's path.
- `data` `string | ArrayBufferView<ArrayBufferLike>`: The content.
- `options?` `WriteFileOptions`: Node.js's writeFile options, such as the encoding or mode.

**Returns** `Promise<void>`: A promise that resolves once the file is written.

**Example**

```ts
import { writeFile } from 'jg-fs';

const csv = 'id,name\n1,Ada\n';
await writeFile('out/report.csv', csv, { encoding: 'utf-8' });
```

##### <a id="api-writeFileAtomic"></a>`writeFileAtomic`

*Function*

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.

```ts
writeFileAtomic(
  path: string,
  data: string | ArrayBufferView<ArrayBufferLike>,
  mode?: number,
): Promise<void>
```

**Parameters**

- `path` `string`: The file's path.
- `data` `string | ArrayBufferView<ArrayBufferLike>`: The content.
- `mode?` `number`: 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**

```ts
import { writeFileAtomic } from 'jg-fs';

const state = { step: 2 };
await writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file
await writeFileAtomic('secrets.env', 'TOKEN=abc\n', 0o600); // only the owner can read it
```

##### <a id="api-writeJsonFile"></a>`writeJsonFile`

*Function*

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.

```ts
writeJsonFile<T = Record<string, unknown>>(
  path: string,
  data: string | T,
  space?: number,
): Promise<void>
```

**Parameters**

- `path` `string`: The file's path.
- `data` `string | T`: The value, or its JSON text.
- `space?` `number`: The indentation for stringified values. Defaults to `2`.

**Returns** `Promise<void>`: A promise that resolves once the file is written.

**Throws**

- `FILE_CONTENT_IS_EMPTY_OR_INVALID`: if the JSON content cannot be stringified or is empty

**Example**

```ts
import { writeJsonFile } from 'jg-fs';

await writeJsonFile('config.json', { port: 8080 });
```

##### <a id="api-writePrivateFile"></a>`writePrivateFile`

*Function*

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.

```ts
writePrivateFile(
  path: string,
  data: string | ArrayBufferView<ArrayBufferLike>,
  options?: IWritePrivateFileOptions,
): Promise<void>
```

**Parameters**

- `path` `string`: The file's path.
- `data` `string | ArrayBufferView<ArrayBufferLike>`: The content.
- `options?` `IWritePrivateFileOptions`: The root the file must be inside, and whether to create it exclusively. Defaults to `{}`.

**Returns** `Promise<void>`: A promise that resolves once the file is written.

**Throws**

- `FILE_ALREADY_EXISTS`: if isExclusive is set and anything already exists at the path
- `NOT_A_FILE`: if the path's final component is a symbolic link, or it isn't a regular file
- `FILE_NOT_PRIVATE`: if other users can access the file at the path, or the file written
- `PATH_NOT_CANONICAL`: if the file's directory or the root goes through a symbolic link
- `PATH_OUTSIDE_ROOT`: if the file isn't strictly inside the root
- `NOT_A_DIRECTORY`: if the root or the file's directory doesn't exist or isn't a directory

**Example**

```ts
import { writePrivateFile } from 'jg-fs';

await writePrivateFile('/home/ada/.config/app/token.json', '{"token":"…"}');
```

##### <a id="api-writeTextFile"></a>`writeTextFile`

*Function*

Writes a UTF-8 text file. An empty string writes an empty file.

```ts
writeTextFile(path: string, data: string): Promise<void>
```

**Parameters**

- `path` `string`: The file's path.
- `data` `string`: The text.

**Returns** `Promise<void>`: A promise that resolves once the file is written.

**Throws**

- `FILE_CONTENT_IS_EMPTY_OR_INVALID`: if the data isn't a string

**Example**

```ts
import { writeTextFile } from 'jg-fs';

await writeTextFile('notes/today.md', '# Today');
```

#### <a id="api-constants"></a>Constants in `jg-fs`

##### <a id="api-ERRORS"></a>`ERRORS`

*Constant*

the codes of every error the package throws, from both the async and the sync entry

```ts
const ERRORS: { NOT_A_DIRECTORY: "NOT_A_DIRECTORY"; NON_EXISTENT_DIRECTORY: "NON_EXISTENT_DIRECTORY"; DIRECTORY_ALREADY_EXISTS: "DIRECTORY_ALREADY_EXISTS"; NON_EXISTENT_FILE: "NON_EXISTENT_FILE"; NOT_A_FILE: "NOT_A_FILE"; FILE_CONTENT_IS_EMPTY_OR_INVALID: "FILE_CONTENT_IS_EMPTY_OR_INVALID"; FILE_TOO_LARGE: "FILE_TOO_LARGE"; LOCK_BUSY: "LOCK_BUSY"; LOCK_NOT_OWNED: "LOCK_NOT_OWNED"; LOCK_RELEASE_FAILED: "LOCK_RELEASE_FAILED"; LOCK_ACQUISITION_FAILED: "LOCK_ACQUISITION_FAILED"; FILE_NOT_PRIVATE: "FILE_NOT_PRIVATE"; DIRECTORY_NOT_PRIVATE: "DIRECTORY_NOT_PRIVATE"; PATH_NOT_CANONICAL: "PATH_NOT_CANONICAL"; PATH_OUTSIDE_ROOT: "PATH_OUTSIDE_ROOT"; FILE_ALREADY_EXISTS: "FILE_ALREADY_EXISTS" }
```

#### <a id="api-types"></a>Types in `jg-fs`

##### <a id="api-IDirectoryElementsKeySort"></a>`IDirectoryElementsKeySort`

*Type*

the keys a directory's elements can be sorted by

```ts
type IDirectoryElementsKeySort = 'baseName' | 'size' | 'creation';
```

##### <a id="api-IDirectoryElementsOptions"></a>`IDirectoryElementsOptions`

*Type*

how getDirectoryElements filters and sorts a directory's elements

```ts
type IDirectoryElementsOptions = {
    sortByKey: IDirectoryElementsKeySort;
    sortOrder: ISortDirection;
    includeExts: string[];
};
```

**Members**

- `sortByKey: IDirectoryElementsKeySort` (property): the key the elements are sorted by. Defaults to 'baseName'
- `sortOrder: ISortDirection` (property): the sort direction. Defaults to 'asc'
- `includeExts: string[]` (property): the extensions of the files to include, such as ['.json'], matched case-insensitively. Defaults to all

##### <a id="api-IDirectoryPathElements"></a>`IDirectoryPathElements`

*Type*

a directory's elements, grouped by kind

```ts
type IDirectoryPathElements = {
    directories: IPathElement[];
    files: IPathElement[];
    symbolicLinks: IPathElement[];
};
```

**Members**

- `directories: IPathElement[]` (property)
- `files: IPathElement[]` (property)
- `symbolicLinks: IPathElement[]` (property)

##### <a id="api-IFileLock"></a>`IFileLock`

*Type*

a lock acquireLock holds until it's released

```ts
type IFileLock = {
    path: string;
    owner: IFileLockOwner;
    release: () => Promise<void>;
};
```

**Members**

- `path: string` (property): the lock's directory
- `owner: IFileLockOwner` (property): the process that holds it, as its owner.json names it
- `release: () => Promise<void>` (property): removes the lock; calling it again after it succeeded does nothing

##### <a id="api-IFileLockOwner"></a>`IFileLockOwner`

*Type*

the process that holds a lock, as its lock directory's owner.json names it

```ts
type IFileLockOwner = {
    pid: number;
    nonce: string;
    hostname: string;
};
```

**Members**

- `pid: number` (property): the process id
- `nonce: string` (property): a random id that tells this acquisition apart from any other by the same process id
- `hostname: string` (property): the host the process runs on; a lock is never recovered from another host

##### <a id="api-IPathElement"></a>`IPathElement`

*Type*

the details of a path, read with lstat, so a symbolic link is described rather than followed

```ts
type IPathElement = {
    path: string;
    baseName: string;
    extName: string;
    isFile: boolean;
    isDirectory: boolean;
    isSymbolicLink: boolean;
    size: number;
    creation: number;
};
```

**Members**

- `path: string` (property): the path, as given
- `baseName: string` (property): the last segment of the path
- `extName: string` (property): the extension, such as '.json', or an empty string when there is none
- `isFile: boolean` (property): whether it's a regular file
- `isDirectory: boolean` (property): whether it's a directory
- `isSymbolicLink: boolean` (property): whether it's a symbolic link, in which case isFile and isDirectory are false
- `size: number` (property): the size in bytes
- `creation: number` (property): when it was created, in milliseconds since the epoch

##### <a id="api-IPrivateFileOptions"></a>`IPrivateFileOptions`

*Type*

the options assertPrivateFile takes

```ts
type IPrivateFileOptions = {
    rootPath?: string | undefined;
    maxBytes?: number | undefined;
};
```

**Members**

- `rootPath: string` (property): a directory the file must be strictly inside; it must exist and contain no symbolic link
- `maxBytes: number` (property): the largest size accepted, in bytes

##### <a id="api-IReadJsonFileOptions"></a>`IReadJsonFileOptions`

*Type*

what readJsonFile accepts

```ts
type IReadJsonFileOptions<T> = {
    schema?: ISchema<T>;
    maxBytes?: number;
};
```

**Members**

- `schema: ISchema<T>` (property): validates the parsed content, such as a Zod schema. Its errors propagate unchanged
- `maxBytes: number` (property): 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

##### <a id="api-IReadPrivateFileOptions"></a>`IReadPrivateFileOptions`

*Type*

the options readPrivateFile takes

```ts
type IReadPrivateFileOptions = {
    maxBytes: number;
    rootPath?: string | undefined;
};
```

**Members**

- `maxBytes: number` (property): the largest size accepted, in bytes; it's required, because the file's size isn't the reader's
- `rootPath: string` (property): a directory the file must be strictly inside; it must exist and contain no symbolic link

##### <a id="api-ISchema"></a>`ISchema`

*Type*

validates parsed data and returns it typed, which a Zod schema does

```ts
type ISchema<T> = {
    parse(value: unknown): T;
};
```

**Members**

- `parse(value: unknown): T` (method)

##### <a id="api-IWritePrivateFileOptions"></a>`IWritePrivateFileOptions`

*Type*

the options writePrivateFile takes

```ts
type IWritePrivateFileOptions = {
    rootPath?: string | undefined;
    isExclusive?: boolean | undefined;
};
```

**Members**

- `rootPath: string` (property): a directory the file must be strictly inside; it must exist and contain no symbolic link
- `isExclusive: boolean` (property): creates the file, and fails when anything already exists at the path, instead of replacing it

### <a id="api-entry-sync"></a>`jg-fs/sync`

- Functions: [`acquireLock`](#api-sync-acquireLock), [`assertPrivateDirectory`](#api-sync-assertPrivateDirectory), [`assertPrivateFile`](#api-sync-assertPrivateFile), [`copyDirectory`](#api-sync-copyDirectory), [`copyFile`](#api-sync-copyFile), [`createDirectory`](#api-sync-createDirectory), [`createDirectorySymLink`](#api-sync-createDirectorySymLink), [`createFileSymLink`](#api-sync-createFileSymLink), [`createPrivateDirectory`](#api-sync-createPrivateDirectory), [`deleteDirectory`](#api-sync-deleteDirectory), [`deleteFile`](#api-sync-deleteFile), [`getDirectoryElements`](#api-sync-getDirectoryElements), [`getPathElement`](#api-sync-getPathElement), [`isDirectory`](#api-sync-isDirectory), [`isFile`](#api-sync-isFile), [`isPathWithin`](#api-sync-isPathWithin), [`pathExists`](#api-sync-pathExists), [`readBoundedFile`](#api-sync-readBoundedFile), [`readBufferFile`](#api-sync-readBufferFile), [`readDirectory`](#api-sync-readDirectory), [`readFile`](#api-sync-readFile), [`readJsonFile`](#api-sync-readJsonFile), [`readPrivateFile`](#api-sync-readPrivateFile), [`readTextFile`](#api-sync-readTextFile), [`writeBufferFile`](#api-sync-writeBufferFile), [`writeFile`](#api-sync-writeFile), [`writeFileAtomic`](#api-sync-writeFileAtomic), [`writeJsonFile`](#api-sync-writeJsonFile), [`writePrivateFile`](#api-sync-writePrivateFile), [`writeTextFile`](#api-sync-writeTextFile)
- Constants: [`ERRORS`](#api-sync-ERRORS)
- Types: [`IDirectoryElementsKeySort`](#api-sync-IDirectoryElementsKeySort), [`IDirectoryElementsOptions`](#api-sync-IDirectoryElementsOptions), [`IDirectoryPathElements`](#api-sync-IDirectoryPathElements), [`IFileLock`](#api-sync-IFileLock), [`IFileLockOwner`](#api-sync-IFileLockOwner), [`IPathElement`](#api-sync-IPathElement), [`IPrivateFileOptions`](#api-sync-IPrivateFileOptions), [`IReadJsonFileOptions`](#api-sync-IReadJsonFileOptions), [`IReadPrivateFileOptions`](#api-sync-IReadPrivateFileOptions), [`ISchema`](#api-sync-ISchema), [`IWritePrivateFileOptions`](#api-sync-IWritePrivateFileOptions)

#### <a id="api-sync-functions"></a>Functions in `jg-fs/sync`

##### <a id="api-sync-acquireLock"></a>`acquireLock`

*Function*

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.

```ts
acquireLock(path: string): IFileLock
```

**Parameters**

- `path` `string`: The lock's directory, whose parent must exist.

**Returns** `IFileLock`: The lock, released with `release()`.

**Throws**

- `LOCK_BUSY`: The lock '<path>' is busy.
- `NOT_A_DIRECTORY`: The path '<path>' is not a directory.
- `LOCK_ACQUISITION_FAILED`: The lock '<path>' could not be acquired.

**Example**

```ts
import { acquireLock, writeFileAtomic } from 'jg-fs/sync';

const lock = acquireLock('.locks/nightly-report');
try {
  writeFileAtomic('reports/nightly.json', JSON.stringify({ generatedAt: Date.now() }));
} finally {
  lock.release();
}
```

##### <a id="api-sync-assertPrivateDirectory"></a>`assertPrivateDirectory`

*Function*

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.

```ts
assertPrivateDirectory(path: string): void
```

**Parameters**

- `path` `string`: The directory's path.

**Throws**

- `NOT_A_DIRECTORY`: if the path doesn't exist, is a symbolic link, or isn't a directory
- `PATH_NOT_CANONICAL`: if the path goes through a symbolic link
- `DIRECTORY_NOT_PRIVATE`: if other users can access the directory

**Example**

```ts
import { assertPrivateDirectory } from 'jg-fs/sync';

assertPrivateDirectory('/home/ada/.config/app');
```

##### <a id="api-sync-assertPrivateFile"></a>`assertPrivateFile`

*Function*

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.

```ts
assertPrivateFile(path: string, options?: IPrivateFileOptions): void
```

**Parameters**

- `path` `string`: The file's path.
- `options?` `IPrivateFileOptions`: The root it must be inside, and the largest size accepted, in bytes. Defaults to `{}`.

**Throws**

- `NON_EXISTENT_FILE`: if the file doesn't exist
- `NOT_A_FILE`: if the path's final component is a symbolic link, or it isn't a regular file
- `FILE_NOT_PRIVATE`: if other users can access the file
- `FILE_TOO_LARGE`: if the file holds more than maxBytes bytes
- `PATH_NOT_CANONICAL`: if the path or the root goes through a symbolic link
- `PATH_OUTSIDE_ROOT`: if the file isn't strictly inside the root
- `NOT_A_DIRECTORY`: if the root doesn't exist or isn't a directory

**Example**

```ts
import { assertPrivateFile } from 'jg-fs/sync';

assertPrivateFile('/home/ada/.config/app/token.json', { rootPath: '/home/ada/.config/app' });
```

##### <a id="api-sync-copyDirectory"></a>`copyDirectory`

*Function*

Copies a directory and everything in it, replacing the destination completely.

```ts
copyDirectory(srcPath: string, destPath: string): void
```

**Parameters**

- `srcPath` `string`: The directory to copy.
- `destPath` `string`: Where to copy it.

**Throws**

- `NOT_A_DIRECTORY`: if the srcPath is not a directory

**Example**

```ts
import { copyDirectory } from 'jg-fs/sync';

copyDirectory('templates', 'dist/templates');
```

##### <a id="api-sync-copyFile"></a>`copyFile`

*Function*

Copies a file, replacing the destination when it exists.

```ts
copyFile(srcPath: string, destPath: string): void
```

**Parameters**

- `srcPath` `string`: The file to copy.
- `destPath` `string`: Where to copy it.

**Throws**

- `NOT_A_FILE`: if the srcPath doesn't exist or is not a file

**Example**

```ts
import { copyFile } from 'jg-fs/sync';

copyFile('config.example.json', 'config.json');
```

##### <a id="api-sync-createDirectory"></a>`createDirectory`

*Function*

Creates a directory, with any missing parents. An existing directory is kept, or replaced with an empty one when `deleteIfExists` is set.

```ts
createDirectory(path: string, deleteIfExists?: boolean): void
```

**Parameters**

- `path` `string`: The directory's path.
- `deleteIfExists?` `boolean`: Whether to delete an existing directory and its contents first.

**Throws**

- `DIRECTORY_ALREADY_EXISTS`: if something other than a directory exists at the path

**Example**

```ts
import { createDirectory } from 'jg-fs/sync';

createDirectory('dist/assets'); // creates missing parents too
createDirectory('dist', true); // replaces it with an empty directory
```

##### <a id="api-sync-createDirectorySymLink"></a>`createDirectorySymLink`

*Function*

Creates a symbolic link to a directory.

```ts
createDirectorySymLink(target: string, path: string): void
```

**Parameters**

- `target` `string`: The directory the link points to.
- `path` `string`: Where to create the link.

**Throws**

- `NON_EXISTENT_DIRECTORY`: if the target directory doesn't exist
- `NOT_A_DIRECTORY`: if the target directory is not considered a directory by the OS

**Example**

```ts
import { createDirectorySymLink } from 'jg-fs/sync';

createDirectorySymLink('releases/1.4.0', 'current');
```

##### <a id="api-sync-createFileSymLink"></a>`createFileSymLink`

*Function*

Creates a symbolic link to a file.

```ts
createFileSymLink(target: string, path: string): void
```

**Parameters**

- `target` `string`: The file the link points to.
- `path` `string`: Where to create the link.

**Throws**

- `NON_EXISTENT_FILE`: if the target file does not exist
- `NOT_A_FILE`: if the target is not a file

**Example**

```ts
import { createFileSymLink } from 'jg-fs/sync';

createFileSymLink('config.production.json', 'config.json');
```

##### <a id="api-sync-createPrivateDirectory"></a>`createPrivateDirectory`

*Function*

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.

```ts
createPrivateDirectory(path: string): void
```

**Parameters**

- `path` `string`: The directory's path.

**Throws**

- `NOT_A_DIRECTORY`: if the path exists and isn't a directory
- `PATH_NOT_CANONICAL`: if the path goes through a symbolic link
- `DIRECTORY_NOT_PRIVATE`: if other users can access the directory

**Example**

```ts
import { createPrivateDirectory } from 'jg-fs/sync';

createPrivateDirectory('/home/ada/.config/app/sessions');
```

##### <a id="api-sync-deleteDirectory"></a>`deleteDirectory`

*Function*

Deletes a directory and everything in it. A missing directory is not an error.

```ts
deleteDirectory(path: string): void
```

**Parameters**

- `path` `string`: The directory's path.

**Example**

```ts
import { deleteDirectory } from 'jg-fs/sync';

deleteDirectory('dist');
```

##### <a id="api-sync-deleteFile"></a>`deleteFile`

*Function*

Deletes a file.

```ts
deleteFile(path: string): void
```

**Parameters**

- `path` `string`: The file's path.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

**Example**

```ts
import { deleteFile } from 'jg-fs/sync';

deleteFile('cache.json');
```

##### <a id="api-sync-getDirectoryElements"></a>`getDirectoryElements`

*Function*

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.

```ts
getDirectoryElements(
  path: string,
  options?: Partial<IDirectoryElementsOptions>,
): IDirectoryPathElements
```

**Parameters**

- `path` `string`: The directory's path.
- `options?` `Partial<IDirectoryElementsOptions>`: The extensions to include and how to sort.

**Returns** `IDirectoryPathElements`: The grouped elements.

**Throws**

- `NOT_A_DIRECTORY`: if the path doesn't exist or is not a directory

**Example**

```ts
import { getDirectoryElements } from 'jg-fs/sync';

const { files } = getDirectoryElements('content', { includeExts: ['.md'], sortByKey: 'creation', sortOrder: 'desc' });
```

##### <a id="api-sync-getPathElement"></a>`getPathElement`

*Function*

Reads a path's details without following a symbolic link at the path.

```ts
getPathElement(path: string): IPathElement | null
```

**Parameters**

- `path` `string`: The path.

**Returns** `IPathElement | null`: The path's details, or `null` when it doesn't exist.

**Example**

```ts
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
```

##### <a id="api-sync-isDirectory"></a>`isDirectory`

*Function*

Checks whether a path exists and is a directory.

```ts
isDirectory(path: string): boolean
```

**Parameters**

- `path` `string`: The path.

**Returns** `boolean`: Whether the path is a directory.

**Example**

```ts
import { isDirectory } from 'jg-fs/sync';

isDirectory('dist'); // true
```

##### <a id="api-sync-isFile"></a>`isFile`

*Function*

Checks whether a path exists and is a regular file.

```ts
isFile(path: string): boolean
```

**Parameters**

- `path` `string`: The path.

**Returns** `boolean`: Whether the path is a file.

**Example**

```ts
import { isFile } from 'jg-fs/sync';

isFile('package.json'); // true
```

##### <a id="api-sync-isPathWithin"></a>`isPathWithin`

*Function*

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.

```ts
isPathWithin(rootPath: string, targetPath: string): boolean
```

**Parameters**

- `rootPath` `string`: The root path.
- `targetPath` `string`: The path to check.

**Returns** `boolean`: Whether the target is the root or inside it.

**Example**

```ts
import { isPathWithin } from 'jg-fs';

isPathWithin('/data/app', '/data/app/logs/today.log'); // true
isPathWithin('/data/app', '/data/app/../secrets'); // false
isPathWithin('/data/app', '/data/application'); // false
```

##### <a id="api-sync-pathExists"></a>`pathExists`

*Function*

Checks whether a path exists, whether it's a file, a directory, or anything else.

```ts
pathExists(path: string): boolean
```

**Parameters**

- `path` `string`: The path.

**Returns** `boolean`: Whether the path exists.

**Example**

```ts
import { pathExists } from 'jg-fs/sync';

pathExists('config.json'); // true when the file exists
```

##### <a id="api-sync-readBoundedFile"></a>`readBoundedFile`

*Function*

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.

```ts
readBoundedFile(path: string, maxBytes: number): Buffer
```

**Parameters**

- `path` `string`: The file's path.
- `maxBytes` `number`: The largest size accepted, in bytes.

**Returns** `Buffer`: The content.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist, its final component is a symbolic link, or it isn't a regular file
- `FILE_TOO_LARGE`: if the file holds more than maxBytes bytes

**Example**

```ts
import { join } from 'node:path';
import { readBoundedFile } from 'jg-fs/sync';

const upload = readBoundedFile(join('uploads', 'avatar.png'), 5 * 1024 * 1024); // at most 5 MB
```

##### <a id="api-sync-readBufferFile"></a>`readBufferFile`

*Function*

Reads a binary file. An empty file reads as an empty Buffer.

```ts
readBufferFile(path: string): Buffer
```

**Parameters**

- `path` `string`: The file's path.

**Returns** `Buffer`: The content.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

**Example**

```ts
import { readBufferFile } from 'jg-fs/sync';

const image = readBufferFile('avatar.png');
```

##### <a id="api-sync-readDirectory"></a>`readDirectory`

*Function*

Lists a directory's contents as paths joined to the directory's path.

```ts
readDirectory(path: string, recursive?: boolean): string[]
```

**Parameters**

- `path` `string`: The directory's path.
- `recursive?` `boolean`: Whether to include the contents of subdirectories. Defaults to `false`.

**Returns** `string[]`: The content paths.

**Throws**

- `NOT_A_DIRECTORY`: if the path doesn't exist or is not a directory

**Example**

```ts
import { readDirectory } from 'jg-fs/sync';

readDirectory('dist'); // ['dist/index.js', 'dist/assets']
readDirectory('dist', true); // includes the contents of dist/assets
```

##### <a id="api-sync-readFile"></a>`readFile`

*Function*

Reads a file as a string when an encoding is given, or as a Buffer otherwise.

```ts
readFile(path: string, options: IReadStringFileOptions): string
```

**Parameters**

- `path` `string`: The file's path.
- `options` `IReadStringFileOptions`: Node.js's readFile options, such as the encoding.

**Returns** `string`: The content.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

```ts
readFile(path: string, options?: IReadBufferFileOptions): Buffer
```

**Parameters**

- `path` `string`: The file's path.
- `options?` `IReadBufferFileOptions`: Node.js's readFile options, such as the encoding.

**Returns** `Buffer`: The content.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

```ts
readFile(
  path: string,
  options?: IReadFileOptions,
): string | Buffer<ArrayBufferLike>
```

**Parameters**

- `path` `string`: The file's path.
- `options?` `IReadFileOptions`: Node.js's readFile options, such as the encoding.

**Returns** `string | Buffer<ArrayBufferLike>`: The content.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

**Example**

```ts
import { readFile } from 'jg-fs/sync';

readFile('notes.md', 'utf-8'); // a string
readFile('avatar.png'); // a Buffer
```

##### <a id="api-sync-readJsonFile"></a>`readJsonFile`

*Function*

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.

```ts
readJsonFile<T = unknown>(path: string, options?: IReadJsonFileOptions<T>): T
```

**Parameters**

- `path` `string`: 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_FILE`: if the path doesn't exist or is not a file
- `FILE_TOO_LARGE`: if the file holds more than maxBytes bytes
- `FILE_CONTENT_IS_EMPTY_OR_INVALID`: if the file's JSON content cannot be parsed

**Example 1**

```ts
import { readJsonFile } from 'jg-fs/sync';

const config = readJsonFile('config.json', { schema: Config }); // Config's type
```

**Example 2**

```ts
import { readJsonFile } from 'jg-fs/sync';

const value = readJsonFile('data.json'); // unknown
```

##### <a id="api-sync-readPrivateFile"></a>`readPrivateFile`

*Function*

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.

```ts
readPrivateFile(path: string, options: IReadPrivateFileOptions): Buffer
```

**Parameters**

- `path` `string`: The file's path.
- `options` `IReadPrivateFileOptions`: The largest size accepted, in bytes, and the root the file must be inside.

**Returns** `Buffer`: The content.

**Throws**

- `NON_EXISTENT_FILE`: if the file doesn't exist
- `NOT_A_FILE`: if the path's final component is a symbolic link, or it isn't a regular file
- `FILE_NOT_PRIVATE`: if other users can access the file
- `FILE_TOO_LARGE`: if the file holds more than maxBytes bytes
- `PATH_NOT_CANONICAL`: if the path or the root goes through a symbolic link
- `PATH_OUTSIDE_ROOT`: if the file isn't strictly inside the root
- `NOT_A_DIRECTORY`: if the root doesn't exist or isn't a directory

**Example**

```ts
import { readPrivateFile } from 'jg-fs/sync';

const key = readPrivateFile('/home/ada/.ssh/id_ed25519', { maxBytes: 16 * 1024 });
```

##### <a id="api-sync-readTextFile"></a>`readTextFile`

*Function*

Reads a UTF-8 text file. An empty file reads as an empty string.

```ts
readTextFile(path: string): string
```

**Parameters**

- `path` `string`: The file's path.

**Returns** `string`: The text.

**Throws**

- `NOT_A_FILE`: if the path doesn't exist or is not a file

**Example**

```ts
import { readTextFile } from 'jg-fs/sync';

const markdown = readTextFile('README.md');
```

##### <a id="api-sync-writeBufferFile"></a>`writeBufferFile`

*Function*

Writes a binary file. An empty Buffer writes an empty file.

```ts
writeBufferFile(path: string, data: Buffer): void
```

**Parameters**

- `path` `string`: The file's path.
- `data` `Buffer`: The content.

**Throws**

- `FILE_CONTENT_IS_EMPTY_OR_INVALID`: if the data isn't a Buffer

**Example**

```ts
import { writeBufferFile } from 'jg-fs/sync';

const imageBuffer = Buffer.from('iVBORw0KGgo=', 'base64');
writeBufferFile('avatar.png', imageBuffer);
```

##### <a id="api-sync-writeFile"></a>`writeFile`

*Function*

Writes a file, creating its directory first when it doesn't exist.

```ts
writeFile(
  path: string,
  data: string | ArrayBufferView<ArrayBufferLike>,
  options?: WriteFileOptions,
): void
```

**Parameters**

- `path` `string`: The file's path.
- `data` `string | ArrayBufferView<ArrayBufferLike>`: The content.
- `options?` `WriteFileOptions`: Node.js's writeFile options, such as the encoding or mode.

**Example**

```ts
import { writeFile } from 'jg-fs/sync';

const csv = 'id,name\n1,Ada\n';
writeFile('out/report.csv', csv, { encoding: 'utf-8' });
```

##### <a id="api-sync-writeFileAtomic"></a>`writeFileAtomic`

*Function*

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.

```ts
writeFileAtomic(
  path: string,
  data: string | ArrayBufferView<ArrayBufferLike>,
  mode?: number,
): void
```

**Parameters**

- `path` `string`: The file's path.
- `data` `string | ArrayBufferView<ArrayBufferLike>`: The content.
- `mode?` `number`: The permission bits of the written file, such as `0o600`.

**Example**

```ts
import { writeFileAtomic } from 'jg-fs/sync';

const state = { step: 2 };
writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file
writeFileAtomic('secrets.env', 'TOKEN=abc\n', 0o600); // only the owner can read it
```

##### <a id="api-sync-writeJsonFile"></a>`writeJsonFile`

*Function*

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.

```ts
writeJsonFile<T = Record<string, unknown>>(
  path: string,
  data: string | T,
  space?: number,
): void
```

**Parameters**

- `path` `string`: The file's path.
- `data` `string | T`: The value, or its JSON text.
- `space?` `number`: The indentation for stringified values. Defaults to `2`.

**Throws**

- `FILE_CONTENT_IS_EMPTY_OR_INVALID`: if the JSON content cannot be stringified or is empty

**Example**

```ts
import { writeJsonFile } from 'jg-fs/sync';

writeJsonFile('config.json', { port: 8080 });
```

##### <a id="api-sync-writePrivateFile"></a>`writePrivateFile`

*Function*

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.

```ts
writePrivateFile(
  path: string,
  data: string | ArrayBufferView<ArrayBufferLike>,
  options?: IWritePrivateFileOptions,
): void
```

**Parameters**

- `path` `string`: The file's path.
- `data` `string | ArrayBufferView<ArrayBufferLike>`: The content.
- `options?` `IWritePrivateFileOptions`: The root the file must be inside, and whether to create it exclusively. Defaults to `{}`.

**Throws**

- `FILE_ALREADY_EXISTS`: if isExclusive is set and anything already exists at the path
- `NOT_A_FILE`: if the path's final component is a symbolic link, or it isn't a regular file
- `FILE_NOT_PRIVATE`: if other users can access the file at the path, or the file written
- `PATH_NOT_CANONICAL`: if the file's directory or the root goes through a symbolic link
- `PATH_OUTSIDE_ROOT`: if the file isn't strictly inside the root
- `NOT_A_DIRECTORY`: if the root or the file's directory doesn't exist or isn't a directory

**Example**

```ts
import { writePrivateFile } from 'jg-fs/sync';

writePrivateFile('/home/ada/.config/app/token.json', '{"token":"…"}');
```

##### <a id="api-sync-writeTextFile"></a>`writeTextFile`

*Function*

Writes a UTF-8 text file. An empty string writes an empty file.

```ts
writeTextFile(path: string, data: string): void
```

**Parameters**

- `path` `string`: The file's path.
- `data` `string`: The text.

**Throws**

- `FILE_CONTENT_IS_EMPTY_OR_INVALID`: if the data isn't a string

**Example**

```ts
import { writeTextFile } from 'jg-fs/sync';

writeTextFile('notes/today.md', '# Today');
```

#### <a id="api-sync-constants"></a>Constants in `jg-fs/sync`

##### <a id="api-sync-ERRORS"></a>`ERRORS`

*Constant*

the codes of every error the package throws, from both the async and the sync entry

```ts
const ERRORS: { NOT_A_DIRECTORY: "NOT_A_DIRECTORY"; NON_EXISTENT_DIRECTORY: "NON_EXISTENT_DIRECTORY"; DIRECTORY_ALREADY_EXISTS: "DIRECTORY_ALREADY_EXISTS"; NON_EXISTENT_FILE: "NON_EXISTENT_FILE"; NOT_A_FILE: "NOT_A_FILE"; FILE_CONTENT_IS_EMPTY_OR_INVALID: "FILE_CONTENT_IS_EMPTY_OR_INVALID"; FILE_TOO_LARGE: "FILE_TOO_LARGE"; LOCK_BUSY: "LOCK_BUSY"; LOCK_NOT_OWNED: "LOCK_NOT_OWNED"; LOCK_RELEASE_FAILED: "LOCK_RELEASE_FAILED"; LOCK_ACQUISITION_FAILED: "LOCK_ACQUISITION_FAILED"; FILE_NOT_PRIVATE: "FILE_NOT_PRIVATE"; DIRECTORY_NOT_PRIVATE: "DIRECTORY_NOT_PRIVATE"; PATH_NOT_CANONICAL: "PATH_NOT_CANONICAL"; PATH_OUTSIDE_ROOT: "PATH_OUTSIDE_ROOT"; FILE_ALREADY_EXISTS: "FILE_ALREADY_EXISTS" }
```

#### <a id="api-sync-types"></a>Types in `jg-fs/sync`

##### <a id="api-sync-IDirectoryElementsKeySort"></a>`IDirectoryElementsKeySort`

*Type*

the keys a directory's elements can be sorted by

```ts
type IDirectoryElementsKeySort = 'baseName' | 'size' | 'creation';
```

##### <a id="api-sync-IDirectoryElementsOptions"></a>`IDirectoryElementsOptions`

*Type*

how getDirectoryElements filters and sorts a directory's elements

```ts
type IDirectoryElementsOptions = {
    sortByKey: IDirectoryElementsKeySort;
    sortOrder: ISortDirection;
    includeExts: string[];
};
```

**Members**

- `sortByKey: IDirectoryElementsKeySort` (property): the key the elements are sorted by. Defaults to 'baseName'
- `sortOrder: ISortDirection` (property): the sort direction. Defaults to 'asc'
- `includeExts: string[]` (property): the extensions of the files to include, such as ['.json'], matched case-insensitively. Defaults to all

##### <a id="api-sync-IDirectoryPathElements"></a>`IDirectoryPathElements`

*Type*

a directory's elements, grouped by kind

```ts
type IDirectoryPathElements = {
    directories: IPathElement[];
    files: IPathElement[];
    symbolicLinks: IPathElement[];
};
```

**Members**

- `directories: IPathElement[]` (property)
- `files: IPathElement[]` (property)
- `symbolicLinks: IPathElement[]` (property)

##### <a id="api-sync-IFileLock"></a>`IFileLock`

*Type*

a lock acquireLock holds until it's released

```ts
type IFileLock = {
    path: string;
    owner: IFileLockOwner;
    release: () => void;
};
```

**Members**

- `path: string` (property): the lock's directory
- `owner: IFileLockOwner` (property): the process that holds it, as its owner.json names it
- `release: () => void` (property): removes the lock; calling it again after it succeeded does nothing

##### <a id="api-sync-IFileLockOwner"></a>`IFileLockOwner`

*Type*

the process that holds a lock, as its lock directory's owner.json names it

```ts
type IFileLockOwner = {
    pid: number;
    nonce: string;
    hostname: string;
};
```

**Members**

- `pid: number` (property): the process id
- `nonce: string` (property): a random id that tells this acquisition apart from any other by the same process id
- `hostname: string` (property): the host the process runs on; a lock is never recovered from another host

##### <a id="api-sync-IPathElement"></a>`IPathElement`

*Type*

the details of a path, read with lstat, so a symbolic link is described rather than followed

```ts
type IPathElement = {
    path: string;
    baseName: string;
    extName: string;
    isFile: boolean;
    isDirectory: boolean;
    isSymbolicLink: boolean;
    size: number;
    creation: number;
};
```

**Members**

- `path: string` (property): the path, as given
- `baseName: string` (property): the last segment of the path
- `extName: string` (property): the extension, such as '.json', or an empty string when there is none
- `isFile: boolean` (property): whether it's a regular file
- `isDirectory: boolean` (property): whether it's a directory
- `isSymbolicLink: boolean` (property): whether it's a symbolic link, in which case isFile and isDirectory are false
- `size: number` (property): the size in bytes
- `creation: number` (property): when it was created, in milliseconds since the epoch

##### <a id="api-sync-IPrivateFileOptions"></a>`IPrivateFileOptions`

*Type*

the options assertPrivateFile takes

```ts
type IPrivateFileOptions = {
    rootPath?: string | undefined;
    maxBytes?: number | undefined;
};
```

**Members**

- `rootPath: string` (property): a directory the file must be strictly inside; it must exist and contain no symbolic link
- `maxBytes: number` (property): the largest size accepted, in bytes

##### <a id="api-sync-IReadJsonFileOptions"></a>`IReadJsonFileOptions`

*Type*

what readJsonFile accepts

```ts
type IReadJsonFileOptions<T> = {
    schema?: ISchema<T>;
    maxBytes?: number;
};
```

**Members**

- `schema: ISchema<T>` (property): validates the parsed content, such as a Zod schema. Its errors propagate unchanged
- `maxBytes: number` (property): 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

##### <a id="api-sync-IReadPrivateFileOptions"></a>`IReadPrivateFileOptions`

*Type*

the options readPrivateFile takes

```ts
type IReadPrivateFileOptions = {
    maxBytes: number;
    rootPath?: string | undefined;
};
```

**Members**

- `maxBytes: number` (property): the largest size accepted, in bytes; it's required, because the file's size isn't the reader's
- `rootPath: string` (property): a directory the file must be strictly inside; it must exist and contain no symbolic link

##### <a id="api-sync-ISchema"></a>`ISchema`

*Type*

validates parsed data and returns it typed, which a Zod schema does

```ts
type ISchema<T> = {
    parse(value: unknown): T;
};
```

**Members**

- `parse(value: unknown): T` (method)

##### <a id="api-sync-IWritePrivateFileOptions"></a>`IWritePrivateFileOptions`

*Type*

the options writePrivateFile takes

```ts
type IWritePrivateFileOptions = {
    rootPath?: string | undefined;
    isExclusive?: boolean | undefined;
};
```

**Members**

- `rootPath: string` (property): a directory the file must be strictly inside; it must exist and contain no symbolic link
- `isExclusive: boolean` (property): creates the file, and fails when anything already exists at the path, instead of replacing it
