@procedures/fs 0.2.3 → 0.4.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/CHANGELOG.md +66 -0
- package/README.md +76 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/handlers.d.ts +40 -0
- package/dist/handlers.d.ts.map +1 -0
- package/dist/handlers.js +163 -0
- package/dist/handlers.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +23 -2
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +95 -4
- package/dist/module.js.map +1 -1
- package/dist/procedures/fs/copy.d.ts +6 -1
- package/dist/procedures/fs/copy.d.ts.map +1 -1
- package/dist/procedures/fs/copy.js +28 -5
- package/dist/procedures/fs/copy.js.map +1 -1
- package/dist/procedures/fs/hash.d.ts +12 -0
- package/dist/procedures/fs/hash.d.ts.map +1 -0
- package/dist/procedures/fs/hash.js +23 -0
- package/dist/procedures/fs/hash.js.map +1 -0
- package/dist/procedures/fs/index.d.ts +4 -0
- package/dist/procedures/fs/index.d.ts.map +1 -1
- package/dist/procedures/fs/index.js +4 -0
- package/dist/procedures/fs/index.js.map +1 -1
- package/dist/procedures/fs/temp.d.ts +14 -0
- package/dist/procedures/fs/temp.d.ts.map +1 -0
- package/dist/procedures/fs/temp.js +20 -0
- package/dist/procedures/fs/temp.js.map +1 -0
- package/dist/procedures/fs/watch.d.ts +14 -0
- package/dist/procedures/fs/watch.d.ts.map +1 -0
- package/dist/procedures/fs/watch.js +26 -0
- package/dist/procedures/fs/watch.js.map +1 -0
- package/dist/procedures/fs/write-json.d.ts +12 -0
- package/dist/procedures/fs/write-json.d.ts.map +1 -0
- package/dist/procedures/fs/write-json.js +33 -0
- package/dist/procedures/fs/write-json.js.map +1 -0
- package/dist/root.d.ts +52 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +129 -0
- package/dist/root.js.map +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +43 -2
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/handlers.ts +214 -0
- package/src/index.ts +2 -0
- package/src/module.ts +128 -5
- package/src/procedures/fs/copy.ts +28 -5
- package/src/procedures/fs/hash.ts +25 -0
- package/src/procedures/fs/index.ts +4 -0
- package/src/procedures/fs/temp.ts +22 -0
- package/src/procedures/fs/watch.ts +26 -0
- package/src/procedures/fs/write-json.ts +32 -0
- package/src/root.ts +139 -0
- package/src/types.ts +112 -2
package/src/root.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root of the file procedures: one folder that confines each path (roadmap R33).
|
|
3
|
+
*
|
|
4
|
+
* `createFsModule({ root })` sends each path of an input through `inRoot()`:
|
|
5
|
+
* - A relative path starts at the root, not at the working folder of the process.
|
|
6
|
+
* - A path outside the root gives `FORBIDDEN` with the reason `outside-root`. This includes a
|
|
7
|
+
* path that goes up with `..`, an absolute path outside the root, a path on another drive, and
|
|
8
|
+
* a symlink or a junction that leads out of the root.
|
|
9
|
+
* - The check uses the real path (`fs.realpath`) of the path. For a path that does not exist, it
|
|
10
|
+
* uses the real path of the nearest existing ancestor. So a symlinked parent folder cannot
|
|
11
|
+
* lead a write or a `mkdir` out of the root.
|
|
12
|
+
* - The procedure gets the absolute path, so the paths of the output are absolute.
|
|
13
|
+
*
|
|
14
|
+
* On Windows, the comparison ignores the case of the letters, and `/` and `\` are the same.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readlink, realpath } from "node:fs/promises";
|
|
18
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
19
|
+
import { InvocationError, type ProcedurePath } from "@procedures/client";
|
|
20
|
+
|
|
21
|
+
/** The reason of the `FORBIDDEN` error for a path outside the root. */
|
|
22
|
+
export const OUTSIDE_ROOT = "outside-root";
|
|
23
|
+
|
|
24
|
+
/** The maximum number of symlinks that one check follows. After it, the check fails with `ELOOP`. */
|
|
25
|
+
const MAX_LINKS = 40;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* This function tells if `path` is `folder` or a path in `folder`. The two paths are absolute.
|
|
29
|
+
* On Windows, the case of the letters is not significant.
|
|
30
|
+
*/
|
|
31
|
+
export function isInside(folder: string, path: string): boolean {
|
|
32
|
+
const rel = relative(folder, path);
|
|
33
|
+
return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True for the error of a path that does not exist. */
|
|
37
|
+
function isMissing(error: unknown): boolean {
|
|
38
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
39
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The target of a symlink, or undefined when the path is not a symlink or does not exist. */
|
|
43
|
+
async function linkTarget(path: string): Promise<string | undefined> {
|
|
44
|
+
try {
|
|
45
|
+
return await readlink(path);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// EINVAL: the path is not a symlink
|
|
48
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
49
|
+
if (code === "EINVAL" || isMissing(error)) return undefined;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* This function gives the real path of an absolute path. For a path that does not exist, it
|
|
56
|
+
* gives the real path of the nearest existing ancestor and the rest of the path. A symlink whose
|
|
57
|
+
* target does not exist leads to its target, because a write follows it.
|
|
58
|
+
*/
|
|
59
|
+
export async function realPathOf(path: string, links = 0): Promise<string> {
|
|
60
|
+
const rest: string[] = [];
|
|
61
|
+
let current = path;
|
|
62
|
+
for (;;) {
|
|
63
|
+
try {
|
|
64
|
+
return join(await realpath(current), ...rest);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!isMissing(error)) throw error;
|
|
67
|
+
}
|
|
68
|
+
const target = await linkTarget(current);
|
|
69
|
+
if (target !== undefined) {
|
|
70
|
+
if (links >= MAX_LINKS) {
|
|
71
|
+
const loop = new Error(`Too many symlinks in the path ${path}`) as NodeJS.ErrnoException;
|
|
72
|
+
loop.code = "ELOOP";
|
|
73
|
+
throw loop;
|
|
74
|
+
}
|
|
75
|
+
return join(await realPathOf(resolve(dirname(current), target), links + 1), ...rest);
|
|
76
|
+
}
|
|
77
|
+
const parent = dirname(current);
|
|
78
|
+
// No ancestor exists, for example a drive that is not there
|
|
79
|
+
if (parent === current) return join(current, ...rest);
|
|
80
|
+
rest.unshift(basename(current));
|
|
81
|
+
current = parent;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A root: the absolute path that the host gave, and its real path. */
|
|
86
|
+
export interface ResolvedRoot {
|
|
87
|
+
folder: string;
|
|
88
|
+
real: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** This function gives the absolute path and the real path of a root. The root must exist. */
|
|
92
|
+
export async function resolveRoot(root: string): Promise<ResolvedRoot> {
|
|
93
|
+
const folder = resolve(root);
|
|
94
|
+
try {
|
|
95
|
+
return { folder, real: await realpath(folder) };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (!isMissing(error)) throw error;
|
|
98
|
+
const missing = new Error(`The root of the file procedures does not exist: ${folder}`) as NodeJS.ErrnoException;
|
|
99
|
+
missing.code = "ENOENT";
|
|
100
|
+
throw missing;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The `FORBIDDEN` error for a path outside the root. */
|
|
105
|
+
export function outsideRoot(procedure: ProcedurePath, message: string): InvocationError {
|
|
106
|
+
return new InvocationError("FORBIDDEN", `${procedure.join(".")}: ${message}`, procedure, OUTSIDE_ROOT);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** True for the `FORBIDDEN` error of a path outside the root. */
|
|
110
|
+
export function isOutsideRoot(error: unknown): boolean {
|
|
111
|
+
return error instanceof InvocationError && error.code === "FORBIDDEN" && error.reason === OUTSIDE_ROOT;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* This function tells if the real path of an absolute path is in the root. It follows each
|
|
116
|
+
* symlink of the path, also a symlink whose target does not exist.
|
|
117
|
+
*/
|
|
118
|
+
export async function realPathIsInside(root: ResolvedRoot, absolute: string): Promise<boolean> {
|
|
119
|
+
return isInside(root.real, await realPathOf(absolute));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* This function gives the absolute path of `path` in the root. A relative path starts at the
|
|
124
|
+
* root. The real path decides: a path whose real path is outside the real path of the root gives
|
|
125
|
+
* `FORBIDDEN` with the reason `outside-root`.
|
|
126
|
+
*/
|
|
127
|
+
export async function inRoot(root: ResolvedRoot, path: string, procedure: ProcedurePath): Promise<string> {
|
|
128
|
+
const absolute = resolve(root.folder, path);
|
|
129
|
+
const real = await realPathOf(absolute);
|
|
130
|
+
// A path counts as inside by its given form or by its real form. On Windows, the two forms of a
|
|
131
|
+
// folder can differ by more than the links: a short 8.3 name (RUNNER~1) has a long real name.
|
|
132
|
+
if (!isInside(root.folder, absolute) && !isInside(root.real, absolute) && !isInside(root.real, real)) {
|
|
133
|
+
throw outsideRoot(procedure, `the path ${JSON.stringify(path)} is outside the root ${root.folder}`);
|
|
134
|
+
}
|
|
135
|
+
if (!isInside(root.real, real)) {
|
|
136
|
+
throw outsideRoot(procedure, `the path ${JSON.stringify(path)} leads out of the root ${root.folder} through a symlink`);
|
|
137
|
+
}
|
|
138
|
+
return absolute;
|
|
139
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -229,8 +229,12 @@ export const CopyInputSchema: z.ZodObject<{
|
|
|
229
229
|
dest: z.string().describe("Destination path"),
|
|
230
230
|
/** Copy directories recursively (default: true) */
|
|
231
231
|
recursive: z.boolean().optional().default(true).describe("Copy directories recursively (default: true)"),
|
|
232
|
-
/**
|
|
233
|
-
overwrite: z
|
|
232
|
+
/** Replace an existing destination (default: false). Without it, an existing destination is an error. */
|
|
233
|
+
overwrite: z
|
|
234
|
+
.boolean()
|
|
235
|
+
.optional()
|
|
236
|
+
.default(false)
|
|
237
|
+
.describe("Replace an existing destination (default: false). Without it, an existing destination is an error (EEXIST), and the copy changes nothing."),
|
|
234
238
|
});
|
|
235
239
|
|
|
236
240
|
export type CopyInput = z.infer<typeof CopyInputSchema>;
|
|
@@ -405,3 +409,109 @@ export const ReadJsonOutputSchema: z.ZodType<ReadJsonOutput> = z.looseObject({
|
|
|
405
409
|
data: z.unknown().describe("The parsed JSON value"),
|
|
406
410
|
stats: StatOutputSchema.describe("The stats of the file"),
|
|
407
411
|
});
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
// =============================================================================
|
|
415
|
+
// fs.write.json, fs.hash, fs.temp and fs.watch (roadmap R44)
|
|
416
|
+
// =============================================================================
|
|
417
|
+
|
|
418
|
+
export interface FsWriteJsonInput {
|
|
419
|
+
/** The path of the file */
|
|
420
|
+
path: string;
|
|
421
|
+
/** The value to write (required: null writes null) */
|
|
422
|
+
value: unknown;
|
|
423
|
+
/** The spaces of the indent (default 2) */
|
|
424
|
+
indent?: number | undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export interface FsWriteJsonOutput {
|
|
428
|
+
path: string;
|
|
429
|
+
bytesWritten: number;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export const FsWriteJsonInputSchema: z.ZodType<FsWriteJsonInput> = z.object({
|
|
433
|
+
path: z.string().describe("The path of the file"),
|
|
434
|
+
value: z.unknown().describe("The value to write as JSON"),
|
|
435
|
+
indent: z.number().int().min(0).max(10).optional().describe("The spaces of the indent (default 2)"),
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
export const FsWriteJsonOutputSchema: z.ZodType<FsWriteJsonOutput> = z.looseObject({
|
|
439
|
+
path: z.string().describe("The path of the file"),
|
|
440
|
+
bytesWritten: z.number().describe("The bytes of the file"),
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
export type HashAlgorithm = "sha256" | "sha512" | "sha1" | "md5";
|
|
444
|
+
|
|
445
|
+
export interface FsHashInput {
|
|
446
|
+
path: string;
|
|
447
|
+
/** The algorithm (default sha256) */
|
|
448
|
+
algorithm?: HashAlgorithm | undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export interface FsHashOutput {
|
|
452
|
+
path: string;
|
|
453
|
+
algorithm: HashAlgorithm;
|
|
454
|
+
/** The hash, as hexadecimal text */
|
|
455
|
+
hash: string;
|
|
456
|
+
/** The bytes of the file */
|
|
457
|
+
size: number;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export const FsHashInputSchema: z.ZodType<FsHashInput> = z.object({
|
|
461
|
+
path: z.string().describe("The path of the file"),
|
|
462
|
+
algorithm: z.enum(["sha256", "sha512", "sha1", "md5"]).optional().describe("The algorithm (default sha256)"),
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
export const FsHashOutputSchema: z.ZodType<FsHashOutput> = z.looseObject({
|
|
466
|
+
path: z.string().describe("The path of the file"),
|
|
467
|
+
algorithm: z.enum(["sha256", "sha512", "sha1", "md5"]).describe("The algorithm"),
|
|
468
|
+
hash: z.string().describe("The hash, as hexadecimal text"),
|
|
469
|
+
size: z.number().describe("The bytes of the file"),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
export interface FsTempInput {
|
|
473
|
+
/** The start of the name of the folder (default "procedures-") */
|
|
474
|
+
prefix?: string | undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export interface FsTempOutput {
|
|
478
|
+
/** The path of the new folder */
|
|
479
|
+
path: string;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export const FsTempInputSchema: z.ZodType<FsTempInput> = z.object({
|
|
483
|
+
prefix: z
|
|
484
|
+
.string()
|
|
485
|
+
.regex(/^[A-Za-z0-9._-]{0,40}$/, "letters, digits, dot, underscore and hyphen only")
|
|
486
|
+
.optional()
|
|
487
|
+
.describe('The start of the name of the folder (default "procedures-")'),
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
export const FsTempOutputSchema: z.ZodType<FsTempOutput> = z.looseObject({
|
|
491
|
+
path: z.string().describe("The path of the new folder"),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
export interface FsWatchInput {
|
|
495
|
+
/** The file or folder to watch */
|
|
496
|
+
path: string;
|
|
497
|
+
/** Also the changes in the subfolders */
|
|
498
|
+
recursive?: boolean | undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** One change: an item of the stream of fs.watch. */
|
|
502
|
+
export interface FsWatchOutput {
|
|
503
|
+
/** "rename" for a file that appears or goes away, "change" for a change of its content */
|
|
504
|
+
event: "rename" | "change";
|
|
505
|
+
/** The name of the file, relative to the path, when the platform gives it */
|
|
506
|
+
filename: string | null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export const FsWatchInputSchema: z.ZodType<FsWatchInput> = z.object({
|
|
510
|
+
path: z.string().describe("The file or folder to watch"),
|
|
511
|
+
recursive: z.boolean().optional().describe("Also the changes in the subfolders"),
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
export const FsWatchOutputSchema: z.ZodType<FsWatchOutput> = z.looseObject({
|
|
515
|
+
event: z.enum(["rename", "change"]).describe('"rename" for a file that appears or goes away, "change" for its content'),
|
|
516
|
+
filename: z.string().nullable().describe("The name of the file, relative to the path, when the platform gives it"),
|
|
517
|
+
});
|