jg-fs 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +15 -9
- package/CHANGELOG.md +20 -0
- package/README.md +6 -2
- package/RECIPES.md +125 -0
- package/dist/async/files/files.d.ts +11 -4
- package/dist/async/files/files.js +19 -9
- package/dist/async/files/files.js.map +1 -1
- package/dist/async/files/utilities.d.ts +15 -0
- package/dist/async/files/utilities.js +43 -2
- package/dist/async/files/utilities.js.map +1 -1
- package/dist/shared/constants.d.ts +1 -0
- package/dist/shared/constants.js +5 -0
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js +2 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/types.d.ts +1 -0
- package/dist/shared/types.js.map +1 -1
- package/dist/shared/validations.d.ts +8 -0
- package/dist/shared/validations.js +11 -0
- package/dist/shared/validations.js.map +1 -1
- package/dist/sync/files/files.d.ts +11 -4
- package/dist/sync/files/files.js +19 -9
- package/dist/sync/files/files.js.map +1 -1
- package/dist/sync/files/utilities.d.ts +15 -0
- package/dist/sync/files/utilities.js +41 -2
- package/dist/sync/files/utilities.js.map +1 -1
- package/package.json +5 -4
package/API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# <a id="api-reference"></a>jg-fs API reference
|
|
2
2
|
|
|
3
|
-
Every export of `jg-fs@1.
|
|
3
|
+
Every export of `jg-fs@1.2.0`, 62 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.
|
|
4
4
|
|
|
5
5
|
## <a id="api-entry-main"></a>`jg-fs`
|
|
6
6
|
|
|
@@ -528,7 +528,7 @@ await readFile('avatar.png'); // a Buffer
|
|
|
528
528
|
|
|
529
529
|
*Function*
|
|
530
530
|
|
|
531
|
-
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.
|
|
531
|
+
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.
|
|
532
532
|
|
|
533
533
|
```ts
|
|
534
534
|
readJsonFile<T = unknown>(
|
|
@@ -540,13 +540,14 @@ readJsonFile<T = unknown>(
|
|
|
540
540
|
**Parameters**
|
|
541
541
|
|
|
542
542
|
- `path` `string`: The file's path.
|
|
543
|
-
- `options?` `IReadJsonFileOptions<T>`: The schema, such as a Zod schema, whose errors propagate unchanged. Defaults to `{}`.
|
|
543
|
+
- `options?` `IReadJsonFileOptions<T>`: The schema, such as a Zod schema, whose errors propagate unchanged, and the largest file accepted, in bytes. Defaults to `{}`.
|
|
544
544
|
|
|
545
545
|
**Returns** `Promise<T>`: A promise that resolves to the parsed value.
|
|
546
546
|
|
|
547
547
|
**Throws**
|
|
548
548
|
|
|
549
549
|
- `NOT_A_FILE`: if the path doesn't exist or is not a file
|
|
550
|
+
- `FILE_TOO_LARGE`: if the file holds more than maxBytes bytes
|
|
550
551
|
- `FILE_CONTENT_IS_EMPTY_OR_INVALID`: if the file's JSON content cannot be parsed
|
|
551
552
|
|
|
552
553
|
**Example 1**
|
|
@@ -658,7 +659,7 @@ await writeFile('out/report.csv', csv, { encoding: 'utf-8' });
|
|
|
658
659
|
|
|
659
660
|
*Function*
|
|
660
661
|
|
|
661
|
-
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.
|
|
662
|
+
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.
|
|
662
663
|
|
|
663
664
|
```ts
|
|
664
665
|
writeFileAtomic(
|
|
@@ -690,7 +691,7 @@ await writeFileAtomic('secrets.env', 'TOKEN=abc\n', 0o600); // only the owner ca
|
|
|
690
691
|
|
|
691
692
|
*Function*
|
|
692
693
|
|
|
693
|
-
Writes a JSON file. A string is written as given; anything else is stringified.
|
|
694
|
+
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.
|
|
694
695
|
|
|
695
696
|
```ts
|
|
696
697
|
writeJsonFile<T = Record<string, unknown>>(
|
|
@@ -852,12 +853,14 @@ what readJsonFile accepts
|
|
|
852
853
|
```ts
|
|
853
854
|
type IReadJsonFileOptions<T> = {
|
|
854
855
|
schema?: ISchema<T>;
|
|
856
|
+
maxBytes?: number;
|
|
855
857
|
};
|
|
856
858
|
```
|
|
857
859
|
|
|
858
860
|
**Members**
|
|
859
861
|
|
|
860
862
|
- `schema: ISchema<T>` (property): validates the parsed content, such as a Zod schema. Its errors propagate unchanged
|
|
863
|
+
- `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
|
|
861
864
|
|
|
862
865
|
#### <a id="api-ISchema"></a>`ISchema`
|
|
863
866
|
|
|
@@ -1381,7 +1384,7 @@ readFile('avatar.png'); // a Buffer
|
|
|
1381
1384
|
|
|
1382
1385
|
*Function*
|
|
1383
1386
|
|
|
1384
|
-
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.
|
|
1387
|
+
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.
|
|
1385
1388
|
|
|
1386
1389
|
```ts
|
|
1387
1390
|
readJsonFile<T = unknown>(path: string, options?: IReadJsonFileOptions<T>): T
|
|
@@ -1390,13 +1393,14 @@ readJsonFile<T = unknown>(path: string, options?: IReadJsonFileOptions<T>): T
|
|
|
1390
1393
|
**Parameters**
|
|
1391
1394
|
|
|
1392
1395
|
- `path` `string`: The file's path.
|
|
1393
|
-
- `options?` `IReadJsonFileOptions<T>`: The schema, such as a Zod schema, whose errors propagate unchanged. Defaults to `{}`.
|
|
1396
|
+
- `options?` `IReadJsonFileOptions<T>`: The schema, such as a Zod schema, whose errors propagate unchanged, and the largest file accepted, in bytes. Defaults to `{}`.
|
|
1394
1397
|
|
|
1395
1398
|
**Returns** `T`: The parsed value.
|
|
1396
1399
|
|
|
1397
1400
|
**Throws**
|
|
1398
1401
|
|
|
1399
1402
|
- `NOT_A_FILE`: if the path doesn't exist or is not a file
|
|
1403
|
+
- `FILE_TOO_LARGE`: if the file holds more than maxBytes bytes
|
|
1400
1404
|
- `FILE_CONTENT_IS_EMPTY_OR_INVALID`: if the file's JSON content cannot be parsed
|
|
1401
1405
|
|
|
1402
1406
|
**Example 1**
|
|
@@ -1504,7 +1508,7 @@ writeFile('out/report.csv', csv, { encoding: 'utf-8' });
|
|
|
1504
1508
|
|
|
1505
1509
|
*Function*
|
|
1506
1510
|
|
|
1507
|
-
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.
|
|
1511
|
+
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.
|
|
1508
1512
|
|
|
1509
1513
|
```ts
|
|
1510
1514
|
writeFileAtomic(
|
|
@@ -1534,7 +1538,7 @@ writeFileAtomic('secrets.env', 'TOKEN=abc\n', 0o600); // only the owner can read
|
|
|
1534
1538
|
|
|
1535
1539
|
*Function*
|
|
1536
1540
|
|
|
1537
|
-
Writes a JSON file. A string is written as given; anything else is stringified.
|
|
1541
|
+
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.
|
|
1538
1542
|
|
|
1539
1543
|
```ts
|
|
1540
1544
|
writeJsonFile<T = Record<string, unknown>>(
|
|
@@ -1692,12 +1696,14 @@ what readJsonFile accepts
|
|
|
1692
1696
|
```ts
|
|
1693
1697
|
type IReadJsonFileOptions<T> = {
|
|
1694
1698
|
schema?: ISchema<T>;
|
|
1699
|
+
maxBytes?: number;
|
|
1695
1700
|
};
|
|
1696
1701
|
```
|
|
1697
1702
|
|
|
1698
1703
|
**Members**
|
|
1699
1704
|
|
|
1700
1705
|
- `schema: ISchema<T>` (property): validates the parsed content, such as a Zod schema. Its errors propagate unchanged
|
|
1706
|
+
- `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
|
|
1701
1707
|
|
|
1702
1708
|
#### <a id="api-sync-ISchema"></a>`ISchema`
|
|
1703
1709
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `readJsonFile` takes `maxBytes`, and then reads the file as `readBoundedFile` does, throwing `FILE_TOO_LARGE` for a larger one, for JSON whose size isn't yours to decide.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `writeJsonFile` writes atomically, as `writeFileAtomic` does, so a crash or a full disk never leaves a truncated file. A symbolic link at the path keeps pointing at the file, whose target is replaced.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `writeFileAtomic`, and so `writeJsonFile`, tries the rename again for about 4 seconds while Windows refuses it with `EACCES`, `EBUSY`, or `EPERM` because another process, such as an antivirus scanner, holds the file.
|
|
16
|
+
|
|
17
|
+
## 1.1.1
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `RECIPES.md`, patterns for apps that use the package: a config file that's never half-written, and reading uploads with a size limit. Each block is typechecked against the release.
|
|
22
|
+
|
|
3
23
|
## 1.1.0
|
|
4
24
|
|
|
5
25
|
A fresh start: breaking changes in a minor version, once, because nothing depended on 1.0.x.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install jg-fs
|
|
|
13
13
|
The root entry is async, and `jg-fs/sync` has the same functions, synchronous:
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
import { readJsonFile, writeFileAtomic, writeTextFile } from 'jg-fs';
|
|
16
|
+
import { readJsonFile, writeFileAtomic, writeJsonFile, writeTextFile } from 'jg-fs';
|
|
17
17
|
import { readTextFile } from 'jg-fs/sync';
|
|
18
18
|
import { z } from 'zod';
|
|
19
19
|
|
|
@@ -21,7 +21,9 @@ const Config = z.object({ port: z.number() });
|
|
|
21
21
|
|
|
22
22
|
await writeTextFile('notes/today.md', '# Today'); // creates notes/ when it's missing
|
|
23
23
|
const config = await readJsonFile('config.json', { schema: Config }); // validated, and typed by the schema
|
|
24
|
-
await
|
|
24
|
+
await writeJsonFile('config.json', { ...config, port: 3000 }); // atomic: readers never see a partial file
|
|
25
|
+
await writeFileAtomic('state.txt', 'step 2'); // any content, atomically
|
|
26
|
+
const upload = await readJsonFile('uploads/import.json', { maxBytes: 1_048_576 }); // FILE_TOO_LARGE past 1 MB
|
|
25
27
|
|
|
26
28
|
const readme = readTextFile('README.md');
|
|
27
29
|
```
|
|
@@ -54,6 +56,8 @@ Failures throw `Exception`s from `jg-exception`, with the codes in `ERRORS`.
|
|
|
54
56
|
|
|
55
57
|
The full API reference ships in the package as `API.md`, generated from its TSDoc for the installed version, and is at https://jg-packages.jesusgraterol.dev/packages/jg-fs/.
|
|
56
58
|
|
|
59
|
+
Recipes for common jobs ship as `RECIPES.md`, with every block typechecked against the release, and are at https://jg-packages.jesusgraterol.dev/packages/jg-fs/recipes/.
|
|
60
|
+
|
|
57
61
|
## License
|
|
58
62
|
|
|
59
63
|
[MIT](https://github.com/jesusgraterol/jg-packages/blob/main/packages/jg-fs/LICENSE)
|
package/RECIPES.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# jg-fs recipes
|
|
2
|
+
|
|
3
|
+
Patterns for Node.js apps that keep their files whole through crashes, and read files from users within a size limit.
|
|
4
|
+
|
|
5
|
+
## A config file that's never half-written
|
|
6
|
+
|
|
7
|
+
Use this for a file the app reads at startup and rewrites while it runs, such as settings changed from an admin page. A plain write that's cut short, by a crash or a full disk, leaves a truncated file that won't parse at the next start. `writeJsonFile` writes a temporary file next to it, flushes it to disk, and renames it over the old one, so the file holds either the old config or the new one:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { hasErrorCode } from 'jg-exception';
|
|
11
|
+
import { ERRORS, readJsonFile, writeJsonFile } from 'jg-fs';
|
|
12
|
+
|
|
13
|
+
// the config's type, from your schema
|
|
14
|
+
type IConfig = ReturnType<typeof Config.parse>;
|
|
15
|
+
|
|
16
|
+
const CONFIG_PATH = 'data/config.json';
|
|
17
|
+
|
|
18
|
+
// what the app runs with until someone saves a change
|
|
19
|
+
const DEFAULT_CONFIG: IConfig = { port: 8080 };
|
|
20
|
+
|
|
21
|
+
export const loadConfig = async (): Promise<IConfig> => {
|
|
22
|
+
try {
|
|
23
|
+
// Config is your schema, such as a Zod object, so a file edited by hand into a wrong shape
|
|
24
|
+
// fails here, at startup, instead of somewhere later
|
|
25
|
+
return await readJsonFile(CONFIG_PATH, { schema: Config });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
// nothing was saved yet
|
|
28
|
+
if (hasErrorCode(error, ERRORS.NOT_A_FILE)) {
|
|
29
|
+
return DEFAULT_CONFIG;
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// the update in progress, which the next one waits for
|
|
36
|
+
let pendingUpdate: Promise<unknown> = Promise.resolve();
|
|
37
|
+
|
|
38
|
+
// reads, changes, and saves the config one update at a time, so two updates never start from
|
|
39
|
+
// the same version and lose one of the changes
|
|
40
|
+
export const updateConfig = (change: (config: IConfig) => IConfig): Promise<IConfig> => {
|
|
41
|
+
const update = pendingUpdate.then(async () => {
|
|
42
|
+
const config = change(await loadConfig());
|
|
43
|
+
await writeJsonFile(CONFIG_PATH, config);
|
|
44
|
+
return config;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// a failed update rejects for its caller, and the next one still runs
|
|
48
|
+
pendingUpdate = update.catch(() => undefined);
|
|
49
|
+
return update;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
await updateConfig((config) => ({ ...config, port: 3000 }));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`writeFileAtomic` writes any other content the same way, and both try again for a few seconds while Windows refuses the rename because a scanner holds the file. A symbolic link at the path keeps pointing at the config. Any error other than a missing file propagates, such as `FILE_CONTENT_IS_EMPTY_OR_INVALID` for a file that isn't JSON, or the schema's own error, so the app never starts from a config it can't trust. The atomic write keeps each save whole, but doesn't order saves: the queue does that inside one process, and when several processes write the file, give it one owner. A crash during a save can leave a temporary `.<random>.tmp` file next to the config, which is safe to delete.
|
|
56
|
+
|
|
57
|
+
`jg-fs/sync` has the same functions without promises, for code that can't await, such as an `exit` handler or a short CLI command. An explicit mode keeps a secret private from the first byte written:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { homedir } from 'node:os';
|
|
61
|
+
import { join } from 'node:path';
|
|
62
|
+
import { writeFileAtomic } from 'jg-fs/sync';
|
|
63
|
+
|
|
64
|
+
// the token your CLI's login command received
|
|
65
|
+
declare const token: string;
|
|
66
|
+
|
|
67
|
+
// kept outside any project, so it's never committed with one
|
|
68
|
+
const credentialsPath = join(homedir(), '.my-cli', 'credentials.json');
|
|
69
|
+
|
|
70
|
+
// on POSIX, only the owner can read it, and the temporary file is never more widely readable
|
|
71
|
+
writeFileAtomic(credentialsPath, JSON.stringify({ token }), 0o600);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Without a mode, a replaced file keeps its permission bits, so a config someone restricted with `chmod` stays restricted after the app saves it.
|
|
75
|
+
|
|
76
|
+
## Reading uploads with a size limit
|
|
77
|
+
|
|
78
|
+
Use this when a request names a file that someone uploaded earlier, such as an import or an attachment, and the app reads it into memory. The name comes from a client, and the file can be larger than you expect, so the read checks three things: the path stays inside the uploads directory, the file is a regular file and not a symbolic link, and its size is within a limit:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { join, resolve } from 'node:path';
|
|
82
|
+
import { Exception, hasErrorCode } from 'jg-exception';
|
|
83
|
+
import { ERRORS, isPathWithin, readBoundedFile } from 'jg-fs';
|
|
84
|
+
|
|
85
|
+
// where your upload handler saves files, a directory that only the app writes to
|
|
86
|
+
const UPLOADS_DIRECTORY = resolve('uploads');
|
|
87
|
+
|
|
88
|
+
// the largest upload the app loads into memory
|
|
89
|
+
const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
90
|
+
|
|
91
|
+
// reads an upload by the name a request gave, such as 'report.csv'. The messages name the upload
|
|
92
|
+
// and not the server's path, and the cause keeps jg-fs's error for the logs
|
|
93
|
+
export const readUpload = async (name: string): Promise<Buffer> => {
|
|
94
|
+
// a name such as '../.env' leads outside the directory, which counts as not found
|
|
95
|
+
const path = join(UPLOADS_DIRECTORY, name);
|
|
96
|
+
if (!isPathWithin(UPLOADS_DIRECTORY, path)) {
|
|
97
|
+
throw new Exception('The upload was not found.', {
|
|
98
|
+
code: 'UPLOAD_NOT_FOUND',
|
|
99
|
+
data: { name },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return await readBoundedFile(path, MAX_UPLOAD_BYTES);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// a missing file, a directory, or a symbolic link someone planted
|
|
106
|
+
if (hasErrorCode(error, ERRORS.NOT_A_FILE)) {
|
|
107
|
+
throw new Exception('The upload was not found.', {
|
|
108
|
+
code: 'UPLOAD_NOT_FOUND',
|
|
109
|
+
data: { name },
|
|
110
|
+
cause: error,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (hasErrorCode(error, ERRORS.FILE_TOO_LARGE)) {
|
|
114
|
+
throw new Exception('The upload is too large to read.', {
|
|
115
|
+
code: 'UPLOAD_TOO_LARGE',
|
|
116
|
+
data: { name, maxBytes: MAX_UPLOAD_BYTES },
|
|
117
|
+
cause: error,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`readBoundedFile` checks the size before it reads, and reads at most one byte past the limit, so a file that grows during the read can't make it load more. It refuses a symbolic link as the last part of the path, but follows links in the directories above it, which is why the uploads go in a directory that only the app writes to. A request handler can answer `UPLOAD_NOT_FOUND` with a 404, and `jg-fs/sync` has the same `readBoundedFile` for code that can't await.
|
|
@@ -38,7 +38,9 @@ export declare const writeFile: (path: string, data: string | NodeJS.ArrayBuffer
|
|
|
38
38
|
*/
|
|
39
39
|
export declare const writeTextFile: (path: string, data: string) => Promise<void>;
|
|
40
40
|
/**
|
|
41
|
-
* Writes a JSON file
|
|
41
|
+
* Writes a JSON file atomically, as writeFileAtomic does, so a crash or a full disk never leaves a
|
|
42
|
+
* truncated file that won't parse. A symbolic link at the path keeps pointing at the file, whose
|
|
43
|
+
* target is replaced. A string is written as given; anything else is stringified.
|
|
42
44
|
* @param path The file's path.
|
|
43
45
|
* @param data The value, or its JSON text.
|
|
44
46
|
* @param space The indentation for stringified values.
|
|
@@ -95,9 +97,11 @@ export declare function readFile(path: string, options?: IReadFileOptions): Prom
|
|
|
95
97
|
export declare const readTextFile: (path: string) => Promise<string>;
|
|
96
98
|
/**
|
|
97
99
|
* Reads and parses a JSON file, and validates it with the schema when there is one. Without a
|
|
98
|
-
* schema, the value is `unknown`, or the type you pass.
|
|
100
|
+
* schema, the value is `unknown`, or the type you pass. With `maxBytes`, it reads the file as
|
|
101
|
+
* readBoundedFile does, for a file whose size isn't yours to decide.
|
|
99
102
|
* @param path The file's path.
|
|
100
|
-
* @param options The schema, such as a Zod schema, whose errors propagate unchanged
|
|
103
|
+
* @param options The schema, such as a Zod schema, whose errors propagate unchanged, and the
|
|
104
|
+
* largest file accepted, in bytes.
|
|
101
105
|
* @returns A promise that resolves to the parsed value.
|
|
102
106
|
* @example
|
|
103
107
|
* import { readJsonFile } from 'jg-fs';
|
|
@@ -109,6 +113,7 @@ export declare const readTextFile: (path: string) => Promise<string>;
|
|
|
109
113
|
* const value = await readJsonFile('data.json'); // unknown
|
|
110
114
|
* @throws
|
|
111
115
|
* - NOT_A_FILE: if the path doesn't exist or is not a file
|
|
116
|
+
* - FILE_TOO_LARGE: if the file holds more than maxBytes bytes
|
|
112
117
|
* - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed
|
|
113
118
|
*/
|
|
114
119
|
export declare const readJsonFile: <T = unknown>(path: string, options?: IReadJsonFileOptions<T>) => Promise<T>;
|
|
@@ -169,7 +174,9 @@ export declare const createFileSymLink: (target: string, path: string) => Promis
|
|
|
169
174
|
* to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins;
|
|
170
175
|
* without one, a replaced file keeps its permission bits and a new file gets the platform default.
|
|
171
176
|
* The temporary file is never more widely readable than the final mode. When writing fails before
|
|
172
|
-
* the rename, the temporary file is removed and the destination is untouched.
|
|
177
|
+
* the rename, the temporary file is removed and the destination is untouched. While Windows refuses
|
|
178
|
+
* the rename because another process, such as an antivirus scanner, holds the file, it tries again
|
|
179
|
+
* for about 4 seconds. A symbolic link at the path is replaced by the file.
|
|
173
180
|
* @param path The file's path.
|
|
174
181
|
* @param data The content.
|
|
175
182
|
* @param mode The permission bits of the written file, such as `0o600`.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { copyFile as copyFileNode, mkdir, open, readFile as readFileNode,
|
|
3
|
+
import { copyFile as copyFileNode, mkdir, open, readFile as readFileNode, rm, symlink, unlink, writeFile as writeFileNode, } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { Exception } from 'jg-exception';
|
|
6
6
|
import { assertBufferData, assertTextData, BOUNDED_READ_CHUNK_SIZE, ERRORS, fileTooLarge, notAFile, parseJsonContent, serializeJsonData, } from '../../shared/index.js';
|
|
7
7
|
import { getPathElement } from '../general/index.js';
|
|
8
|
-
import { openWithoutFollowing, readPermissionBits, syncDirectory } from './utilities.js';
|
|
8
|
+
import { openWithoutFollowing, readPermissionBits, renameReplacing, resolveWriteTarget, syncDirectory, } from './utilities.js';
|
|
9
9
|
/**
|
|
10
10
|
* Checks whether a path exists and is a regular file.
|
|
11
11
|
* @param path The path.
|
|
@@ -50,7 +50,9 @@ export const writeTextFile = async (path, data) => {
|
|
|
50
50
|
await writeFile(path, data, { encoding: 'utf-8' });
|
|
51
51
|
};
|
|
52
52
|
/**
|
|
53
|
-
* Writes a JSON file
|
|
53
|
+
* Writes a JSON file atomically, as writeFileAtomic does, so a crash or a full disk never leaves a
|
|
54
|
+
* truncated file that won't parse. A symbolic link at the path keeps pointing at the file, whose
|
|
55
|
+
* target is replaced. A string is written as given; anything else is stringified.
|
|
54
56
|
* @param path The file's path.
|
|
55
57
|
* @param data The value, or its JSON text.
|
|
56
58
|
* @param space The indentation for stringified values.
|
|
@@ -63,7 +65,8 @@ export const writeTextFile = async (path, data) => {
|
|
|
63
65
|
* - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty
|
|
64
66
|
*/
|
|
65
67
|
export const writeJsonFile = async (path, data, space = 2) => {
|
|
66
|
-
|
|
68
|
+
const content = serializeJsonData(path, data, space);
|
|
69
|
+
await writeFileAtomic(await resolveWriteTarget(path), content);
|
|
67
70
|
};
|
|
68
71
|
/**
|
|
69
72
|
* Writes a binary file. An empty Buffer writes an empty file.
|
|
@@ -102,9 +105,11 @@ export async function readFile(path, options = null) {
|
|
|
102
105
|
export const readTextFile = (path) => readFile(path, { encoding: 'utf-8' });
|
|
103
106
|
/**
|
|
104
107
|
* Reads and parses a JSON file, and validates it with the schema when there is one. Without a
|
|
105
|
-
* schema, the value is `unknown`, or the type you pass.
|
|
108
|
+
* schema, the value is `unknown`, or the type you pass. With `maxBytes`, it reads the file as
|
|
109
|
+
* readBoundedFile does, for a file whose size isn't yours to decide.
|
|
106
110
|
* @param path The file's path.
|
|
107
|
-
* @param options The schema, such as a Zod schema, whose errors propagate unchanged
|
|
111
|
+
* @param options The schema, such as a Zod schema, whose errors propagate unchanged, and the
|
|
112
|
+
* largest file accepted, in bytes.
|
|
108
113
|
* @returns A promise that resolves to the parsed value.
|
|
109
114
|
* @example
|
|
110
115
|
* import { readJsonFile } from 'jg-fs';
|
|
@@ -116,9 +121,12 @@ export const readTextFile = (path) => readFile(path, { encoding: 'utf-8' });
|
|
|
116
121
|
* const value = await readJsonFile('data.json'); // unknown
|
|
117
122
|
* @throws
|
|
118
123
|
* - NOT_A_FILE: if the path doesn't exist or is not a file
|
|
124
|
+
* - FILE_TOO_LARGE: if the file holds more than maxBytes bytes
|
|
119
125
|
* - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed
|
|
120
126
|
*/
|
|
121
|
-
export const readJsonFile = async (path, options = {}) => parseJsonContent(path,
|
|
127
|
+
export const readJsonFile = async (path, options = {}) => parseJsonContent(path, options.maxBytes === undefined
|
|
128
|
+
? await readTextFile(path)
|
|
129
|
+
: (await readBoundedFile(path, options.maxBytes)).toString('utf-8'), options.schema);
|
|
122
130
|
/**
|
|
123
131
|
* Reads a binary file. An empty file reads as an empty Buffer.
|
|
124
132
|
* @param path The file's path.
|
|
@@ -197,7 +205,9 @@ export const createFileSymLink = async (target, path) => {
|
|
|
197
205
|
* to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins;
|
|
198
206
|
* without one, a replaced file keeps its permission bits and a new file gets the platform default.
|
|
199
207
|
* The temporary file is never more widely readable than the final mode. When writing fails before
|
|
200
|
-
* the rename, the temporary file is removed and the destination is untouched.
|
|
208
|
+
* the rename, the temporary file is removed and the destination is untouched. While Windows refuses
|
|
209
|
+
* the rename because another process, such as an antivirus scanner, holds the file, it tries again
|
|
210
|
+
* for about 4 seconds. A symbolic link at the path is replaced by the file.
|
|
201
211
|
* @param path The file's path.
|
|
202
212
|
* @param data The content.
|
|
203
213
|
* @param mode The permission bits of the written file, such as `0o600`.
|
|
@@ -228,7 +238,7 @@ export const writeFileAtomic = async (path, data, mode) => {
|
|
|
228
238
|
finally {
|
|
229
239
|
await handle.close();
|
|
230
240
|
}
|
|
231
|
-
await
|
|
241
|
+
await renameReplacing(tempPath, path);
|
|
232
242
|
isRenamed = true;
|
|
233
243
|
}
|
|
234
244
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../../src/async/files/files.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from 'node:buffer';\nimport { randomUUID } from 'node:crypto';\nimport type { WriteFileOptions } from 'node:fs';\nimport {\n copyFile as copyFileNode,\n mkdir,\n open,\n readFile as readFileNode,\n rename,\n rm,\n symlink,\n unlink,\n writeFile as writeFileNode,\n} from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { Exception } from 'jg-exception';\n\nimport {\n assertBufferData,\n assertTextData,\n BOUNDED_READ_CHUNK_SIZE,\n ERRORS,\n fileTooLarge,\n notAFile,\n parseJsonContent,\n serializeJsonData,\n type IReadBufferFileOptions,\n type IReadFileOptions,\n type IReadJsonFileOptions,\n type IReadStringFileOptions,\n} from '../../shared/index.js';\nimport { getPathElement } from '../general/index.js';\n\nimport { openWithoutFollowing, readPermissionBits, syncDirectory } from './utilities.js';\n\n/**\n * Checks whether a path exists and is a regular file.\n * @param path The path.\n * @returns A promise that resolves to whether the path is a file.\n * @example\n * import { isFile } from 'jg-fs';\n *\n * await isFile('package.json'); // true\n */\nexport const isFile = async (path: string): Promise<boolean> =>\n (await getPathElement(path))?.isFile ?? false;\n\n/**\n * Writes a file, creating its directory first when it doesn't exist.\n * @param path The file's path.\n * @param data The content.\n * @param options Node.js's writeFile options, such as the encoding or mode.\n * @returns A promise that resolves once the file is written.\n * @example\n * import { writeFile } from 'jg-fs';\n *\n * const csv = 'id,name\\n1,Ada\\n';\n * await writeFile('out/report.csv', csv, { encoding: 'utf-8' });\n */\nexport const writeFile = async (\n path: string,\n data: string | NodeJS.ArrayBufferView,\n options?: WriteFileOptions,\n): Promise<void> => {\n // a recursive mkdir succeeds when a concurrent call already created the directory\n await mkdir(dirname(path), { recursive: true });\n await writeFileNode(path, data, options);\n};\n\n/**\n * Writes a UTF-8 text file. An empty string writes an empty file.\n * @param path The file's path.\n * @param data The text.\n * @returns A promise that resolves once the file is written.\n * @example\n * import { writeTextFile } from 'jg-fs';\n *\n * await writeTextFile('notes/today.md', '# Today');\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a string\n */\nexport const writeTextFile = async (path: string, data: string): Promise<void> => {\n assertTextData(path, data);\n await writeFile(path, data, { encoding: 'utf-8' });\n};\n\n/**\n * Writes a JSON file. A string is written as given; anything else is stringified.\n * @param path The file's path.\n * @param data The value, or its JSON text.\n * @param space The indentation for stringified values.\n * @returns A promise that resolves once the file is written.\n * @example\n * import { writeJsonFile } from 'jg-fs';\n *\n * await writeJsonFile('config.json', { port: 8080 });\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty\n */\nexport const writeJsonFile = async <T = Record<string, unknown>>(\n path: string,\n data: T | string,\n space: number = 2,\n): Promise<void> => {\n await writeFile(path, serializeJsonData(path, data, space), { encoding: 'utf-8' });\n};\n\n/**\n * Writes a binary file. An empty Buffer writes an empty file.\n * @param path The file's path.\n * @param data The content.\n * @returns A promise that resolves once the file is written.\n * @example\n * import { writeBufferFile } from 'jg-fs';\n *\n * const imageBuffer = Buffer.from('iVBORw0KGgo=', 'base64');\n * await writeBufferFile('avatar.png', imageBuffer);\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a Buffer\n */\nexport const writeBufferFile = async (path: string, data: Buffer): Promise<void> => {\n assertBufferData(path, data);\n await writeFile(path, data);\n};\n\n/**\n * Reads a file as a string when an encoding is given, or as a Buffer otherwise.\n * @param path The file's path.\n * @param options Node.js's readFile options, such as the encoding.\n * @returns A promise that resolves to the content.\n * @example\n * import { readFile } from 'jg-fs';\n *\n * await readFile('notes.md', 'utf-8'); // a string\n * await readFile('avatar.png'); // a Buffer\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport async function readFile(path: string, options: IReadStringFileOptions): Promise<string>;\nexport async function readFile(path: string, options?: IReadBufferFileOptions): Promise<Buffer>;\nexport async function readFile(path: string, options?: IReadFileOptions): Promise<string | Buffer>;\nexport async function readFile(\n path: string,\n options: IReadFileOptions = null,\n): Promise<string | Buffer> {\n if (!(await isFile(path))) {\n throw notAFile(path);\n }\n return readFileNode(path, options);\n}\n\n/**\n * Reads a UTF-8 text file. An empty file reads as an empty string.\n * @param path The file's path.\n * @returns A promise that resolves to the text.\n * @example\n * import { readTextFile } from 'jg-fs';\n *\n * const markdown = await readTextFile('README.md');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const readTextFile = (path: string): Promise<string> =>\n readFile(path, { encoding: 'utf-8' });\n\n/**\n * Reads and parses a JSON file, and validates it with the schema when there is one. Without a\n * schema, the value is `unknown`, or the type you pass.\n * @param path The file's path.\n * @param options The schema, such as a Zod schema, whose errors propagate unchanged.\n * @returns A promise that resolves to the parsed value.\n * @example\n * import { readJsonFile } from 'jg-fs';\n *\n * const config = await readJsonFile('config.json', { schema: Config }); // Config's type\n * @example\n * import { readJsonFile } from 'jg-fs';\n *\n * const value = await readJsonFile('data.json'); // unknown\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed\n */\nexport const readJsonFile = async <T = unknown>(\n path: string,\n options: IReadJsonFileOptions<T> = {},\n): Promise<T> => parseJsonContent(path, await readTextFile(path), options.schema);\n\n/**\n * Reads a binary file. An empty file reads as an empty Buffer.\n * @param path The file's path.\n * @returns A promise that resolves to the content.\n * @example\n * import { readBufferFile } from 'jg-fs';\n *\n * const image = await readBufferFile('avatar.png');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const readBufferFile = (path: string): Promise<Buffer> => readFile(path, null);\n\n/**\n * Copies a file, replacing the destination when it exists.\n * @param srcPath The file to copy.\n * @param destPath Where to copy it.\n * @returns A promise that resolves once the copy exists.\n * @example\n * import { copyFile } from 'jg-fs';\n *\n * await copyFile('config.example.json', 'config.json');\n * @throws\n * - NOT_A_FILE: if the srcPath doesn't exist or is not a file\n */\nexport const copyFile = async (srcPath: string, destPath: string): Promise<void> => {\n if (!(await isFile(srcPath))) {\n throw notAFile(srcPath);\n }\n await copyFileNode(srcPath, destPath);\n};\n\n/**\n * Deletes a file.\n * @param path The file's path.\n * @returns A promise that resolves once the file is gone.\n * @example\n * import { deleteFile } from 'jg-fs';\n *\n * await deleteFile('cache.json');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const deleteFile = async (path: string): Promise<void> => {\n if (!(await isFile(path))) {\n throw notAFile(path);\n }\n await unlink(path);\n};\n\n/**\n * Creates a symbolic link to a file.\n * @param target The file the link points to.\n * @param path Where to create the link.\n * @returns A promise that resolves once the link exists.\n * @example\n * import { createFileSymLink } from 'jg-fs';\n *\n * await createFileSymLink('config.production.json', 'config.json');\n * @throws\n * - NON_EXISTENT_FILE: if the target file does not exist\n * - NOT_A_FILE: if the target is not a file\n */\nexport const createFileSymLink = async (target: string, path: string): Promise<void> => {\n const element = await getPathElement(target);\n if (element === null) {\n throw new Exception(`The target file '${target}' does not exist.`, {\n code: ERRORS.NON_EXISTENT_FILE,\n });\n }\n if (!element.isFile) {\n throw notAFile(target);\n }\n await symlink(target, path, 'file');\n};\n\n/**\n * Writes a file atomically: readers see either the previous content or the complete new content,\n * never a partial write. The data goes to a temporary file in the same directory, which is flushed\n * to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins;\n * without one, a replaced file keeps its permission bits and a new file gets the platform default.\n * The temporary file is never more widely readable than the final mode. When writing fails before\n * the rename, the temporary file is removed and the destination is untouched.\n * @param path The file's path.\n * @param data The content.\n * @param mode The permission bits of the written file, such as `0o600`.\n * @returns A promise that resolves once the file is written and flushed.\n * @example\n * import { writeFileAtomic } from 'jg-fs';\n *\n * const state = { step: 2 };\n * await writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file\n * await writeFileAtomic('secrets.env', 'TOKEN=abc\\n', 0o600); // only the owner can read it\n */\nexport const writeFileAtomic = async (\n path: string,\n data: string | NodeJS.ArrayBufferView,\n mode?: number,\n): Promise<void> => {\n const directory = dirname(path);\n await mkdir(directory, { recursive: true });\n const finalMode = mode ?? (await readPermissionBits(path));\n const tempPath = join(directory, `.${randomUUID()}.tmp`);\n let isRenamed = false;\n try {\n // a known final mode is applied with chmod, so the umask can't change it\n const handle = await open(tempPath, 'wx', finalMode === undefined ? 0o666 : 0o600);\n try {\n if (finalMode !== undefined) {\n await handle.chmod(finalMode);\n }\n await handle.writeFile(data);\n await handle.sync();\n } finally {\n await handle.close();\n }\n await rename(tempPath, path);\n isRenamed = true;\n } finally {\n if (!isRenamed) {\n await rm(tempPath, { force: true });\n }\n }\n if (process.platform !== 'win32') {\n await syncDirectory(directory);\n }\n};\n\n/**\n * Reads a regular file of at most `maxBytes` bytes. It never follows a symbolic link in the\n * path's last component, though links in parent directories are followed, so callers that need\n * containment combine it with `isPathWithin`. It reads at most `maxBytes + 1` bytes, even when the\n * file grows while it's read.\n * @param path The file's path.\n * @param maxBytes The largest size accepted, in bytes.\n * @returns A promise that resolves to the content.\n * @example\n * import { join } from 'node:path';\n * import { readBoundedFile } from 'jg-fs';\n *\n * const upload = await readBoundedFile(join('uploads', 'avatar.png'), 5 * 1024 * 1024); // at most 5 MB\n * @throws\n * - NOT_A_FILE: if the path doesn't exist, its final component is a symbolic link, or it isn't a regular file\n * - FILE_TOO_LARGE: if the file holds more than maxBytes bytes\n */\nexport const readBoundedFile = async (path: string, maxBytes: number): Promise<Buffer> => {\n const handle = await openWithoutFollowing(path);\n try {\n const stats = await handle.stat();\n if (!stats.isFile()) {\n throw notAFile(path);\n }\n if (stats.size > maxBytes) {\n throw fileTooLarge(path, maxBytes);\n }\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n while (totalBytes <= maxBytes) {\n const chunk = Buffer.allocUnsafe(\n Math.min(BOUNDED_READ_CHUNK_SIZE, maxBytes + 1 - totalBytes),\n );\n const { bytesRead } = await handle.read(chunk, 0, chunk.length, totalBytes);\n if (bytesRead === 0) {\n break;\n }\n chunks.push(chunk.subarray(0, bytesRead));\n totalBytes += bytesRead;\n }\n if (totalBytes > maxBytes) {\n throw fileTooLarge(path, maxBytes);\n }\n return Buffer.concat(chunks, totalBytes);\n } finally {\n await handle.close();\n }\n};\n"]}
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../../src/async/files/files.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from 'node:buffer';\nimport { randomUUID } from 'node:crypto';\nimport type { WriteFileOptions } from 'node:fs';\nimport {\n copyFile as copyFileNode,\n mkdir,\n open,\n readFile as readFileNode,\n rm,\n symlink,\n unlink,\n writeFile as writeFileNode,\n} from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { Exception } from 'jg-exception';\n\nimport {\n assertBufferData,\n assertTextData,\n BOUNDED_READ_CHUNK_SIZE,\n ERRORS,\n fileTooLarge,\n notAFile,\n parseJsonContent,\n serializeJsonData,\n type IReadBufferFileOptions,\n type IReadFileOptions,\n type IReadJsonFileOptions,\n type IReadStringFileOptions,\n} from '../../shared/index.js';\nimport { getPathElement } from '../general/index.js';\n\nimport {\n openWithoutFollowing,\n readPermissionBits,\n renameReplacing,\n resolveWriteTarget,\n syncDirectory,\n} from './utilities.js';\n\n/**\n * Checks whether a path exists and is a regular file.\n * @param path The path.\n * @returns A promise that resolves to whether the path is a file.\n * @example\n * import { isFile } from 'jg-fs';\n *\n * await isFile('package.json'); // true\n */\nexport const isFile = async (path: string): Promise<boolean> =>\n (await getPathElement(path))?.isFile ?? false;\n\n/**\n * Writes a file, creating its directory first when it doesn't exist.\n * @param path The file's path.\n * @param data The content.\n * @param options Node.js's writeFile options, such as the encoding or mode.\n * @returns A promise that resolves once the file is written.\n * @example\n * import { writeFile } from 'jg-fs';\n *\n * const csv = 'id,name\\n1,Ada\\n';\n * await writeFile('out/report.csv', csv, { encoding: 'utf-8' });\n */\nexport const writeFile = async (\n path: string,\n data: string | NodeJS.ArrayBufferView,\n options?: WriteFileOptions,\n): Promise<void> => {\n // a recursive mkdir succeeds when a concurrent call already created the directory\n await mkdir(dirname(path), { recursive: true });\n await writeFileNode(path, data, options);\n};\n\n/**\n * Writes a UTF-8 text file. An empty string writes an empty file.\n * @param path The file's path.\n * @param data The text.\n * @returns A promise that resolves once the file is written.\n * @example\n * import { writeTextFile } from 'jg-fs';\n *\n * await writeTextFile('notes/today.md', '# Today');\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a string\n */\nexport const writeTextFile = async (path: string, data: string): Promise<void> => {\n assertTextData(path, data);\n await writeFile(path, data, { encoding: 'utf-8' });\n};\n\n/**\n * Writes a JSON file atomically, as writeFileAtomic does, so a crash or a full disk never leaves a\n * truncated file that won't parse. A symbolic link at the path keeps pointing at the file, whose\n * target is replaced. A string is written as given; anything else is stringified.\n * @param path The file's path.\n * @param data The value, or its JSON text.\n * @param space The indentation for stringified values.\n * @returns A promise that resolves once the file is written.\n * @example\n * import { writeJsonFile } from 'jg-fs';\n *\n * await writeJsonFile('config.json', { port: 8080 });\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty\n */\nexport const writeJsonFile = async <T = Record<string, unknown>>(\n path: string,\n data: T | string,\n space: number = 2,\n): Promise<void> => {\n const content = serializeJsonData(path, data, space);\n await writeFileAtomic(await resolveWriteTarget(path), content);\n};\n\n/**\n * Writes a binary file. An empty Buffer writes an empty file.\n * @param path The file's path.\n * @param data The content.\n * @returns A promise that resolves once the file is written.\n * @example\n * import { writeBufferFile } from 'jg-fs';\n *\n * const imageBuffer = Buffer.from('iVBORw0KGgo=', 'base64');\n * await writeBufferFile('avatar.png', imageBuffer);\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a Buffer\n */\nexport const writeBufferFile = async (path: string, data: Buffer): Promise<void> => {\n assertBufferData(path, data);\n await writeFile(path, data);\n};\n\n/**\n * Reads a file as a string when an encoding is given, or as a Buffer otherwise.\n * @param path The file's path.\n * @param options Node.js's readFile options, such as the encoding.\n * @returns A promise that resolves to the content.\n * @example\n * import { readFile } from 'jg-fs';\n *\n * await readFile('notes.md', 'utf-8'); // a string\n * await readFile('avatar.png'); // a Buffer\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport async function readFile(path: string, options: IReadStringFileOptions): Promise<string>;\nexport async function readFile(path: string, options?: IReadBufferFileOptions): Promise<Buffer>;\nexport async function readFile(path: string, options?: IReadFileOptions): Promise<string | Buffer>;\nexport async function readFile(\n path: string,\n options: IReadFileOptions = null,\n): Promise<string | Buffer> {\n if (!(await isFile(path))) {\n throw notAFile(path);\n }\n return readFileNode(path, options);\n}\n\n/**\n * Reads a UTF-8 text file. An empty file reads as an empty string.\n * @param path The file's path.\n * @returns A promise that resolves to the text.\n * @example\n * import { readTextFile } from 'jg-fs';\n *\n * const markdown = await readTextFile('README.md');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const readTextFile = (path: string): Promise<string> =>\n readFile(path, { encoding: 'utf-8' });\n\n/**\n * Reads and parses a JSON file, and validates it with the schema when there is one. Without a\n * schema, the value is `unknown`, or the type you pass. With `maxBytes`, it reads the file as\n * readBoundedFile does, for a file whose size isn't yours to decide.\n * @param path The file's path.\n * @param options The schema, such as a Zod schema, whose errors propagate unchanged, and the\n * largest file accepted, in bytes.\n * @returns A promise that resolves to the parsed value.\n * @example\n * import { readJsonFile } from 'jg-fs';\n *\n * const config = await readJsonFile('config.json', { schema: Config }); // Config's type\n * @example\n * import { readJsonFile } from 'jg-fs';\n *\n * const value = await readJsonFile('data.json'); // unknown\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n * - FILE_TOO_LARGE: if the file holds more than maxBytes bytes\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed\n */\nexport const readJsonFile = async <T = unknown>(\n path: string,\n options: IReadJsonFileOptions<T> = {},\n): Promise<T> =>\n parseJsonContent(\n path,\n options.maxBytes === undefined\n ? await readTextFile(path)\n : (await readBoundedFile(path, options.maxBytes)).toString('utf-8'),\n options.schema,\n );\n\n/**\n * Reads a binary file. An empty file reads as an empty Buffer.\n * @param path The file's path.\n * @returns A promise that resolves to the content.\n * @example\n * import { readBufferFile } from 'jg-fs';\n *\n * const image = await readBufferFile('avatar.png');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const readBufferFile = (path: string): Promise<Buffer> => readFile(path, null);\n\n/**\n * Copies a file, replacing the destination when it exists.\n * @param srcPath The file to copy.\n * @param destPath Where to copy it.\n * @returns A promise that resolves once the copy exists.\n * @example\n * import { copyFile } from 'jg-fs';\n *\n * await copyFile('config.example.json', 'config.json');\n * @throws\n * - NOT_A_FILE: if the srcPath doesn't exist or is not a file\n */\nexport const copyFile = async (srcPath: string, destPath: string): Promise<void> => {\n if (!(await isFile(srcPath))) {\n throw notAFile(srcPath);\n }\n await copyFileNode(srcPath, destPath);\n};\n\n/**\n * Deletes a file.\n * @param path The file's path.\n * @returns A promise that resolves once the file is gone.\n * @example\n * import { deleteFile } from 'jg-fs';\n *\n * await deleteFile('cache.json');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const deleteFile = async (path: string): Promise<void> => {\n if (!(await isFile(path))) {\n throw notAFile(path);\n }\n await unlink(path);\n};\n\n/**\n * Creates a symbolic link to a file.\n * @param target The file the link points to.\n * @param path Where to create the link.\n * @returns A promise that resolves once the link exists.\n * @example\n * import { createFileSymLink } from 'jg-fs';\n *\n * await createFileSymLink('config.production.json', 'config.json');\n * @throws\n * - NON_EXISTENT_FILE: if the target file does not exist\n * - NOT_A_FILE: if the target is not a file\n */\nexport const createFileSymLink = async (target: string, path: string): Promise<void> => {\n const element = await getPathElement(target);\n if (element === null) {\n throw new Exception(`The target file '${target}' does not exist.`, {\n code: ERRORS.NON_EXISTENT_FILE,\n });\n }\n if (!element.isFile) {\n throw notAFile(target);\n }\n await symlink(target, path, 'file');\n};\n\n/**\n * Writes a file atomically: readers see either the previous content or the complete new content,\n * never a partial write. The data goes to a temporary file in the same directory, which is flushed\n * to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins;\n * without one, a replaced file keeps its permission bits and a new file gets the platform default.\n * The temporary file is never more widely readable than the final mode. When writing fails before\n * the rename, the temporary file is removed and the destination is untouched. While Windows refuses\n * the rename because another process, such as an antivirus scanner, holds the file, it tries again\n * for about 4 seconds. A symbolic link at the path is replaced by the file.\n * @param path The file's path.\n * @param data The content.\n * @param mode The permission bits of the written file, such as `0o600`.\n * @returns A promise that resolves once the file is written and flushed.\n * @example\n * import { writeFileAtomic } from 'jg-fs';\n *\n * const state = { step: 2 };\n * await writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file\n * await writeFileAtomic('secrets.env', 'TOKEN=abc\\n', 0o600); // only the owner can read it\n */\nexport const writeFileAtomic = async (\n path: string,\n data: string | NodeJS.ArrayBufferView,\n mode?: number,\n): Promise<void> => {\n const directory = dirname(path);\n await mkdir(directory, { recursive: true });\n const finalMode = mode ?? (await readPermissionBits(path));\n const tempPath = join(directory, `.${randomUUID()}.tmp`);\n let isRenamed = false;\n try {\n // a known final mode is applied with chmod, so the umask can't change it\n const handle = await open(tempPath, 'wx', finalMode === undefined ? 0o666 : 0o600);\n try {\n if (finalMode !== undefined) {\n await handle.chmod(finalMode);\n }\n await handle.writeFile(data);\n await handle.sync();\n } finally {\n await handle.close();\n }\n await renameReplacing(tempPath, path);\n isRenamed = true;\n } finally {\n if (!isRenamed) {\n await rm(tempPath, { force: true });\n }\n }\n if (process.platform !== 'win32') {\n await syncDirectory(directory);\n }\n};\n\n/**\n * Reads a regular file of at most `maxBytes` bytes. It never follows a symbolic link in the\n * path's last component, though links in parent directories are followed, so callers that need\n * containment combine it with `isPathWithin`. It reads at most `maxBytes + 1` bytes, even when the\n * file grows while it's read.\n * @param path The file's path.\n * @param maxBytes The largest size accepted, in bytes.\n * @returns A promise that resolves to the content.\n * @example\n * import { join } from 'node:path';\n * import { readBoundedFile } from 'jg-fs';\n *\n * const upload = await readBoundedFile(join('uploads', 'avatar.png'), 5 * 1024 * 1024); // at most 5 MB\n * @throws\n * - NOT_A_FILE: if the path doesn't exist, its final component is a symbolic link, or it isn't a regular file\n * - FILE_TOO_LARGE: if the file holds more than maxBytes bytes\n */\nexport const readBoundedFile = async (path: string, maxBytes: number): Promise<Buffer> => {\n const handle = await openWithoutFollowing(path);\n try {\n const stats = await handle.stat();\n if (!stats.isFile()) {\n throw notAFile(path);\n }\n if (stats.size > maxBytes) {\n throw fileTooLarge(path, maxBytes);\n }\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n while (totalBytes <= maxBytes) {\n const chunk = Buffer.allocUnsafe(\n Math.min(BOUNDED_READ_CHUNK_SIZE, maxBytes + 1 - totalBytes),\n );\n const { bytesRead } = await handle.read(chunk, 0, chunk.length, totalBytes);\n if (bytesRead === 0) {\n break;\n }\n chunks.push(chunk.subarray(0, bytesRead));\n totalBytes += bytesRead;\n }\n if (totalBytes > maxBytes) {\n throw fileTooLarge(path, maxBytes);\n }\n return Buffer.concat(chunks, totalBytes);\n } finally {\n await handle.close();\n }\n};\n"]}
|
|
@@ -5,6 +5,21 @@ import { type FileHandle } from 'node:fs/promises';
|
|
|
5
5
|
* @returns A promise that resolves to the bits, or `undefined` when the file doesn't exist.
|
|
6
6
|
*/
|
|
7
7
|
export declare const readPermissionBits: (path: string) => Promise<number | undefined>;
|
|
8
|
+
/**
|
|
9
|
+
* Renames a file over another, trying again for about 4 seconds while Windows refuses because
|
|
10
|
+
* another process, such as an antivirus scanner, holds the destination.
|
|
11
|
+
* @param from The file to rename.
|
|
12
|
+
* @param to Its new path.
|
|
13
|
+
* @returns A promise that resolves once it's renamed.
|
|
14
|
+
*/
|
|
15
|
+
export declare const renameReplacing: (from: string, to: string) => Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the file a write replaces: the target of a symbolic link at the path, so the link keeps
|
|
18
|
+
* pointing at the new content, or the path itself when nothing is there yet.
|
|
19
|
+
* @param path The path.
|
|
20
|
+
* @returns A promise that resolves to the path to write.
|
|
21
|
+
*/
|
|
22
|
+
export declare const resolveWriteTarget: (path: string) => Promise<string>;
|
|
8
23
|
/**
|
|
9
24
|
* Flushes a directory's entries to disk, so a rename inside it survives a crash.
|
|
10
25
|
* @param path The directory's path.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { constants, lstat, open, stat } from 'node:fs/promises';
|
|
2
|
-
import { isNoFollowError, isNotFoundError, notARegularFile } from '../../shared/index.js';
|
|
1
|
+
import { constants, lstat, open, realpath, rename, stat } from 'node:fs/promises';
|
|
2
|
+
import { isBusyRenameError, isNoFollowError, isNotFoundError, notARegularFile, RENAME_RETRY_DELAYS_MS, } from '../../shared/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Reads the permission bits of an existing file.
|
|
5
5
|
* @param path The file's path.
|
|
@@ -16,6 +16,47 @@ export const readPermissionBits = async (path) => {
|
|
|
16
16
|
throw error;
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Renames a file over another, trying again for about 4 seconds while Windows refuses because
|
|
21
|
+
* another process, such as an antivirus scanner, holds the destination.
|
|
22
|
+
* @param from The file to rename.
|
|
23
|
+
* @param to Its new path.
|
|
24
|
+
* @returns A promise that resolves once it's renamed.
|
|
25
|
+
*/
|
|
26
|
+
export const renameReplacing = async (from, to) => {
|
|
27
|
+
for (let attempt = 0;; attempt += 1) {
|
|
28
|
+
try {
|
|
29
|
+
await rename(from, to);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const delay = RENAME_RETRY_DELAYS_MS[attempt];
|
|
34
|
+
if (delay === undefined || !isBusyRenameError(error)) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
await new Promise((resolve) => {
|
|
38
|
+
setTimeout(resolve, delay);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Resolves the file a write replaces: the target of a symbolic link at the path, so the link keeps
|
|
45
|
+
* pointing at the new content, or the path itself when nothing is there yet.
|
|
46
|
+
* @param path The path.
|
|
47
|
+
* @returns A promise that resolves to the path to write.
|
|
48
|
+
*/
|
|
49
|
+
export const resolveWriteTarget = async (path) => {
|
|
50
|
+
try {
|
|
51
|
+
return await realpath(path);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (isNotFoundError(error)) {
|
|
55
|
+
return path;
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
19
60
|
/**
|
|
20
61
|
* Flushes a directory's entries to disk, so a rename inside it survives a crash.
|
|
21
62
|
* @param path The directory's path.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.js","sourceRoot":"","sources":["../../../src/async/files/utilities.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAmB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"utilities.js","sourceRoot":"","sources":["../../../src/async/files/utilities.ts"],"names":[],"mappings":"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","sourcesContent":["import { constants, lstat, open, realpath, rename, stat, type FileHandle } from 'node:fs/promises';\n\nimport {\n isBusyRenameError,\n isNoFollowError,\n isNotFoundError,\n notARegularFile,\n RENAME_RETRY_DELAYS_MS,\n} from '../../shared/index.js';\n\n/**\n * Reads the permission bits of an existing file.\n * @param path The file's path.\n * @returns A promise that resolves to the bits, or `undefined` when the file doesn't exist.\n */\nexport const readPermissionBits = async (path: string): Promise<number | undefined> => {\n try {\n return (await stat(path)).mode & 0o777;\n } catch (error) {\n if (isNotFoundError(error)) {\n return undefined;\n }\n throw error;\n }\n};\n\n/**\n * Renames a file over another, trying again for about 4 seconds while Windows refuses because\n * another process, such as an antivirus scanner, holds the destination.\n * @param from The file to rename.\n * @param to Its new path.\n * @returns A promise that resolves once it's renamed.\n */\nexport const renameReplacing = async (from: string, to: string): Promise<void> => {\n for (let attempt = 0; ; attempt += 1) {\n try {\n await rename(from, to);\n return;\n } catch (error) {\n const delay = RENAME_RETRY_DELAYS_MS[attempt];\n if (delay === undefined || !isBusyRenameError(error)) {\n throw error;\n }\n await new Promise((resolve) => {\n setTimeout(resolve, delay);\n });\n }\n }\n};\n\n/**\n * Resolves the file a write replaces: the target of a symbolic link at the path, so the link keeps\n * pointing at the new content, or the path itself when nothing is there yet.\n * @param path The path.\n * @returns A promise that resolves to the path to write.\n */\nexport const resolveWriteTarget = async (path: string): Promise<string> => {\n try {\n return await realpath(path);\n } catch (error) {\n if (isNotFoundError(error)) {\n return path;\n }\n throw error;\n }\n};\n\n/**\n * Flushes a directory's entries to disk, so a rename inside it survives a crash.\n * @param path The directory's path.\n * @returns A promise that resolves once the directory is flushed.\n */\nexport const syncDirectory = async (path: string): Promise<void> => {\n const handle = await open(path, 'r');\n try {\n await handle.sync();\n } finally {\n await handle.close();\n }\n};\n\n/**\n * Opens a file for reading without following a symbolic link in its last component.\n * @param path The file's path.\n * @returns A promise that resolves to the open file.\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or its final component is a symbolic link\n */\nexport const openWithoutFollowing = async (path: string): Promise<FileHandle> => {\n try {\n if (process.platform === 'win32') {\n // Windows has no O_NOFOLLOW, so a link swapped in after this check isn't detected\n if ((await lstat(path)).isSymbolicLink()) {\n throw Object.assign(new Error('The path is a symbolic link.'), { code: 'ELOOP' });\n }\n return await open(path, constants.O_RDONLY);\n }\n // O_NONBLOCK keeps a named pipe at the path from blocking the open\n return await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);\n } catch (error) {\n if (isNoFollowError(error)) {\n throw notARegularFile(path, error);\n }\n throw error;\n }\n};\n"]}
|
|
@@ -2,3 +2,4 @@ import type { IDirectoryElementsOptions } from './types.js';
|
|
|
2
2
|
export declare const DIRECTORY_ELEMENTS_DEFAULT_OPTIONS: IDirectoryElementsOptions;
|
|
3
3
|
export declare const DIRECTORY_ELEMENTS_CONCURRENCY = 32;
|
|
4
4
|
export declare const BOUNDED_READ_CHUNK_SIZE = 65536;
|
|
5
|
+
export declare const RENAME_RETRY_DELAYS_MS: readonly number[];
|
package/dist/shared/constants.js
CHANGED
|
@@ -8,4 +8,9 @@ export const DIRECTORY_ELEMENTS_DEFAULT_OPTIONS = {
|
|
|
8
8
|
export const DIRECTORY_ELEMENTS_CONCURRENCY = 32;
|
|
9
9
|
// the number of bytes readBoundedFile reads per call
|
|
10
10
|
export const BOUNDED_READ_CHUNK_SIZE = 65536;
|
|
11
|
+
// the waits, in milliseconds, between the attempts of a rename Windows refuses while another
|
|
12
|
+
// process, such as an antivirus scanner or a search indexer, holds the file: about 4 seconds in all
|
|
13
|
+
export const RENAME_RETRY_DELAYS_MS = [
|
|
14
|
+
10, 20, 40, 80, 160, 320, 640, 1000, 1000, 1000,
|
|
15
|
+
];
|
|
11
16
|
//# sourceMappingURL=constants.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/shared/constants.ts"],"names":[],"mappings":"AAEA,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kCAAkC,GAA8B;IAC3E,SAAS,EAAE,UAAU;IACrB,SAAS,EAAE,KAAK;IAChB,WAAW,EAAE,EAAE;CAChB,CAAC;AAEF,mIAAmI;AACnI,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC;AAEjD,qDAAqD;AACrD,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,CAAC","sourcesContent":["import type { IDirectoryElementsOptions } from './types.js';\n\n// the options getDirectoryElements uses for anything the caller leaves out\nexport const DIRECTORY_ELEMENTS_DEFAULT_OPTIONS: IDirectoryElementsOptions = {\n sortByKey: 'baseName',\n sortOrder: 'asc',\n includeExts: [],\n};\n\n// how many entries the async getDirectoryElements reads at once, so large directories don't open a stat per entry at the same time\nexport const DIRECTORY_ELEMENTS_CONCURRENCY = 32;\n\n// the number of bytes readBoundedFile reads per call\nexport const BOUNDED_READ_CHUNK_SIZE = 65536;\n"]}
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/shared/constants.ts"],"names":[],"mappings":"AAEA,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kCAAkC,GAA8B;IAC3E,SAAS,EAAE,UAAU;IACrB,SAAS,EAAE,KAAK;IAChB,WAAW,EAAE,EAAE;CAChB,CAAC;AAEF,mIAAmI;AACnI,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC;AAEjD,qDAAqD;AACrD,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,CAAC;AAE7C,6FAA6F;AAC7F,oGAAoG;AACpG,MAAM,CAAC,MAAM,sBAAsB,GAAsB;IACvD,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;CAChD,CAAC","sourcesContent":["import type { IDirectoryElementsOptions } from './types.js';\n\n// the options getDirectoryElements uses for anything the caller leaves out\nexport const DIRECTORY_ELEMENTS_DEFAULT_OPTIONS: IDirectoryElementsOptions = {\n sortByKey: 'baseName',\n sortOrder: 'asc',\n includeExts: [],\n};\n\n// how many entries the async getDirectoryElements reads at once, so large directories don't open a stat per entry at the same time\nexport const DIRECTORY_ELEMENTS_CONCURRENCY = 32;\n\n// the number of bytes readBoundedFile reads per call\nexport const BOUNDED_READ_CHUNK_SIZE = 65536;\n\n// the waits, in milliseconds, between the attempts of a rename Windows refuses while another\n// process, such as an antivirus scanner or a search indexer, holds the file: about 4 seconds in all\nexport const RENAME_RETRY_DELAYS_MS: readonly number[] = [\n 10, 20, 40, 80, 160, 320, 640, 1000, 1000, 1000,\n];\n"]}
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { ERRORS } from './errors.js';
|
|
2
2
|
export type { IDirectoryElementsKeySort, IDirectoryElementsOptions, IDirectoryPathElements, IPathElement, IReadBufferFileOptions, IReadFileOptions, IReadJsonFileOptions, IReadStringFileOptions, ISchema, } from './types.js';
|
|
3
|
-
export { BOUNDED_READ_CHUNK_SIZE, DIRECTORY_ELEMENTS_CONCURRENCY, DIRECTORY_ELEMENTS_DEFAULT_OPTIONS, } from './constants.js';
|
|
3
|
+
export { BOUNDED_READ_CHUNK_SIZE, DIRECTORY_ELEMENTS_CONCURRENCY, DIRECTORY_ELEMENTS_DEFAULT_OPTIONS, RENAME_RETRY_DELAYS_MS, } from './constants.js';
|
|
4
4
|
export { isPathWithin, toPathElement } from './paths.js';
|
|
5
5
|
export { buildDirectoryElementsOptions, groupDirectoryElements } from './directory-elements.js';
|
|
6
|
-
export { assertBufferData, assertTextData, fileTooLarge, isNoFollowError, isNotFoundError, notADirectory, notAFile, notARegularFile, parseJsonContent, serializeJsonData, } from './validations.js';
|
|
6
|
+
export { assertBufferData, assertTextData, fileTooLarge, isBusyRenameError, isNoFollowError, isNotFoundError, notADirectory, notAFile, notARegularFile, parseJsonContent, serializeJsonData, } from './validations.js';
|
package/dist/shared/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// errors
|
|
2
2
|
export { ERRORS } from './errors.js';
|
|
3
3
|
// constants
|
|
4
|
-
export { BOUNDED_READ_CHUNK_SIZE, DIRECTORY_ELEMENTS_CONCURRENCY, DIRECTORY_ELEMENTS_DEFAULT_OPTIONS, } from './constants.js';
|
|
4
|
+
export { BOUNDED_READ_CHUNK_SIZE, DIRECTORY_ELEMENTS_CONCURRENCY, DIRECTORY_ELEMENTS_DEFAULT_OPTIONS, RENAME_RETRY_DELAYS_MS, } from './constants.js';
|
|
5
5
|
// paths
|
|
6
6
|
export { isPathWithin, toPathElement } from './paths.js';
|
|
7
7
|
// directory elements
|
|
8
8
|
export { buildDirectoryElementsOptions, groupDirectoryElements } from './directory-elements.js';
|
|
9
9
|
// validations and exceptions
|
|
10
|
-
export { assertBufferData, assertTextData, fileTooLarge, isNoFollowError, isNotFoundError, notADirectory, notAFile, notARegularFile, parseJsonContent, serializeJsonData, } from './validations.js';
|
|
10
|
+
export { assertBufferData, assertTextData, fileTooLarge, isBusyRenameError, isNoFollowError, isNotFoundError, notADirectory, notAFile, notARegularFile, parseJsonContent, serializeJsonData, } from './validations.js';
|
|
11
11
|
//# sourceMappingURL=index.js.map
|
package/dist/shared/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/shared/index.ts"],"names":[],"mappings":"AAAA,SAAS;AACT,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAerC,YAAY;AACZ,OAAO,EACL,uBAAuB,EACvB,8BAA8B,EAC9B,kCAAkC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/shared/index.ts"],"names":[],"mappings":"AAAA,SAAS;AACT,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAerC,YAAY;AACZ,OAAO,EACL,uBAAuB,EACvB,8BAA8B,EAC9B,kCAAkC,EAClC,sBAAsB,GACvB,MAAM,gBAAgB,CAAC;AAExB,QAAQ;AACR,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEzD,qBAAqB;AACrB,OAAO,EAAE,6BAA6B,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEhG,6BAA6B;AAC7B,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,aAAa,EACb,QAAQ,EACR,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC","sourcesContent":["// errors\nexport { ERRORS } from './errors.js';\n\n// types\nexport type {\n IDirectoryElementsKeySort,\n IDirectoryElementsOptions,\n IDirectoryPathElements,\n IPathElement,\n IReadBufferFileOptions,\n IReadFileOptions,\n IReadJsonFileOptions,\n IReadStringFileOptions,\n ISchema,\n} from './types.js';\n\n// constants\nexport {\n BOUNDED_READ_CHUNK_SIZE,\n DIRECTORY_ELEMENTS_CONCURRENCY,\n DIRECTORY_ELEMENTS_DEFAULT_OPTIONS,\n RENAME_RETRY_DELAYS_MS,\n} from './constants.js';\n\n// paths\nexport { isPathWithin, toPathElement } from './paths.js';\n\n// directory elements\nexport { buildDirectoryElementsOptions, groupDirectoryElements } from './directory-elements.js';\n\n// validations and exceptions\nexport {\n assertBufferData,\n assertTextData,\n fileTooLarge,\n isBusyRenameError,\n isNoFollowError,\n isNotFoundError,\n notADirectory,\n notAFile,\n notARegularFile,\n parseJsonContent,\n serializeJsonData,\n} from './validations.js';\n"]}
|
package/dist/shared/types.d.ts
CHANGED
package/dist/shared/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ISortDirection } from 'jg-utilities';\n\n// the details of a path, read with lstat, so a symbolic link is described rather than followed\nexport type IPathElement = {\n // the path, as given\n path: string;\n\n // the last segment of the path\n baseName: string;\n\n // the extension, such as '.json', or an empty string when there is none\n extName: string;\n\n // whether it's a regular file\n isFile: boolean;\n\n // whether it's a directory\n isDirectory: boolean;\n\n // whether it's a symbolic link, in which case isFile and isDirectory are false\n isSymbolicLink: boolean;\n\n // the size in bytes\n size: number;\n\n // when it was created, in milliseconds since the epoch\n creation: number;\n};\n\n// the keys a directory's elements can be sorted by\nexport type IDirectoryElementsKeySort = 'baseName' | 'size' | 'creation';\n\n// how getDirectoryElements filters and sorts a directory's elements\nexport type IDirectoryElementsOptions = {\n // the key the elements are sorted by. Defaults to 'baseName'\n sortByKey: IDirectoryElementsKeySort;\n\n // the sort direction. Defaults to 'asc'\n sortOrder: ISortDirection;\n\n // the extensions of the files to include, such as ['.json'], matched case-insensitively. Defaults to all\n includeExts: string[];\n};\n\n// a directory's elements, grouped by kind\nexport type IDirectoryPathElements = {\n directories: IPathElement[];\n files: IPathElement[];\n symbolicLinks: IPathElement[];\n};\n\n// the readFile options that make it return a string\nexport type IReadStringFileOptions =\n | {\n encoding: BufferEncoding;\n flag?: string | undefined;\n }\n | BufferEncoding;\n\n// the readFile options that make it return a Buffer\nexport type IReadBufferFileOptions = {\n encoding?: null | undefined;\n flag?: string | undefined;\n} | null;\n\n// the options readFile accepts\nexport type IReadFileOptions = IReadBufferFileOptions | IReadStringFileOptions;\n\n// validates parsed data and returns it typed, which a Zod schema does\nexport type ISchema<T> = {\n parse(value: unknown): T;\n};\n\n// what readJsonFile accepts\nexport type IReadJsonFileOptions<T> = {\n // validates the parsed content, such as a Zod schema. Its errors propagate unchanged\n schema?: ISchema<T>;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ISortDirection } from 'jg-utilities';\n\n// the details of a path, read with lstat, so a symbolic link is described rather than followed\nexport type IPathElement = {\n // the path, as given\n path: string;\n\n // the last segment of the path\n baseName: string;\n\n // the extension, such as '.json', or an empty string when there is none\n extName: string;\n\n // whether it's a regular file\n isFile: boolean;\n\n // whether it's a directory\n isDirectory: boolean;\n\n // whether it's a symbolic link, in which case isFile and isDirectory are false\n isSymbolicLink: boolean;\n\n // the size in bytes\n size: number;\n\n // when it was created, in milliseconds since the epoch\n creation: number;\n};\n\n// the keys a directory's elements can be sorted by\nexport type IDirectoryElementsKeySort = 'baseName' | 'size' | 'creation';\n\n// how getDirectoryElements filters and sorts a directory's elements\nexport type IDirectoryElementsOptions = {\n // the key the elements are sorted by. Defaults to 'baseName'\n sortByKey: IDirectoryElementsKeySort;\n\n // the sort direction. Defaults to 'asc'\n sortOrder: ISortDirection;\n\n // the extensions of the files to include, such as ['.json'], matched case-insensitively. Defaults to all\n includeExts: string[];\n};\n\n// a directory's elements, grouped by kind\nexport type IDirectoryPathElements = {\n directories: IPathElement[];\n files: IPathElement[];\n symbolicLinks: IPathElement[];\n};\n\n// the readFile options that make it return a string\nexport type IReadStringFileOptions =\n | {\n encoding: BufferEncoding;\n flag?: string | undefined;\n }\n | BufferEncoding;\n\n// the readFile options that make it return a Buffer\nexport type IReadBufferFileOptions = {\n encoding?: null | undefined;\n flag?: string | undefined;\n} | null;\n\n// the options readFile accepts\nexport type IReadFileOptions = IReadBufferFileOptions | IReadStringFileOptions;\n\n// validates parsed data and returns it typed, which a Zod schema does\nexport type ISchema<T> = {\n parse(value: unknown): T;\n};\n\n// what readJsonFile accepts\nexport type IReadJsonFileOptions<T> = {\n // validates the parsed content, such as a Zod schema. Its errors propagate unchanged\n schema?: ISchema<T>;\n\n // the largest file accepted, in bytes, for a file whose size isn't yours to decide, such as one\n // a user uploaded. With it, the file is read as readBoundedFile reads it\n maxBytes?: number;\n};\n"]}
|
|
@@ -62,6 +62,14 @@ export declare const parseJsonContent: <T>(path: string, content: string, schema
|
|
|
62
62
|
* @returns Whether its code is ENOENT.
|
|
63
63
|
*/
|
|
64
64
|
export declare const isNotFoundError: (error: unknown) => boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Checks whether a rename failed only because another process holds the file for now, which
|
|
67
|
+
* Windows reports as EACCES, EBUSY, or EPERM, so the rename is worth another attempt.
|
|
68
|
+
* @param error The rename's error.
|
|
69
|
+
* @param platform The platform. Defaults to the current one.
|
|
70
|
+
* @returns Whether to try the rename again.
|
|
71
|
+
*/
|
|
72
|
+
export declare const isBusyRenameError: (error: unknown, platform?: NodeJS.Platform) => boolean;
|
|
65
73
|
/**
|
|
66
74
|
* Builds the exception for a bounded read whose path doesn't exist or ends in a symbolic link.
|
|
67
75
|
* @param path The path.
|
|
@@ -106,6 +106,17 @@ export const parseJsonContent = (path, content, schema) => {
|
|
|
106
106
|
* @returns Whether its code is ENOENT.
|
|
107
107
|
*/
|
|
108
108
|
export const isNotFoundError = (error) => error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
109
|
+
/**
|
|
110
|
+
* Checks whether a rename failed only because another process holds the file for now, which
|
|
111
|
+
* Windows reports as EACCES, EBUSY, or EPERM, so the rename is worth another attempt.
|
|
112
|
+
* @param error The rename's error.
|
|
113
|
+
* @param platform The platform. Defaults to the current one.
|
|
114
|
+
* @returns Whether to try the rename again.
|
|
115
|
+
*/
|
|
116
|
+
export const isBusyRenameError = (error, platform = process.platform) => platform === 'win32' &&
|
|
117
|
+
error instanceof Error &&
|
|
118
|
+
'code' in error &&
|
|
119
|
+
(error.code === 'EACCES' || error.code === 'EBUSY' || error.code === 'EPERM');
|
|
109
120
|
/**
|
|
110
121
|
* Builds the exception for a bounded read whose path doesn't exist or ends in a symbolic link.
|
|
111
122
|
* @param path The path.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validations.js","sourceRoot":"","sources":["../../src/shared/validations.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from 'node:buffer';\nimport { Exception } from 'jg-exception';\n\nimport { ERRORS } from './errors.js';\nimport type { ISchema } from './types.js';\n\n/*\n * The checks and exceptions both entries share, so the async and sync functions validate data and\n * report errors identically.\n */\n\n/**\n * Builds the exception for a path that isn't a file.\n * @param path The path.\n * @returns The NOT_A_FILE exception.\n */\nexport const notAFile = (path: string): Exception =>\n new Exception(`The path '${path}' is not a file.`, { code: ERRORS.NOT_A_FILE });\n\n/**\n * Builds the exception for a path that isn't a directory.\n * @param path The path.\n * @returns The NOT_A_DIRECTORY exception.\n */\nexport const notADirectory = (path: string): Exception =>\n new Exception(`The path '${path}' is not a directory.`, { code: ERRORS.NOT_A_DIRECTORY });\n\n/**\n * Builds the exception for a file larger than a bounded read allows.\n * @param path The path.\n * @param maxBytes The limit.\n * @returns The FILE_TOO_LARGE exception.\n */\nexport const fileTooLarge = (path: string, maxBytes: number): Exception =>\n new Exception(`The file '${path}' is larger than ${maxBytes} bytes.`, {\n code: ERRORS.FILE_TOO_LARGE,\n });\n\n/**\n * Ensures text written to a file is a string. An empty string is valid and writes an empty file.\n * @param path The file's path.\n * @param data The text.\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a string\n */\nexport const assertTextData = (path: string, data: unknown): void => {\n if (typeof data !== 'string') {\n throw new Exception(\n `The data for the file '${path}' must be a string. Received: ${typeof data}`,\n { code: ERRORS.FILE_CONTENT_IS_EMPTY_OR_INVALID },\n );\n }\n};\n\n/**\n * Ensures data written to a binary file is a Buffer. An empty Buffer is valid.\n * @param path The file's path.\n * @param data The data.\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a Buffer\n */\nexport const assertBufferData = (path: string, data: unknown): void => {\n if (!Buffer.isBuffer(data)) {\n throw new Exception(\n `The data for the file '${path}' must be a Buffer. Received: ${typeof data}`,\n { code: ERRORS.FILE_CONTENT_IS_EMPTY_OR_INVALID },\n );\n }\n};\n\n/**\n * Turns data into the JSON text of a file: a string is written as given, and anything else is\n * stringified. JSON can't be empty, so an empty result is rejected.\n * @param path The file's path.\n * @param data The data, or its JSON text.\n * @param space The indentation for stringified data.\n * @returns The JSON text.\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty\n */\nexport const serializeJsonData = (path: string, data: unknown, space: number): string => {\n let text: unknown;\n try {\n text = typeof data === 'string' ? data : JSON.stringify(data, undefined, space);\n } catch (error) {\n throw new Exception(`The JSON data for the file '${path}' could not be stringified.`, {\n code: ERRORS.FILE_CONTENT_IS_EMPTY_OR_INVALID,\n cause: error,\n });\n }\n if (typeof text !== 'string' || text === '') {\n throw new Exception(`The JSON data for the file '${path}' is empty.`, {\n code: ERRORS.FILE_CONTENT_IS_EMPTY_OR_INVALID,\n });\n }\n return text;\n};\n\n/**\n * Parses a JSON file's text, and validates it with the schema when there is one.\n * @param path The file's path.\n * @param content The file's text.\n * @param schema The schema, whose errors propagate unchanged.\n * @returns The parsed value.\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed\n */\nexport const parseJsonContent = <T>(path: string, content: string, schema?: ISchema<T>): T => {\n let value: unknown;\n try {\n value = JSON.parse(content);\n } catch (error) {\n throw new Exception(`The JSON file '${path}' could not be parsed.`, {\n code: ERRORS.FILE_CONTENT_IS_EMPTY_OR_INVALID,\n cause: error,\n });\n }\n return schema === undefined ? (value as T) : schema.parse(value);\n};\n\n/**\n * Checks whether an error is a missing-path error from Node.js.\n * @param error The error.\n * @returns Whether its code is ENOENT.\n */\nexport const isNotFoundError = (error: unknown): boolean =>\n error instanceof Error && 'code' in error && error.code === 'ENOENT';\n\n/**\n * Builds the exception for a bounded read whose path doesn't exist or ends in a symbolic link.\n * @param path The path.\n * @param error The error opening it.\n * @returns The NOT_A_FILE exception.\n */\nexport const notARegularFile = (path: string, error: unknown): Exception =>\n new Exception(`The file '${path}' doesn't exist or is a symbolic link.`, {\n code: ERRORS.NOT_A_FILE,\n cause: error,\n });\n\n/**\n * Checks whether opening a path without following links failed because it doesn't exist or its\n * last component is a link (ELOOP, or EMLINK on some BSDs).\n * @param error The error.\n * @returns Whether it means the path isn't a regular file to read.\n */\nexport const isNoFollowError = (error: unknown): boolean =>\n error instanceof Error &&\n 'code' in error &&\n (error.code === 'ENOENT' || error.code === 'ELOOP' || error.code === 'EMLINK');\n"]}
|
|
1
|
+
{"version":3,"file":"validations.js","sourceRoot":"","sources":["../../src/shared/validations.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from 'node:buffer';\nimport { Exception } from 'jg-exception';\n\nimport { ERRORS } from './errors.js';\nimport type { ISchema } from './types.js';\n\n/*\n * The checks and exceptions both entries share, so the async and sync functions validate data and\n * report errors identically.\n */\n\n/**\n * Builds the exception for a path that isn't a file.\n * @param path The path.\n * @returns The NOT_A_FILE exception.\n */\nexport const notAFile = (path: string): Exception =>\n new Exception(`The path '${path}' is not a file.`, { code: ERRORS.NOT_A_FILE });\n\n/**\n * Builds the exception for a path that isn't a directory.\n * @param path The path.\n * @returns The NOT_A_DIRECTORY exception.\n */\nexport const notADirectory = (path: string): Exception =>\n new Exception(`The path '${path}' is not a directory.`, { code: ERRORS.NOT_A_DIRECTORY });\n\n/**\n * Builds the exception for a file larger than a bounded read allows.\n * @param path The path.\n * @param maxBytes The limit.\n * @returns The FILE_TOO_LARGE exception.\n */\nexport const fileTooLarge = (path: string, maxBytes: number): Exception =>\n new Exception(`The file '${path}' is larger than ${maxBytes} bytes.`, {\n code: ERRORS.FILE_TOO_LARGE,\n });\n\n/**\n * Ensures text written to a file is a string. An empty string is valid and writes an empty file.\n * @param path The file's path.\n * @param data The text.\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a string\n */\nexport const assertTextData = (path: string, data: unknown): void => {\n if (typeof data !== 'string') {\n throw new Exception(\n `The data for the file '${path}' must be a string. Received: ${typeof data}`,\n { code: ERRORS.FILE_CONTENT_IS_EMPTY_OR_INVALID },\n );\n }\n};\n\n/**\n * Ensures data written to a binary file is a Buffer. An empty Buffer is valid.\n * @param path The file's path.\n * @param data The data.\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a Buffer\n */\nexport const assertBufferData = (path: string, data: unknown): void => {\n if (!Buffer.isBuffer(data)) {\n throw new Exception(\n `The data for the file '${path}' must be a Buffer. Received: ${typeof data}`,\n { code: ERRORS.FILE_CONTENT_IS_EMPTY_OR_INVALID },\n );\n }\n};\n\n/**\n * Turns data into the JSON text of a file: a string is written as given, and anything else is\n * stringified. JSON can't be empty, so an empty result is rejected.\n * @param path The file's path.\n * @param data The data, or its JSON text.\n * @param space The indentation for stringified data.\n * @returns The JSON text.\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty\n */\nexport const serializeJsonData = (path: string, data: unknown, space: number): string => {\n let text: unknown;\n try {\n text = typeof data === 'string' ? data : JSON.stringify(data, undefined, space);\n } catch (error) {\n throw new Exception(`The JSON data for the file '${path}' could not be stringified.`, {\n code: ERRORS.FILE_CONTENT_IS_EMPTY_OR_INVALID,\n cause: error,\n });\n }\n if (typeof text !== 'string' || text === '') {\n throw new Exception(`The JSON data for the file '${path}' is empty.`, {\n code: ERRORS.FILE_CONTENT_IS_EMPTY_OR_INVALID,\n });\n }\n return text;\n};\n\n/**\n * Parses a JSON file's text, and validates it with the schema when there is one.\n * @param path The file's path.\n * @param content The file's text.\n * @param schema The schema, whose errors propagate unchanged.\n * @returns The parsed value.\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed\n */\nexport const parseJsonContent = <T>(path: string, content: string, schema?: ISchema<T>): T => {\n let value: unknown;\n try {\n value = JSON.parse(content);\n } catch (error) {\n throw new Exception(`The JSON file '${path}' could not be parsed.`, {\n code: ERRORS.FILE_CONTENT_IS_EMPTY_OR_INVALID,\n cause: error,\n });\n }\n return schema === undefined ? (value as T) : schema.parse(value);\n};\n\n/**\n * Checks whether an error is a missing-path error from Node.js.\n * @param error The error.\n * @returns Whether its code is ENOENT.\n */\nexport const isNotFoundError = (error: unknown): boolean =>\n error instanceof Error && 'code' in error && error.code === 'ENOENT';\n\n/**\n * Checks whether a rename failed only because another process holds the file for now, which\n * Windows reports as EACCES, EBUSY, or EPERM, so the rename is worth another attempt.\n * @param error The rename's error.\n * @param platform The platform. Defaults to the current one.\n * @returns Whether to try the rename again.\n */\nexport const isBusyRenameError = (\n error: unknown,\n platform: NodeJS.Platform = process.platform,\n): boolean =>\n platform === 'win32' &&\n error instanceof Error &&\n 'code' in error &&\n (error.code === 'EACCES' || error.code === 'EBUSY' || error.code === 'EPERM');\n\n/**\n * Builds the exception for a bounded read whose path doesn't exist or ends in a symbolic link.\n * @param path The path.\n * @param error The error opening it.\n * @returns The NOT_A_FILE exception.\n */\nexport const notARegularFile = (path: string, error: unknown): Exception =>\n new Exception(`The file '${path}' doesn't exist or is a symbolic link.`, {\n code: ERRORS.NOT_A_FILE,\n cause: error,\n });\n\n/**\n * Checks whether opening a path without following links failed because it doesn't exist or its\n * last component is a link (ELOOP, or EMLINK on some BSDs).\n * @param error The error.\n * @returns Whether it means the path isn't a regular file to read.\n */\nexport const isNoFollowError = (error: unknown): boolean =>\n error instanceof Error &&\n 'code' in error &&\n (error.code === 'ENOENT' || error.code === 'ELOOP' || error.code === 'EMLINK');\n"]}
|
|
@@ -36,7 +36,9 @@ export declare const writeFile: (path: string, data: string | NodeJS.ArrayBuffer
|
|
|
36
36
|
*/
|
|
37
37
|
export declare const writeTextFile: (path: string, data: string) => void;
|
|
38
38
|
/**
|
|
39
|
-
* Writes a JSON file
|
|
39
|
+
* Writes a JSON file atomically, as writeFileAtomic does, so a crash or a full disk never leaves a
|
|
40
|
+
* truncated file that won't parse. A symbolic link at the path keeps pointing at the file, whose
|
|
41
|
+
* target is replaced. A string is written as given; anything else is stringified.
|
|
40
42
|
* @param path The file's path.
|
|
41
43
|
* @param data The value, or its JSON text.
|
|
42
44
|
* @param space The indentation for stringified values.
|
|
@@ -91,9 +93,11 @@ export declare function readFile(path: string, options?: IReadFileOptions): stri
|
|
|
91
93
|
export declare const readTextFile: (path: string) => string;
|
|
92
94
|
/**
|
|
93
95
|
* Reads and parses a JSON file, and validates it with the schema when there is one. Without a
|
|
94
|
-
* schema, the value is `unknown`, or the type you pass.
|
|
96
|
+
* schema, the value is `unknown`, or the type you pass. With `maxBytes`, it reads the file as
|
|
97
|
+
* readBoundedFile does, for a file whose size isn't yours to decide.
|
|
95
98
|
* @param path The file's path.
|
|
96
|
-
* @param options The schema, such as a Zod schema, whose errors propagate unchanged
|
|
99
|
+
* @param options The schema, such as a Zod schema, whose errors propagate unchanged, and the
|
|
100
|
+
* largest file accepted, in bytes.
|
|
97
101
|
* @returns The parsed value.
|
|
98
102
|
* @example
|
|
99
103
|
* import { readJsonFile } from 'jg-fs/sync';
|
|
@@ -105,6 +109,7 @@ export declare const readTextFile: (path: string) => string;
|
|
|
105
109
|
* const value = readJsonFile('data.json'); // unknown
|
|
106
110
|
* @throws
|
|
107
111
|
* - NOT_A_FILE: if the path doesn't exist or is not a file
|
|
112
|
+
* - FILE_TOO_LARGE: if the file holds more than maxBytes bytes
|
|
108
113
|
* - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed
|
|
109
114
|
*/
|
|
110
115
|
export declare const readJsonFile: <T = unknown>(path: string, options?: IReadJsonFileOptions<T>) => T;
|
|
@@ -162,7 +167,9 @@ export declare const createFileSymLink: (target: string, path: string) => void;
|
|
|
162
167
|
* to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins;
|
|
163
168
|
* without one, a replaced file keeps its permission bits and a new file gets the platform default.
|
|
164
169
|
* The temporary file is never more widely readable than the final mode. When writing fails before
|
|
165
|
-
* the rename, the temporary file is removed and the destination is untouched.
|
|
170
|
+
* the rename, the temporary file is removed and the destination is untouched. While Windows refuses
|
|
171
|
+
* the rename because another process, such as an antivirus scanner, holds the file, it tries again
|
|
172
|
+
* for about 4 seconds. A symbolic link at the path is replaced by the file.
|
|
166
173
|
* @param path The file's path.
|
|
167
174
|
* @param data The content.
|
|
168
175
|
* @param mode The permission bits of the written file, such as `0o600`.
|
package/dist/sync/files/files.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { closeSync, copyFileSync, fchmodSync, fstatSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync,
|
|
3
|
+
import { closeSync, copyFileSync, fchmodSync, fstatSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { Exception } from 'jg-exception';
|
|
6
6
|
import { assertBufferData, assertTextData, BOUNDED_READ_CHUNK_SIZE, ERRORS, fileTooLarge, notAFile, parseJsonContent, serializeJsonData, } from '../../shared/index.js';
|
|
7
7
|
import { getPathElement } from '../general/index.js';
|
|
8
|
-
import { openWithoutFollowing, readPermissionBits, syncDirectory } from './utilities.js';
|
|
8
|
+
import { openWithoutFollowing, readPermissionBits, renameReplacing, resolveWriteTarget, syncDirectory, } from './utilities.js';
|
|
9
9
|
/**
|
|
10
10
|
* Checks whether a path exists and is a regular file.
|
|
11
11
|
* @param path The path.
|
|
@@ -47,7 +47,9 @@ export const writeTextFile = (path, data) => {
|
|
|
47
47
|
writeFile(path, data, { encoding: 'utf-8' });
|
|
48
48
|
};
|
|
49
49
|
/**
|
|
50
|
-
* Writes a JSON file
|
|
50
|
+
* Writes a JSON file atomically, as writeFileAtomic does, so a crash or a full disk never leaves a
|
|
51
|
+
* truncated file that won't parse. A symbolic link at the path keeps pointing at the file, whose
|
|
52
|
+
* target is replaced. A string is written as given; anything else is stringified.
|
|
51
53
|
* @param path The file's path.
|
|
52
54
|
* @param data The value, or its JSON text.
|
|
53
55
|
* @param space The indentation for stringified values.
|
|
@@ -59,7 +61,8 @@ export const writeTextFile = (path, data) => {
|
|
|
59
61
|
* - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty
|
|
60
62
|
*/
|
|
61
63
|
export const writeJsonFile = (path, data, space = 2) => {
|
|
62
|
-
|
|
64
|
+
const content = serializeJsonData(path, data, space);
|
|
65
|
+
writeFileAtomic(resolveWriteTarget(path), content);
|
|
63
66
|
};
|
|
64
67
|
/**
|
|
65
68
|
* Writes a binary file. An empty Buffer writes an empty file.
|
|
@@ -97,9 +100,11 @@ export function readFile(path, options = null) {
|
|
|
97
100
|
export const readTextFile = (path) => readFile(path, { encoding: 'utf-8' });
|
|
98
101
|
/**
|
|
99
102
|
* Reads and parses a JSON file, and validates it with the schema when there is one. Without a
|
|
100
|
-
* schema, the value is `unknown`, or the type you pass.
|
|
103
|
+
* schema, the value is `unknown`, or the type you pass. With `maxBytes`, it reads the file as
|
|
104
|
+
* readBoundedFile does, for a file whose size isn't yours to decide.
|
|
101
105
|
* @param path The file's path.
|
|
102
|
-
* @param options The schema, such as a Zod schema, whose errors propagate unchanged
|
|
106
|
+
* @param options The schema, such as a Zod schema, whose errors propagate unchanged, and the
|
|
107
|
+
* largest file accepted, in bytes.
|
|
103
108
|
* @returns The parsed value.
|
|
104
109
|
* @example
|
|
105
110
|
* import { readJsonFile } from 'jg-fs/sync';
|
|
@@ -111,9 +116,12 @@ export const readTextFile = (path) => readFile(path, { encoding: 'utf-8' });
|
|
|
111
116
|
* const value = readJsonFile('data.json'); // unknown
|
|
112
117
|
* @throws
|
|
113
118
|
* - NOT_A_FILE: if the path doesn't exist or is not a file
|
|
119
|
+
* - FILE_TOO_LARGE: if the file holds more than maxBytes bytes
|
|
114
120
|
* - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed
|
|
115
121
|
*/
|
|
116
|
-
export const readJsonFile = (path, options = {}) => parseJsonContent(path,
|
|
122
|
+
export const readJsonFile = (path, options = {}) => parseJsonContent(path, options.maxBytes === undefined
|
|
123
|
+
? readTextFile(path)
|
|
124
|
+
: readBoundedFile(path, options.maxBytes).toString('utf-8'), options.schema);
|
|
117
125
|
/**
|
|
118
126
|
* Reads a binary file. An empty file reads as an empty Buffer.
|
|
119
127
|
* @param path The file's path.
|
|
@@ -189,7 +197,9 @@ export const createFileSymLink = (target, path) => {
|
|
|
189
197
|
* to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins;
|
|
190
198
|
* without one, a replaced file keeps its permission bits and a new file gets the platform default.
|
|
191
199
|
* The temporary file is never more widely readable than the final mode. When writing fails before
|
|
192
|
-
* the rename, the temporary file is removed and the destination is untouched.
|
|
200
|
+
* the rename, the temporary file is removed and the destination is untouched. While Windows refuses
|
|
201
|
+
* the rename because another process, such as an antivirus scanner, holds the file, it tries again
|
|
202
|
+
* for about 4 seconds. A symbolic link at the path is replaced by the file.
|
|
193
203
|
* @param path The file's path.
|
|
194
204
|
* @param data The content.
|
|
195
205
|
* @param mode The permission bits of the written file, such as `0o600`.
|
|
@@ -219,7 +229,7 @@ export const writeFileAtomic = (path, data, mode) => {
|
|
|
219
229
|
finally {
|
|
220
230
|
closeSync(descriptor);
|
|
221
231
|
}
|
|
222
|
-
|
|
232
|
+
renameReplacing(tempPath, path);
|
|
223
233
|
isRenamed = true;
|
|
224
234
|
}
|
|
225
235
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../../src/sync/files/files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,SAAS,EACT,YAAY,EACZ,UAAU,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,MAAM,EACN,WAAW,EACX,UAAU,EACV,aAAa,GAEd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,uBAAuB,EACvB,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,gBAAgB,EAChB,iBAAiB,GAKlB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEzF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,IAAI,KAAK,CAAC;AAEvF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,IAAY,EACZ,IAAqC,EACrC,OAA0B,EACpB,EAAE;IACR,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAY,EAAE,IAAY,EAAQ,EAAE;IAChE,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC3B,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;AAC/C,CAAC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,IAAY,EACZ,IAAgB,EAChB,QAAgB,CAAC,EACX,EAAE;IACR,SAAS,CAAC,IAAI,EAAE,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;AAC/E,CAAC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAE,IAAY,EAAQ,EAAE;IAClE,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC7B,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC,CAAC;AAkBF,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,UAA4B,IAAI;IACrE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAClB,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,YAAY,CAAC,IAAI,EAAE,OAA6C,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;AAE5F;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAc,IAAY,EAAE,UAAmC,EAAE,EAAK,EAAE,CAClG,gBAAgB,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAE7E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAe,EAAE,QAAgB,EAAQ,EAAE;IAClE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACrB,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC1B,CAAC;IACD,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAClC,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAY,EAAQ,EAAE;IAC/C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAClB,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;IACD,UAAU,CAAC,IAAI,CAAC,CAAC;AACnB,CAAC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,MAAc,EAAE,IAAY,EAAQ,EAAE;IACtE,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACvC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,MAAM,IAAI,SAAS,CAAC,oBAAoB,MAAM,mBAAmB,EAAE;YACjE,IAAI,EAAE,MAAM,CAAC,iBAAiB;SAC/B,CAAC,CAAC;IACL,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzB,CAAC;IACD,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,IAAY,EACZ,IAAqC,EACrC,IAAa,EACP,EAAE;IACR,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,MAAM,SAAS,GAAG,IAAI,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,UAAU,EAAE,MAAM,CAAC,CAAC;IACzD,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,CAAC;QACH,yEAAyE;QACzE,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACrF,IAAI,CAAC;YACH,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,UAAU,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;YACpC,CAAC;YACD,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YAChC,SAAS,CAAC,UAAU,CAAC,CAAC;QACxB,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,UAAU,CAAC,CAAC;QACxB,CAAC;QACD,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QAC3B,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,aAAa,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAE,QAAgB,EAAU,EAAE;IACxE,MAAM,UAAU,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACpB,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,GAAG,QAAQ,EAAE,CAAC;YAC1B,MAAM,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACrC,CAAC;QACD,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,OAAO,UAAU,IAAI,QAAQ,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAC9B,IAAI,CAAC,GAAG,CAAC,uBAAuB,EAAE,QAAQ,GAAG,CAAC,GAAG,UAAU,CAAC,CAC7D,CAAC;YACF,MAAM,SAAS,GAAG,QAAQ,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;YAC3E,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;gBACpB,MAAM;YACR,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;YAC1C,UAAU,IAAI,SAAS,CAAC;QAC1B,CAAC;QACD,IAAI,UAAU,GAAG,QAAQ,EAAE,CAAC;YAC1B,MAAM,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACrC,CAAC;QACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC3C,CAAC;YAAS,CAAC;QACT,SAAS,CAAC,UAAU,CAAC,CAAC;IACxB,CAAC;AACH,CAAC,CAAC","sourcesContent":["import { Buffer } from 'node:buffer';\nimport { randomUUID } from 'node:crypto';\nimport {\n closeSync,\n copyFileSync,\n fchmodSync,\n fstatSync,\n fsyncSync,\n mkdirSync,\n openSync,\n readFileSync,\n readSync,\n renameSync,\n rmSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n type WriteFileOptions,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { Exception } from 'jg-exception';\n\nimport {\n assertBufferData,\n assertTextData,\n BOUNDED_READ_CHUNK_SIZE,\n ERRORS,\n fileTooLarge,\n notAFile,\n parseJsonContent,\n serializeJsonData,\n type IReadBufferFileOptions,\n type IReadFileOptions,\n type IReadJsonFileOptions,\n type IReadStringFileOptions,\n} from '../../shared/index.js';\nimport { getPathElement } from '../general/index.js';\n\nimport { openWithoutFollowing, readPermissionBits, syncDirectory } from './utilities.js';\n\n/**\n * Checks whether a path exists and is a regular file.\n * @param path The path.\n * @returns Whether the path is a file.\n * @example\n * import { isFile } from 'jg-fs/sync';\n *\n * isFile('package.json'); // true\n */\nexport const isFile = (path: string): boolean => getPathElement(path)?.isFile ?? false;\n\n/**\n * Writes a file, creating its directory first when it doesn't exist.\n * @param path The file's path.\n * @param data The content.\n * @param options Node.js's writeFile options, such as the encoding or mode.\n * @example\n * import { writeFile } from 'jg-fs/sync';\n *\n * const csv = 'id,name\\n1,Ada\\n';\n * writeFile('out/report.csv', csv, { encoding: 'utf-8' });\n */\nexport const writeFile = (\n path: string,\n data: string | NodeJS.ArrayBufferView,\n options?: WriteFileOptions,\n): void => {\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, data, options);\n};\n\n/**\n * Writes a UTF-8 text file. An empty string writes an empty file.\n * @param path The file's path.\n * @param data The text.\n * @example\n * import { writeTextFile } from 'jg-fs/sync';\n *\n * writeTextFile('notes/today.md', '# Today');\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a string\n */\nexport const writeTextFile = (path: string, data: string): void => {\n assertTextData(path, data);\n writeFile(path, data, { encoding: 'utf-8' });\n};\n\n/**\n * Writes a JSON file. A string is written as given; anything else is stringified.\n * @param path The file's path.\n * @param data The value, or its JSON text.\n * @param space The indentation for stringified values.\n * @example\n * import { writeJsonFile } from 'jg-fs/sync';\n *\n * writeJsonFile('config.json', { port: 8080 });\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty\n */\nexport const writeJsonFile = <T = Record<string, unknown>>(\n path: string,\n data: T | string,\n space: number = 2,\n): void => {\n writeFile(path, serializeJsonData(path, data, space), { encoding: 'utf-8' });\n};\n\n/**\n * Writes a binary file. An empty Buffer writes an empty file.\n * @param path The file's path.\n * @param data The content.\n * @example\n * import { writeBufferFile } from 'jg-fs/sync';\n *\n * const imageBuffer = Buffer.from('iVBORw0KGgo=', 'base64');\n * writeBufferFile('avatar.png', imageBuffer);\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a Buffer\n */\nexport const writeBufferFile = (path: string, data: Buffer): void => {\n assertBufferData(path, data);\n writeFile(path, data);\n};\n\n/**\n * Reads a file as a string when an encoding is given, or as a Buffer otherwise.\n * @param path The file's path.\n * @param options Node.js's readFile options, such as the encoding.\n * @returns The content.\n * @example\n * import { readFile } from 'jg-fs/sync';\n *\n * readFile('notes.md', 'utf-8'); // a string\n * readFile('avatar.png'); // a Buffer\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport function readFile(path: string, options: IReadStringFileOptions): string;\nexport function readFile(path: string, options?: IReadBufferFileOptions): Buffer;\nexport function readFile(path: string, options?: IReadFileOptions): string | Buffer;\nexport function readFile(path: string, options: IReadFileOptions = null): string | Buffer {\n if (!isFile(path)) {\n throw notAFile(path);\n }\n return readFileSync(path, options as Parameters<typeof readFileSync>[1]);\n}\n\n/**\n * Reads a UTF-8 text file. An empty file reads as an empty string.\n * @param path The file's path.\n * @returns The text.\n * @example\n * import { readTextFile } from 'jg-fs/sync';\n *\n * const markdown = readTextFile('README.md');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const readTextFile = (path: string): string => readFile(path, { encoding: 'utf-8' });\n\n/**\n * Reads and parses a JSON file, and validates it with the schema when there is one. Without a\n * schema, the value is `unknown`, or the type you pass.\n * @param path The file's path.\n * @param options The schema, such as a Zod schema, whose errors propagate unchanged.\n * @returns The parsed value.\n * @example\n * import { readJsonFile } from 'jg-fs/sync';\n *\n * const config = readJsonFile('config.json', { schema: Config }); // Config's type\n * @example\n * import { readJsonFile } from 'jg-fs/sync';\n *\n * const value = readJsonFile('data.json'); // unknown\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed\n */\nexport const readJsonFile = <T = unknown>(path: string, options: IReadJsonFileOptions<T> = {}): T =>\n parseJsonContent(path, readTextFile(path), options.schema);\n\n/**\n * Reads a binary file. An empty file reads as an empty Buffer.\n * @param path The file's path.\n * @returns The content.\n * @example\n * import { readBufferFile } from 'jg-fs/sync';\n *\n * const image = readBufferFile('avatar.png');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const readBufferFile = (path: string): Buffer => readFile(path, null);\n\n/**\n * Copies a file, replacing the destination when it exists.\n * @param srcPath The file to copy.\n * @param destPath Where to copy it.\n * @example\n * import { copyFile } from 'jg-fs/sync';\n *\n * copyFile('config.example.json', 'config.json');\n * @throws\n * - NOT_A_FILE: if the srcPath doesn't exist or is not a file\n */\nexport const copyFile = (srcPath: string, destPath: string): void => {\n if (!isFile(srcPath)) {\n throw notAFile(srcPath);\n }\n copyFileSync(srcPath, destPath);\n};\n\n/**\n * Deletes a file.\n * @param path The file's path.\n * @example\n * import { deleteFile } from 'jg-fs/sync';\n *\n * deleteFile('cache.json');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const deleteFile = (path: string): void => {\n if (!isFile(path)) {\n throw notAFile(path);\n }\n unlinkSync(path);\n};\n\n/**\n * Creates a symbolic link to a file.\n * @param target The file the link points to.\n * @param path Where to create the link.\n * @example\n * import { createFileSymLink } from 'jg-fs/sync';\n *\n * createFileSymLink('config.production.json', 'config.json');\n * @throws\n * - NON_EXISTENT_FILE: if the target file does not exist\n * - NOT_A_FILE: if the target is not a file\n */\nexport const createFileSymLink = (target: string, path: string): void => {\n const element = getPathElement(target);\n if (element === null) {\n throw new Exception(`The target file '${target}' does not exist.`, {\n code: ERRORS.NON_EXISTENT_FILE,\n });\n }\n if (!element.isFile) {\n throw notAFile(target);\n }\n symlinkSync(target, path, 'file');\n};\n\n/**\n * Writes a file atomically: readers see either the previous content or the complete new content,\n * never a partial write. The data goes to a temporary file in the same directory, which is flushed\n * to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins;\n * without one, a replaced file keeps its permission bits and a new file gets the platform default.\n * The temporary file is never more widely readable than the final mode. When writing fails before\n * the rename, the temporary file is removed and the destination is untouched.\n * @param path The file's path.\n * @param data The content.\n * @param mode The permission bits of the written file, such as `0o600`.\n * @example\n * import { writeFileAtomic } from 'jg-fs/sync';\n *\n * const state = { step: 2 };\n * writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file\n * writeFileAtomic('secrets.env', 'TOKEN=abc\\n', 0o600); // only the owner can read it\n */\nexport const writeFileAtomic = (\n path: string,\n data: string | NodeJS.ArrayBufferView,\n mode?: number,\n): void => {\n const directory = dirname(path);\n mkdirSync(directory, { recursive: true });\n const finalMode = mode ?? readPermissionBits(path);\n const tempPath = join(directory, `.${randomUUID()}.tmp`);\n let isRenamed = false;\n try {\n // a known final mode is applied with chmod, so the umask can't change it\n const descriptor = openSync(tempPath, 'wx', finalMode === undefined ? 0o666 : 0o600);\n try {\n if (finalMode !== undefined) {\n fchmodSync(descriptor, finalMode);\n }\n writeFileSync(descriptor, data);\n fsyncSync(descriptor);\n } finally {\n closeSync(descriptor);\n }\n renameSync(tempPath, path);\n isRenamed = true;\n } finally {\n if (!isRenamed) {\n rmSync(tempPath, { force: true });\n }\n }\n if (process.platform !== 'win32') {\n syncDirectory(directory);\n }\n};\n\n/**\n * Reads a regular file of at most `maxBytes` bytes. It never follows a symbolic link in the\n * path's last component, though links in parent directories are followed, so callers that need\n * containment combine it with `isPathWithin`. It reads at most `maxBytes + 1` bytes, even when the\n * file grows while it's read.\n * @param path The file's path.\n * @param maxBytes The largest size accepted, in bytes.\n * @returns The content.\n * @example\n * import { join } from 'node:path';\n * import { readBoundedFile } from 'jg-fs/sync';\n *\n * const upload = readBoundedFile(join('uploads', 'avatar.png'), 5 * 1024 * 1024); // at most 5 MB\n * @throws\n * - NOT_A_FILE: if the path doesn't exist, its final component is a symbolic link, or it isn't a regular file\n * - FILE_TOO_LARGE: if the file holds more than maxBytes bytes\n */\nexport const readBoundedFile = (path: string, maxBytes: number): Buffer => {\n const descriptor = openWithoutFollowing(path);\n try {\n const stats = fstatSync(descriptor);\n if (!stats.isFile()) {\n throw notAFile(path);\n }\n if (stats.size > maxBytes) {\n throw fileTooLarge(path, maxBytes);\n }\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n while (totalBytes <= maxBytes) {\n const chunk = Buffer.allocUnsafe(\n Math.min(BOUNDED_READ_CHUNK_SIZE, maxBytes + 1 - totalBytes),\n );\n const bytesRead = readSync(descriptor, chunk, 0, chunk.length, totalBytes);\n if (bytesRead === 0) {\n break;\n }\n chunks.push(chunk.subarray(0, bytesRead));\n totalBytes += bytesRead;\n }\n if (totalBytes > maxBytes) {\n throw fileTooLarge(path, maxBytes);\n }\n return Buffer.concat(chunks, totalBytes);\n } finally {\n closeSync(descriptor);\n }\n};\n"]}
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../../src/sync/files/files.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from 'node:buffer';\nimport { randomUUID } from 'node:crypto';\nimport {\n closeSync,\n copyFileSync,\n fchmodSync,\n fstatSync,\n fsyncSync,\n mkdirSync,\n openSync,\n readFileSync,\n readSync,\n rmSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n type WriteFileOptions,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { Exception } from 'jg-exception';\n\nimport {\n assertBufferData,\n assertTextData,\n BOUNDED_READ_CHUNK_SIZE,\n ERRORS,\n fileTooLarge,\n notAFile,\n parseJsonContent,\n serializeJsonData,\n type IReadBufferFileOptions,\n type IReadFileOptions,\n type IReadJsonFileOptions,\n type IReadStringFileOptions,\n} from '../../shared/index.js';\nimport { getPathElement } from '../general/index.js';\n\nimport {\n openWithoutFollowing,\n readPermissionBits,\n renameReplacing,\n resolveWriteTarget,\n syncDirectory,\n} from './utilities.js';\n\n/**\n * Checks whether a path exists and is a regular file.\n * @param path The path.\n * @returns Whether the path is a file.\n * @example\n * import { isFile } from 'jg-fs/sync';\n *\n * isFile('package.json'); // true\n */\nexport const isFile = (path: string): boolean => getPathElement(path)?.isFile ?? false;\n\n/**\n * Writes a file, creating its directory first when it doesn't exist.\n * @param path The file's path.\n * @param data The content.\n * @param options Node.js's writeFile options, such as the encoding or mode.\n * @example\n * import { writeFile } from 'jg-fs/sync';\n *\n * const csv = 'id,name\\n1,Ada\\n';\n * writeFile('out/report.csv', csv, { encoding: 'utf-8' });\n */\nexport const writeFile = (\n path: string,\n data: string | NodeJS.ArrayBufferView,\n options?: WriteFileOptions,\n): void => {\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, data, options);\n};\n\n/**\n * Writes a UTF-8 text file. An empty string writes an empty file.\n * @param path The file's path.\n * @param data The text.\n * @example\n * import { writeTextFile } from 'jg-fs/sync';\n *\n * writeTextFile('notes/today.md', '# Today');\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a string\n */\nexport const writeTextFile = (path: string, data: string): void => {\n assertTextData(path, data);\n writeFile(path, data, { encoding: 'utf-8' });\n};\n\n/**\n * Writes a JSON file atomically, as writeFileAtomic does, so a crash or a full disk never leaves a\n * truncated file that won't parse. A symbolic link at the path keeps pointing at the file, whose\n * target is replaced. A string is written as given; anything else is stringified.\n * @param path The file's path.\n * @param data The value, or its JSON text.\n * @param space The indentation for stringified values.\n * @example\n * import { writeJsonFile } from 'jg-fs/sync';\n *\n * writeJsonFile('config.json', { port: 8080 });\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the JSON content cannot be stringified or is empty\n */\nexport const writeJsonFile = <T = Record<string, unknown>>(\n path: string,\n data: T | string,\n space: number = 2,\n): void => {\n const content = serializeJsonData(path, data, space);\n writeFileAtomic(resolveWriteTarget(path), content);\n};\n\n/**\n * Writes a binary file. An empty Buffer writes an empty file.\n * @param path The file's path.\n * @param data The content.\n * @example\n * import { writeBufferFile } from 'jg-fs/sync';\n *\n * const imageBuffer = Buffer.from('iVBORw0KGgo=', 'base64');\n * writeBufferFile('avatar.png', imageBuffer);\n * @throws\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the data isn't a Buffer\n */\nexport const writeBufferFile = (path: string, data: Buffer): void => {\n assertBufferData(path, data);\n writeFile(path, data);\n};\n\n/**\n * Reads a file as a string when an encoding is given, or as a Buffer otherwise.\n * @param path The file's path.\n * @param options Node.js's readFile options, such as the encoding.\n * @returns The content.\n * @example\n * import { readFile } from 'jg-fs/sync';\n *\n * readFile('notes.md', 'utf-8'); // a string\n * readFile('avatar.png'); // a Buffer\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport function readFile(path: string, options: IReadStringFileOptions): string;\nexport function readFile(path: string, options?: IReadBufferFileOptions): Buffer;\nexport function readFile(path: string, options?: IReadFileOptions): string | Buffer;\nexport function readFile(path: string, options: IReadFileOptions = null): string | Buffer {\n if (!isFile(path)) {\n throw notAFile(path);\n }\n return readFileSync(path, options as Parameters<typeof readFileSync>[1]);\n}\n\n/**\n * Reads a UTF-8 text file. An empty file reads as an empty string.\n * @param path The file's path.\n * @returns The text.\n * @example\n * import { readTextFile } from 'jg-fs/sync';\n *\n * const markdown = readTextFile('README.md');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const readTextFile = (path: string): string => readFile(path, { encoding: 'utf-8' });\n\n/**\n * Reads and parses a JSON file, and validates it with the schema when there is one. Without a\n * schema, the value is `unknown`, or the type you pass. With `maxBytes`, it reads the file as\n * readBoundedFile does, for a file whose size isn't yours to decide.\n * @param path The file's path.\n * @param options The schema, such as a Zod schema, whose errors propagate unchanged, and the\n * largest file accepted, in bytes.\n * @returns The parsed value.\n * @example\n * import { readJsonFile } from 'jg-fs/sync';\n *\n * const config = readJsonFile('config.json', { schema: Config }); // Config's type\n * @example\n * import { readJsonFile } from 'jg-fs/sync';\n *\n * const value = readJsonFile('data.json'); // unknown\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n * - FILE_TOO_LARGE: if the file holds more than maxBytes bytes\n * - FILE_CONTENT_IS_EMPTY_OR_INVALID: if the file's JSON content cannot be parsed\n */\nexport const readJsonFile = <T = unknown>(path: string, options: IReadJsonFileOptions<T> = {}): T =>\n parseJsonContent(\n path,\n options.maxBytes === undefined\n ? readTextFile(path)\n : readBoundedFile(path, options.maxBytes).toString('utf-8'),\n options.schema,\n );\n\n/**\n * Reads a binary file. An empty file reads as an empty Buffer.\n * @param path The file's path.\n * @returns The content.\n * @example\n * import { readBufferFile } from 'jg-fs/sync';\n *\n * const image = readBufferFile('avatar.png');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const readBufferFile = (path: string): Buffer => readFile(path, null);\n\n/**\n * Copies a file, replacing the destination when it exists.\n * @param srcPath The file to copy.\n * @param destPath Where to copy it.\n * @example\n * import { copyFile } from 'jg-fs/sync';\n *\n * copyFile('config.example.json', 'config.json');\n * @throws\n * - NOT_A_FILE: if the srcPath doesn't exist or is not a file\n */\nexport const copyFile = (srcPath: string, destPath: string): void => {\n if (!isFile(srcPath)) {\n throw notAFile(srcPath);\n }\n copyFileSync(srcPath, destPath);\n};\n\n/**\n * Deletes a file.\n * @param path The file's path.\n * @example\n * import { deleteFile } from 'jg-fs/sync';\n *\n * deleteFile('cache.json');\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or is not a file\n */\nexport const deleteFile = (path: string): void => {\n if (!isFile(path)) {\n throw notAFile(path);\n }\n unlinkSync(path);\n};\n\n/**\n * Creates a symbolic link to a file.\n * @param target The file the link points to.\n * @param path Where to create the link.\n * @example\n * import { createFileSymLink } from 'jg-fs/sync';\n *\n * createFileSymLink('config.production.json', 'config.json');\n * @throws\n * - NON_EXISTENT_FILE: if the target file does not exist\n * - NOT_A_FILE: if the target is not a file\n */\nexport const createFileSymLink = (target: string, path: string): void => {\n const element = getPathElement(target);\n if (element === null) {\n throw new Exception(`The target file '${target}' does not exist.`, {\n code: ERRORS.NON_EXISTENT_FILE,\n });\n }\n if (!element.isFile) {\n throw notAFile(target);\n }\n symlinkSync(target, path, 'file');\n};\n\n/**\n * Writes a file atomically: readers see either the previous content or the complete new content,\n * never a partial write. The data goes to a temporary file in the same directory, which is flushed\n * to disk and renamed over the path; on POSIX, the directory is flushed too. An explicit mode wins;\n * without one, a replaced file keeps its permission bits and a new file gets the platform default.\n * The temporary file is never more widely readable than the final mode. When writing fails before\n * the rename, the temporary file is removed and the destination is untouched. While Windows refuses\n * the rename because another process, such as an antivirus scanner, holds the file, it tries again\n * for about 4 seconds. A symbolic link at the path is replaced by the file.\n * @param path The file's path.\n * @param data The content.\n * @param mode The permission bits of the written file, such as `0o600`.\n * @example\n * import { writeFileAtomic } from 'jg-fs/sync';\n *\n * const state = { step: 2 };\n * writeFileAtomic('state.json', JSON.stringify(state)); // readers never see a partial file\n * writeFileAtomic('secrets.env', 'TOKEN=abc\\n', 0o600); // only the owner can read it\n */\nexport const writeFileAtomic = (\n path: string,\n data: string | NodeJS.ArrayBufferView,\n mode?: number,\n): void => {\n const directory = dirname(path);\n mkdirSync(directory, { recursive: true });\n const finalMode = mode ?? readPermissionBits(path);\n const tempPath = join(directory, `.${randomUUID()}.tmp`);\n let isRenamed = false;\n try {\n // a known final mode is applied with chmod, so the umask can't change it\n const descriptor = openSync(tempPath, 'wx', finalMode === undefined ? 0o666 : 0o600);\n try {\n if (finalMode !== undefined) {\n fchmodSync(descriptor, finalMode);\n }\n writeFileSync(descriptor, data);\n fsyncSync(descriptor);\n } finally {\n closeSync(descriptor);\n }\n renameReplacing(tempPath, path);\n isRenamed = true;\n } finally {\n if (!isRenamed) {\n rmSync(tempPath, { force: true });\n }\n }\n if (process.platform !== 'win32') {\n syncDirectory(directory);\n }\n};\n\n/**\n * Reads a regular file of at most `maxBytes` bytes. It never follows a symbolic link in the\n * path's last component, though links in parent directories are followed, so callers that need\n * containment combine it with `isPathWithin`. It reads at most `maxBytes + 1` bytes, even when the\n * file grows while it's read.\n * @param path The file's path.\n * @param maxBytes The largest size accepted, in bytes.\n * @returns The content.\n * @example\n * import { join } from 'node:path';\n * import { readBoundedFile } from 'jg-fs/sync';\n *\n * const upload = readBoundedFile(join('uploads', 'avatar.png'), 5 * 1024 * 1024); // at most 5 MB\n * @throws\n * - NOT_A_FILE: if the path doesn't exist, its final component is a symbolic link, or it isn't a regular file\n * - FILE_TOO_LARGE: if the file holds more than maxBytes bytes\n */\nexport const readBoundedFile = (path: string, maxBytes: number): Buffer => {\n const descriptor = openWithoutFollowing(path);\n try {\n const stats = fstatSync(descriptor);\n if (!stats.isFile()) {\n throw notAFile(path);\n }\n if (stats.size > maxBytes) {\n throw fileTooLarge(path, maxBytes);\n }\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n while (totalBytes <= maxBytes) {\n const chunk = Buffer.allocUnsafe(\n Math.min(BOUNDED_READ_CHUNK_SIZE, maxBytes + 1 - totalBytes),\n );\n const bytesRead = readSync(descriptor, chunk, 0, chunk.length, totalBytes);\n if (bytesRead === 0) {\n break;\n }\n chunks.push(chunk.subarray(0, bytesRead));\n totalBytes += bytesRead;\n }\n if (totalBytes > maxBytes) {\n throw fileTooLarge(path, maxBytes);\n }\n return Buffer.concat(chunks, totalBytes);\n } finally {\n closeSync(descriptor);\n }\n};\n"]}
|
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
* @returns The bits, or `undefined` when the file doesn't exist.
|
|
5
5
|
*/
|
|
6
6
|
export declare const readPermissionBits: (path: string) => number | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Renames a file over another, trying again for about 4 seconds while Windows refuses because
|
|
9
|
+
* another process, such as an antivirus scanner, holds the destination. It blocks between
|
|
10
|
+
* attempts, as every sync function does.
|
|
11
|
+
* @param from The file to rename.
|
|
12
|
+
* @param to Its new path.
|
|
13
|
+
*/
|
|
14
|
+
export declare const renameReplacing: (from: string, to: string) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the file a write replaces: the target of a symbolic link at the path, so the link keeps
|
|
17
|
+
* pointing at the new content, or the path itself when nothing is there yet.
|
|
18
|
+
* @param path The path.
|
|
19
|
+
* @returns The path to write.
|
|
20
|
+
*/
|
|
21
|
+
export declare const resolveWriteTarget: (path: string) => string;
|
|
7
22
|
/**
|
|
8
23
|
* Flushes a directory's entries to disk, so a rename inside it survives a crash.
|
|
9
24
|
* @param path The directory's path.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { closeSync, constants, fsyncSync, lstatSync, openSync, statSync } from 'node:fs';
|
|
2
|
-
import { isNoFollowError, isNotFoundError, notARegularFile } from '../../shared/index.js';
|
|
1
|
+
import { closeSync, constants, fsyncSync, lstatSync, openSync, realpathSync, renameSync, statSync, } from 'node:fs';
|
|
2
|
+
import { isBusyRenameError, isNoFollowError, isNotFoundError, notARegularFile, RENAME_RETRY_DELAYS_MS, } from '../../shared/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Reads the permission bits of an existing file.
|
|
5
5
|
* @param path The file's path.
|
|
@@ -16,6 +16,45 @@ export const readPermissionBits = (path) => {
|
|
|
16
16
|
throw error;
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Renames a file over another, trying again for about 4 seconds while Windows refuses because
|
|
21
|
+
* another process, such as an antivirus scanner, holds the destination. It blocks between
|
|
22
|
+
* attempts, as every sync function does.
|
|
23
|
+
* @param from The file to rename.
|
|
24
|
+
* @param to Its new path.
|
|
25
|
+
*/
|
|
26
|
+
export const renameReplacing = (from, to) => {
|
|
27
|
+
for (let attempt = 0;; attempt += 1) {
|
|
28
|
+
try {
|
|
29
|
+
renameSync(from, to);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const delay = RENAME_RETRY_DELAYS_MS[attempt];
|
|
34
|
+
if (delay === undefined || !isBusyRenameError(error)) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the file a write replaces: the target of a symbolic link at the path, so the link keeps
|
|
43
|
+
* pointing at the new content, or the path itself when nothing is there yet.
|
|
44
|
+
* @param path The path.
|
|
45
|
+
* @returns The path to write.
|
|
46
|
+
*/
|
|
47
|
+
export const resolveWriteTarget = (path) => {
|
|
48
|
+
try {
|
|
49
|
+
return realpathSync(path);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (isNotFoundError(error)) {
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
19
58
|
/**
|
|
20
59
|
* Flushes a directory's entries to disk, so a rename inside it survives a crash.
|
|
21
60
|
* @param path The directory's path.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.js","sourceRoot":"","sources":["../../../src/sync/files/utilities.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"utilities.js","sourceRoot":"","sources":["../../../src/sync/files/utilities.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n closeSync,\n constants,\n fsyncSync,\n lstatSync,\n openSync,\n realpathSync,\n renameSync,\n statSync,\n} from 'node:fs';\n\nimport {\n isBusyRenameError,\n isNoFollowError,\n isNotFoundError,\n notARegularFile,\n RENAME_RETRY_DELAYS_MS,\n} from '../../shared/index.js';\n\n/**\n * Reads the permission bits of an existing file.\n * @param path The file's path.\n * @returns The bits, or `undefined` when the file doesn't exist.\n */\nexport const readPermissionBits = (path: string): number | undefined => {\n try {\n return statSync(path).mode & 0o777;\n } catch (error) {\n if (isNotFoundError(error)) {\n return undefined;\n }\n throw error;\n }\n};\n\n/**\n * Renames a file over another, trying again for about 4 seconds while Windows refuses because\n * another process, such as an antivirus scanner, holds the destination. It blocks between\n * attempts, as every sync function does.\n * @param from The file to rename.\n * @param to Its new path.\n */\nexport const renameReplacing = (from: string, to: string): void => {\n for (let attempt = 0; ; attempt += 1) {\n try {\n renameSync(from, to);\n return;\n } catch (error) {\n const delay = RENAME_RETRY_DELAYS_MS[attempt];\n if (delay === undefined || !isBusyRenameError(error)) {\n throw error;\n }\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);\n }\n }\n};\n\n/**\n * Resolves the file a write replaces: the target of a symbolic link at the path, so the link keeps\n * pointing at the new content, or the path itself when nothing is there yet.\n * @param path The path.\n * @returns The path to write.\n */\nexport const resolveWriteTarget = (path: string): string => {\n try {\n return realpathSync(path);\n } catch (error) {\n if (isNotFoundError(error)) {\n return path;\n }\n throw error;\n }\n};\n\n/**\n * Flushes a directory's entries to disk, so a rename inside it survives a crash.\n * @param path The directory's path.\n */\nexport const syncDirectory = (path: string): void => {\n const descriptor = openSync(path, 'r');\n try {\n fsyncSync(descriptor);\n } finally {\n closeSync(descriptor);\n }\n};\n\n/**\n * Opens a file for reading without following a symbolic link in its last component.\n * @param path The file's path.\n * @returns The file descriptor.\n * @throws\n * - NOT_A_FILE: if the path doesn't exist or its final component is a symbolic link\n */\nexport const openWithoutFollowing = (path: string): number => {\n try {\n if (process.platform === 'win32') {\n // Windows has no O_NOFOLLOW, so a link swapped in after this check isn't detected\n if (lstatSync(path).isSymbolicLink()) {\n throw Object.assign(new Error('The path is a symbolic link.'), { code: 'ELOOP' });\n }\n return openSync(path, constants.O_RDONLY);\n }\n // O_NONBLOCK keeps a named pipe at the path from blocking the open\n return openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);\n } catch (error) {\n if (isNoFollowError(error)) {\n throw notARegularFile(path, error);\n }\n throw error;\n }\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jg-fs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Filesystem helpers for Node.js, async at the root and sync at jg-fs/sync: paths, directories, text, JSON, atomic and bounded files.",
|
|
5
5
|
"author": "Jesus Graterol",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
22
|
"CHANGELOG.md",
|
|
23
|
-
"API.md"
|
|
23
|
+
"API.md",
|
|
24
|
+
"RECIPES.md"
|
|
24
25
|
],
|
|
25
26
|
"sideEffects": false,
|
|
26
27
|
"engines": {
|
|
@@ -48,8 +49,8 @@
|
|
|
48
49
|
"node"
|
|
49
50
|
],
|
|
50
51
|
"dependencies": {
|
|
51
|
-
"jg-exception": "^1.
|
|
52
|
-
"jg-utilities": "^1.1.
|
|
52
|
+
"jg-exception": "^1.2.0",
|
|
53
|
+
"jg-utilities": "^1.1.1"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@jg-packages/eslint-config": "0.0.0",
|