@statewalker/webrun-files-node 0.10.0 → 0.10.3
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/README.md +72 -121
- package/dist/cjs/index.cjs +10 -6
- package/dist/esm/index.js +9 -6
- package/dist/node-files-api.d.ts +3 -2
- package/dist/node-files-api.d.ts.map +1 -1
- package/package.json +13 -8
- package/src/node-files-api.ts +13 -8
package/README.md
CHANGED
|
@@ -1,167 +1,118 @@
|
|
|
1
1
|
# @statewalker/webrun-files-node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## What it is
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`NodeFilesApi`, an implementation of `FilesApi` from `@statewalker/webrun-files` over Node.js
|
|
6
|
+
`fs/promises`. Virtual paths (`/users/alice.json`) are mapped to real files under a root directory.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Why it exists
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Server code, CLIs and tests that work against `FilesApi` need a backend that stores real files on
|
|
11
|
+
disk. With this package the same code that runs on `MemFilesApi` in tests, or on S3 in production,
|
|
12
|
+
reads and writes a local directory.
|
|
13
|
+
|
|
14
|
+
## How to use
|
|
10
15
|
|
|
11
16
|
```bash
|
|
12
|
-
|
|
17
|
+
pnpm add @statewalker/webrun-files-node @statewalker/webrun-files
|
|
13
18
|
```
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
One entry point, `@statewalker/webrun-files-node`: ESM (`dist/esm/index.js`), CommonJS
|
|
21
|
+
(`dist/cjs/index.cjs`), types (`dist/index.d.ts`); sources in `src/`. Node.js only (it imports
|
|
22
|
+
`node:fs/promises`).
|
|
18
23
|
|
|
19
24
|
```typescript
|
|
20
|
-
import { NodeFilesApi } from '@statewalker/webrun-files-node';
|
|
21
|
-
import { readText, writeText } from '@statewalker/webrun-files';
|
|
22
|
-
|
|
23
|
-
// Create API rooted at a specific directory
|
|
24
|
-
const files = new NodeFilesApi({ rootDir: '/var/app/data' });
|
|
25
|
+
import { NodeFilesApi, type NodeFilesApiOptions } from '@statewalker/webrun-files-node';
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Read it back
|
|
30
|
-
const content = await readText(files, '/config.json');
|
|
31
|
-
console.log(content); // {"debug": true}
|
|
27
|
+
const files = new NodeFilesApi({ rootDir: '/var/app/data' }); // rootDir defaults to process.cwd()
|
|
28
|
+
```
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} else {
|
|
38
|
-
console.log(entry.name, entry.kind); // directories carry no size or time
|
|
39
|
-
}
|
|
40
|
-
}
|
|
30
|
+
```
|
|
31
|
+
rootDir: /var/app/data
|
|
32
|
+
virtual path: /users/alice.json
|
|
33
|
+
real path: /var/app/data/users/alice.json
|
|
41
34
|
```
|
|
42
35
|
|
|
43
|
-
|
|
36
|
+
## Examples
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
### Read, write, list
|
|
46
39
|
|
|
47
40
|
```typescript
|
|
48
41
|
import { NodeFilesApi } from '@statewalker/webrun-files-node';
|
|
42
|
+
import { readText, writeText } from '@statewalker/webrun-files';
|
|
49
43
|
|
|
50
|
-
const files = new NodeFilesApi();
|
|
51
|
-
// Virtual path /data/file.txt maps to ./data/file.txt
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Path Mapping
|
|
55
|
-
|
|
56
|
-
Virtual paths are mapped directly to the filesystem:
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
rootDir: "/var/app/data"
|
|
60
|
-
virtual path: "/users/alice.json"
|
|
61
|
-
real path: "/var/app/data/users/alice.json"
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Paths are appended to `rootDir` without confinement: `..` segments are kept, not resolved, so a
|
|
65
|
-
virtual path such as `/../etc/passwd` reaches outside `rootDir`. Do not pass untrusted paths.
|
|
66
|
-
|
|
67
|
-
## API Reference
|
|
68
|
-
|
|
69
|
-
### NodeFilesApi
|
|
44
|
+
const files = new NodeFilesApi({ rootDir: '/var/app/data' });
|
|
70
45
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Root directory for file operations.
|
|
75
|
-
* Virtual paths are appended to this directory (see "Path Mapping" for `..`).
|
|
76
|
-
* Defaults to current working directory if not specified.
|
|
77
|
-
*/
|
|
78
|
-
rootDir?: string;
|
|
79
|
-
}
|
|
46
|
+
await writeText(files, '/deep/nested/config.json', '{"debug": true}'); // parents are created
|
|
47
|
+
console.log(await readText(files, '/deep/nested/config.json'));
|
|
80
48
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// All FilesApi methods
|
|
85
|
-
read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
|
|
86
|
-
write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
|
|
87
|
-
mkdir(path: string): Promise<void>;
|
|
88
|
-
list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
|
|
89
|
-
stats(path: string): Promise<FileStats | undefined>;
|
|
90
|
-
exists(path: string): Promise<boolean>;
|
|
91
|
-
remove(path: string): Promise<boolean>;
|
|
92
|
-
move(source: string, target: string): Promise<boolean>;
|
|
93
|
-
copy(source: string, target: string): Promise<boolean>;
|
|
49
|
+
for await (const entry of files.list('/', { recursive: true })) {
|
|
50
|
+
console.log(entry.path, entry.kind, entry.kind === 'file' ? entry.size : '');
|
|
94
51
|
}
|
|
95
52
|
```
|
|
96
53
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
### Efficient Streaming
|
|
100
|
-
|
|
101
|
-
Large files are read in chunks without loading entirely into memory:
|
|
54
|
+
### Stream a range
|
|
102
55
|
|
|
103
56
|
```typescript
|
|
104
|
-
// Stream a large file
|
|
105
|
-
for await (const chunk of files.read('/large-video.mp4')) {
|
|
106
|
-
// Process each chunk (default 8KB chunks)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Read a specific range
|
|
110
57
|
for await (const chunk of files.read('/large-file.bin', { start: 1000, length: 500 })) {
|
|
111
|
-
//
|
|
58
|
+
// bytes 1000-1499, in chunks of at most 8 KiB
|
|
112
59
|
}
|
|
113
60
|
```
|
|
114
61
|
|
|
115
|
-
###
|
|
116
|
-
|
|
117
|
-
Parent directories are created automatically when writing files:
|
|
62
|
+
### A temporary directory per test
|
|
118
63
|
|
|
119
64
|
```typescript
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
65
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
66
|
+
import { tmpdir } from 'node:os';
|
|
67
|
+
import { join } from 'node:path';
|
|
68
|
+
import { NodeFilesApi } from '@statewalker/webrun-files-node';
|
|
123
69
|
|
|
124
|
-
|
|
70
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'test-'));
|
|
71
|
+
const files = new NodeFilesApi({ rootDir });
|
|
72
|
+
try {
|
|
73
|
+
// ...
|
|
74
|
+
} finally {
|
|
75
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
```
|
|
125
78
|
|
|
126
|
-
|
|
127
|
-
and yields them in path order (as every `FilesApi` does). A recursive listing reads a directory only
|
|
128
|
-
when the listing reaches it, and with `{ after }` skips any directory whose whole subtree sorts at
|
|
129
|
-
or before the cursor.
|
|
79
|
+
## Internals
|
|
130
80
|
|
|
131
|
-
###
|
|
81
|
+
### How each call maps to `fs`
|
|
132
82
|
|
|
133
|
-
|
|
134
|
-
|
|
83
|
+
| Call | Implementation |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `read` | `fs.open` + positioned `handle.read` in 8 KiB buffers, only the requested range |
|
|
86
|
+
| `write` | `fs.mkdir(parent, { recursive: true })`, then collects every chunk and calls `fs.writeFile` once |
|
|
87
|
+
| `list` | `fs.readdir` + `fs.stat` per entry, walked through `listInPathOrder` |
|
|
88
|
+
| `move` | `fs.rename` after creating the target's parent |
|
|
89
|
+
| `copy` | `fs.cp(..., { recursive: true })` |
|
|
90
|
+
| `remove` | `fs.rm(..., { recursive: true, force: true })` for a directory, `fs.unlink` for a file |
|
|
135
91
|
|
|
136
|
-
|
|
137
|
-
// Copy entire directory tree
|
|
138
|
-
await files.copy('/source', '/destination');
|
|
92
|
+
### Why listings stat every entry
|
|
139
93
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
94
|
+
`readdir` returns names in no guaranteed order, and every `FilesApi` must list in `comparePaths`
|
|
95
|
+
order with `after` support. `list()` therefore reads each directory, stats its entries and yields
|
|
96
|
+
them sorted. A recursive listing reads a directory only when the listing reaches it, and skips any
|
|
97
|
+
directory whose whole subtree sorts at or before `after`.
|
|
143
98
|
|
|
144
|
-
|
|
99
|
+
### What breaks, and how it looks
|
|
145
100
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
101
|
+
- **A path that leaves `rootDir` is refused.** `..` segments are resolved against `rootDir`; when
|
|
102
|
+
the result is outside it, the call throws
|
|
103
|
+
`NodeFilesApi: path is outside rootDir: /../etc/passwd` (reads throw when iterated). `..` that
|
|
104
|
+
stays inside (`/a/../b.txt`) is fine. Symbolic links inside `rootDir` are followed as the
|
|
105
|
+
operating system follows them, so a link pointing out of `rootDir` still leads out.
|
|
106
|
+
- **`write` holds the whole file in memory** before writing it, so a very large upload costs its
|
|
107
|
+
full size in RAM. Reads stream.
|
|
108
|
+
- **`move` across devices returns `false`.** It is a single `fs.rename`; when that fails (`EXDEV`,
|
|
109
|
+
permissions) the method returns `false` instead of falling back to copy and delete.
|
|
110
|
+
- **Unreadable reads look like missing files.** `read` swallows every error (missing file,
|
|
111
|
+
permission denied, a directory) and yields nothing; `copy` and `move` return `false`.
|
|
152
112
|
|
|
153
|
-
|
|
154
|
-
const tempDir = await mkdtemp(join(tmpdir(), 'test-'));
|
|
155
|
-
const files = new NodeFilesApi({ rootDir: tempDir });
|
|
113
|
+
### Dependencies
|
|
156
114
|
|
|
157
|
-
|
|
158
|
-
// Run tests...
|
|
159
|
-
await writeText(files, '/test.txt', 'test content');
|
|
160
|
-
} finally {
|
|
161
|
-
// Cleanup
|
|
162
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
163
|
-
}
|
|
164
|
-
```
|
|
115
|
+
`@statewalker/webrun-files` for the types and path helpers; Node.js built-ins otherwise.
|
|
165
116
|
|
|
166
117
|
## License
|
|
167
118
|
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -23,6 +23,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
//#endregion
|
|
24
24
|
let node_fs_promises = require("node:fs/promises");
|
|
25
25
|
node_fs_promises = __toESM(node_fs_promises, 1);
|
|
26
|
+
let node_path = require("node:path");
|
|
27
|
+
node_path = __toESM(node_path, 1);
|
|
26
28
|
//#region ../webrun-files/dist/esm/index.js
|
|
27
29
|
/**
|
|
28
30
|
* The order every `FilesApi.list()` yields: paths compared by Unicode code
|
|
@@ -147,11 +149,13 @@ function dirname(path) {
|
|
|
147
149
|
var NodeFilesApi = class {
|
|
148
150
|
rootDir;
|
|
149
151
|
constructor(options) {
|
|
150
|
-
this.rootDir = options?.rootDir ?? process.cwd();
|
|
152
|
+
this.rootDir = node_path.resolve(options?.rootDir ?? process.cwd());
|
|
151
153
|
}
|
|
154
|
+
/** The real path of a virtual one; throws when `..` segments would take it outside rootDir. */
|
|
152
155
|
resolvePath(virtualPath) {
|
|
153
|
-
const
|
|
154
|
-
|
|
156
|
+
const real = node_path.resolve(this.rootDir, `.${normalizePath(virtualPath)}`);
|
|
157
|
+
if (real !== this.rootDir && !real.startsWith(this.rootDir + node_path.sep)) throw new Error(`NodeFilesApi: path is outside rootDir: ${virtualPath}`);
|
|
158
|
+
return real;
|
|
155
159
|
}
|
|
156
160
|
async *read(path, options) {
|
|
157
161
|
const realPath = this.resolvePath(path);
|
|
@@ -186,7 +190,7 @@ var NodeFilesApi = class {
|
|
|
186
190
|
async write(path, content) {
|
|
187
191
|
const realPath = this.resolvePath(path);
|
|
188
192
|
const normalized = normalizePath(path);
|
|
189
|
-
const dir = this.
|
|
193
|
+
const dir = this.resolvePath(dirname(normalized));
|
|
190
194
|
await node_fs_promises.mkdir(dir, { recursive: true });
|
|
191
195
|
const chunks = [];
|
|
192
196
|
let totalLength = 0;
|
|
@@ -287,7 +291,7 @@ var NodeFilesApi = class {
|
|
|
287
291
|
return false;
|
|
288
292
|
}
|
|
289
293
|
try {
|
|
290
|
-
const targetDir = this.
|
|
294
|
+
const targetDir = this.resolvePath(dirname(normalizedTarget));
|
|
291
295
|
await node_fs_promises.mkdir(targetDir, { recursive: true });
|
|
292
296
|
await node_fs_promises.rename(sourcePath, targetPath);
|
|
293
297
|
return true;
|
|
@@ -305,7 +309,7 @@ var NodeFilesApi = class {
|
|
|
305
309
|
return false;
|
|
306
310
|
}
|
|
307
311
|
try {
|
|
308
|
-
const targetDir = this.
|
|
312
|
+
const targetDir = this.resolvePath(dirname(normalizedTarget));
|
|
309
313
|
await node_fs_promises.mkdir(targetDir, { recursive: true });
|
|
310
314
|
await node_fs_promises.cp(sourcePath, targetPath, { recursive: true });
|
|
311
315
|
return true;
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as nodePath from "node:path";
|
|
2
3
|
//#region ../webrun-files/dist/esm/index.js
|
|
3
4
|
/**
|
|
4
5
|
* The order every `FilesApi.list()` yields: paths compared by Unicode code
|
|
@@ -123,11 +124,13 @@ function dirname(path) {
|
|
|
123
124
|
var NodeFilesApi = class {
|
|
124
125
|
rootDir;
|
|
125
126
|
constructor(options) {
|
|
126
|
-
this.rootDir = options?.rootDir ?? process.cwd();
|
|
127
|
+
this.rootDir = nodePath.resolve(options?.rootDir ?? process.cwd());
|
|
127
128
|
}
|
|
129
|
+
/** The real path of a virtual one; throws when `..` segments would take it outside rootDir. */
|
|
128
130
|
resolvePath(virtualPath) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
+
const real = nodePath.resolve(this.rootDir, `.${normalizePath(virtualPath)}`);
|
|
132
|
+
if (real !== this.rootDir && !real.startsWith(this.rootDir + nodePath.sep)) throw new Error(`NodeFilesApi: path is outside rootDir: ${virtualPath}`);
|
|
133
|
+
return real;
|
|
131
134
|
}
|
|
132
135
|
async *read(path, options) {
|
|
133
136
|
const realPath = this.resolvePath(path);
|
|
@@ -162,7 +165,7 @@ var NodeFilesApi = class {
|
|
|
162
165
|
async write(path, content) {
|
|
163
166
|
const realPath = this.resolvePath(path);
|
|
164
167
|
const normalized = normalizePath(path);
|
|
165
|
-
const dir = this.
|
|
168
|
+
const dir = this.resolvePath(dirname(normalized));
|
|
166
169
|
await fs.mkdir(dir, { recursive: true });
|
|
167
170
|
const chunks = [];
|
|
168
171
|
let totalLength = 0;
|
|
@@ -263,7 +266,7 @@ var NodeFilesApi = class {
|
|
|
263
266
|
return false;
|
|
264
267
|
}
|
|
265
268
|
try {
|
|
266
|
-
const targetDir = this.
|
|
269
|
+
const targetDir = this.resolvePath(dirname(normalizedTarget));
|
|
267
270
|
await fs.mkdir(targetDir, { recursive: true });
|
|
268
271
|
await fs.rename(sourcePath, targetPath);
|
|
269
272
|
return true;
|
|
@@ -281,7 +284,7 @@ var NodeFilesApi = class {
|
|
|
281
284
|
return false;
|
|
282
285
|
}
|
|
283
286
|
try {
|
|
284
|
-
const targetDir = this.
|
|
287
|
+
const targetDir = this.resolvePath(dirname(normalizedTarget));
|
|
285
288
|
await fs.mkdir(targetDir, { recursive: true });
|
|
286
289
|
await fs.cp(sourcePath, targetPath, { recursive: true });
|
|
287
290
|
return true;
|
package/dist/node-files-api.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { FileInfo, FileStats, FilesApi, ListOptions, ReadOptions } from "@statewalker/webrun-files";
|
|
2
2
|
export interface NodeFilesApiOptions {
|
|
3
3
|
/**
|
|
4
|
-
* Root directory for file operations. All paths are resolved relative to this
|
|
5
|
-
* Defaults to current working directory
|
|
4
|
+
* Root directory for file operations. All paths are resolved relative to this, and a path
|
|
5
|
+
* whose `..` segments would leave it is refused. Defaults to the current working directory.
|
|
6
6
|
*/
|
|
7
7
|
rootDir?: string;
|
|
8
8
|
}
|
|
@@ -13,6 +13,7 @@ export interface NodeFilesApiOptions {
|
|
|
13
13
|
export declare class NodeFilesApi implements FilesApi {
|
|
14
14
|
private rootDir;
|
|
15
15
|
constructor(options?: NodeFilesApiOptions);
|
|
16
|
+
/** The real path of a virtual one; throws when `..` segments would take it outside rootDir. */
|
|
16
17
|
private resolvePath;
|
|
17
18
|
read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
|
|
18
19
|
write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-files-api.d.ts","sourceRoot":"","sources":["../src/node-files-api.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"node-files-api.d.ts","sourceRoot":"","sources":["../src/node-files-api.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,qBAAa,YAAa,YAAW,QAAQ;IAC3C,OAAO,CAAC,OAAO,CAAS;IAExB,YAAY,OAAO,CAAC,EAAE,mBAAmB,EAExC;IAED,+FAA+F;IAC/F,OAAO,CAAC,WAAW;IAQZ,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC,CAyC1E;IAEK,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAuBf;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGvC;IAED,qFAAqF;IAC9E,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC,CAExE;IAED,mFAAmF;YACrE,YAAY;IAgCpB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAkBxD;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS3C;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAc3C;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAmB3D;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAmB3D;CACF"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-files-node",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Node.js FilesApi implementation",
|
|
7
|
-
"
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/statewalker/webrun-files/issues"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/statewalker/webrun-files/tree/main/packages/webrun-files-node#readme",
|
|
8
11
|
"author": {
|
|
9
12
|
"name": "Mikhail Kotelnikov",
|
|
10
13
|
"email": "mikhail.kotelnikov@gmail.com"
|
|
@@ -12,13 +15,15 @@
|
|
|
12
15
|
"license": "MIT",
|
|
13
16
|
"repository": {
|
|
14
17
|
"type": "git",
|
|
15
|
-
"url": "git
|
|
18
|
+
"url": "git+https://github.com/statewalker/webrun-files.git",
|
|
19
|
+
"directory": "packages/webrun-files-node"
|
|
16
20
|
},
|
|
17
21
|
"main": "./dist/cjs/index.cjs",
|
|
18
22
|
"module": "./dist/esm/index.js",
|
|
19
23
|
"types": "./dist/index.d.ts",
|
|
20
24
|
"exports": {
|
|
21
25
|
".": {
|
|
26
|
+
"source": "./src/index.ts",
|
|
22
27
|
"types": "./dist/index.d.ts",
|
|
23
28
|
"import": "./dist/esm/index.js",
|
|
24
29
|
"require": "./dist/cjs/index.cjs"
|
|
@@ -29,15 +34,15 @@
|
|
|
29
34
|
"src"
|
|
30
35
|
],
|
|
31
36
|
"dependencies": {
|
|
32
|
-
"@statewalker/webrun-files": "0.10.
|
|
37
|
+
"@statewalker/webrun-files": "^0.10.2"
|
|
33
38
|
},
|
|
34
39
|
"devDependencies": {
|
|
35
|
-
"@types/node": "^26.
|
|
40
|
+
"@types/node": "^26.6.4",
|
|
36
41
|
"rimraf": "^6.1.3",
|
|
37
|
-
"rolldown": "^1.2.
|
|
42
|
+
"rolldown": "^1.2.12",
|
|
38
43
|
"typescript": "^7.0.2",
|
|
39
|
-
"vitest": "^
|
|
40
|
-
"@statewalker/webrun-files-tests": "3.0.0"
|
|
44
|
+
"vitest": "^5.0.3",
|
|
45
|
+
"@statewalker/webrun-files-tests": "^3.0.0"
|
|
41
46
|
},
|
|
42
47
|
"sideEffects": false,
|
|
43
48
|
"publishConfig": {
|
package/src/node-files-api.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Dirent, Stats } from "node:fs";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as nodePath from "node:path";
|
|
3
4
|
import type {
|
|
4
5
|
FileInfo,
|
|
5
6
|
FileStats,
|
|
@@ -11,8 +12,8 @@ import { dirname, joinPath, listInPathOrder, normalizePath } from "@statewalker/
|
|
|
11
12
|
|
|
12
13
|
export interface NodeFilesApiOptions {
|
|
13
14
|
/**
|
|
14
|
-
* Root directory for file operations. All paths are resolved relative to this
|
|
15
|
-
* Defaults to current working directory
|
|
15
|
+
* Root directory for file operations. All paths are resolved relative to this, and a path
|
|
16
|
+
* whose `..` segments would leave it is refused. Defaults to the current working directory.
|
|
16
17
|
*/
|
|
17
18
|
rootDir?: string;
|
|
18
19
|
}
|
|
@@ -25,12 +26,16 @@ export class NodeFilesApi implements FilesApi {
|
|
|
25
26
|
private rootDir: string;
|
|
26
27
|
|
|
27
28
|
constructor(options?: NodeFilesApiOptions) {
|
|
28
|
-
this.rootDir = options?.rootDir ?? process.cwd();
|
|
29
|
+
this.rootDir = nodePath.resolve(options?.rootDir ?? process.cwd());
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/** The real path of a virtual one; throws when `..` segments would take it outside rootDir. */
|
|
31
33
|
private resolvePath(virtualPath: string): string {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
+
const real = nodePath.resolve(this.rootDir, `.${normalizePath(virtualPath)}`);
|
|
35
|
+
if (real !== this.rootDir && !real.startsWith(this.rootDir + nodePath.sep)) {
|
|
36
|
+
throw new Error(`NodeFilesApi: path is outside rootDir: ${virtualPath}`);
|
|
37
|
+
}
|
|
38
|
+
return real;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
async *read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array> {
|
|
@@ -82,7 +87,7 @@ export class NodeFilesApi implements FilesApi {
|
|
|
82
87
|
): Promise<void> {
|
|
83
88
|
const realPath = this.resolvePath(path);
|
|
84
89
|
const normalized = normalizePath(path);
|
|
85
|
-
const dir = this.
|
|
90
|
+
const dir = this.resolvePath(dirname(normalized));
|
|
86
91
|
|
|
87
92
|
await fs.mkdir(dir, { recursive: true });
|
|
88
93
|
|
|
@@ -206,7 +211,7 @@ export class NodeFilesApi implements FilesApi {
|
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
try {
|
|
209
|
-
const targetDir = this.
|
|
214
|
+
const targetDir = this.resolvePath(dirname(normalizedTarget));
|
|
210
215
|
await fs.mkdir(targetDir, { recursive: true });
|
|
211
216
|
await fs.rename(sourcePath, targetPath);
|
|
212
217
|
return true;
|
|
@@ -227,7 +232,7 @@ export class NodeFilesApi implements FilesApi {
|
|
|
227
232
|
}
|
|
228
233
|
|
|
229
234
|
try {
|
|
230
|
-
const targetDir = this.
|
|
235
|
+
const targetDir = this.resolvePath(dirname(normalizedTarget));
|
|
231
236
|
await fs.mkdir(targetDir, { recursive: true });
|
|
232
237
|
await fs.cp(sourcePath, targetPath, { recursive: true });
|
|
233
238
|
return true;
|