pi-bridge 0.1.1
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 +126 -0
- package/bin/pii.js +71 -0
- package/package.json +33 -0
- package/src/local/argv.js +45 -0
- package/src/local/child-process-patch.js +75 -0
- package/src/local/cleanup.js +56 -0
- package/src/local/fake-dir.js +51 -0
- package/src/local/fs-patch.js +639 -0
- package/src/local/http-client.js +130 -0
- package/src/local/path-mapper.js +54 -0
- package/src/local/preload.js +73 -0
- package/src/local/setup.js +79 -0
- package/src/local/ssh.js +20 -0
- package/src/local/tunnel.js +69 -0
- package/src/local/upload.js +81 -0
- package/src/remote/auth.js +33 -0
- package/src/remote/guard.js +49 -0
- package/src/remote/handlers.js +188 -0
- package/src/remote/index.js +43 -0
- package/src/shared/protocol.js +21 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const fsp = require('fs/promises');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { isFakePath, toRemotePath } = require('./path-mapper');
|
|
8
|
+
const { httpGet, httpPost } = require('./http-client');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Patch Node.js built-in fs methods to intercept calls on fake-root paths
|
|
12
|
+
* and redirect them to the remote file server.
|
|
13
|
+
*
|
|
14
|
+
* Covers both sync (readFileSync, readdirSync, …) and async (fs.promises.*)
|
|
15
|
+
* variants so that both pi's startup resource loading and its read tool work.
|
|
16
|
+
*
|
|
17
|
+
* Non-fake paths are forwarded to the original fs implementations unchanged.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} fakeRoot - Local fake root directory (real path, no symlinks)
|
|
20
|
+
* @param {number} port - Remote file server port (127.0.0.1)
|
|
21
|
+
* @param {string} token - Auth token
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// --- Error helpers ---
|
|
25
|
+
|
|
26
|
+
function makeEnoent(p, syscall) {
|
|
27
|
+
syscall = syscall || 'open';
|
|
28
|
+
return Object.assign(
|
|
29
|
+
new Error("ENOENT: no such file or directory, " + syscall + " '" + p + "'"),
|
|
30
|
+
{ code: 'ENOENT', syscall, path: p }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeEisdir(p) {
|
|
35
|
+
return Object.assign(
|
|
36
|
+
new Error("EISDIR: illegal operation on a directory, read"),
|
|
37
|
+
{ code: 'EISDIR', syscall: 'read', path: p }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rethrowRemote(e, p) {
|
|
42
|
+
if (!e || !e.message) throw e;
|
|
43
|
+
if (e.message.includes('403')) throw makeEnoent(p);
|
|
44
|
+
if (e.message.includes('EISDIR')) throw makeEisdir(p);
|
|
45
|
+
if (e.message.includes('ENOENT')) throw makeEnoent(p);
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Async HTTP helper (used for fs.promises patching) ---
|
|
50
|
+
|
|
51
|
+
function httpGetAsync(port, token, urlPath) {
|
|
52
|
+
return new Promise(function(resolve, reject) {
|
|
53
|
+
const req = http.get(
|
|
54
|
+
{ hostname: '127.0.0.1', port: port, path: urlPath, headers: { 'x-token': token } },
|
|
55
|
+
function(res) {
|
|
56
|
+
var body = '';
|
|
57
|
+
res.setEncoding('utf8');
|
|
58
|
+
res.on('data', function(d) { body += d; });
|
|
59
|
+
res.on('end', function() {
|
|
60
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
61
|
+
resolve(body);
|
|
62
|
+
} else {
|
|
63
|
+
var err = new Error('HTTP ' + res.statusCode + ': ' + body);
|
|
64
|
+
err.statusCode = res.statusCode;
|
|
65
|
+
reject(err);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
req.on('error', reject);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function patchFs(fakeRoot, port, token) {
|
|
75
|
+
// --- Sync originals ---
|
|
76
|
+
const orig = {
|
|
77
|
+
readdirSync: fs.readdirSync.bind(fs),
|
|
78
|
+
readFileSync: fs.readFileSync.bind(fs),
|
|
79
|
+
existsSync: fs.existsSync.bind(fs),
|
|
80
|
+
accessSync: fs.accessSync.bind(fs),
|
|
81
|
+
statSync: fs.statSync.bind(fs),
|
|
82
|
+
lstatSync: fs.lstatSync.bind(fs),
|
|
83
|
+
writeFileSync: fs.writeFileSync.bind(fs),
|
|
84
|
+
appendFileSync: fs.appendFileSync.bind(fs),
|
|
85
|
+
mkdirSync: fs.mkdirSync.bind(fs),
|
|
86
|
+
rmSync: fs.rmSync.bind(fs),
|
|
87
|
+
unlinkSync: fs.unlinkSync.bind(fs),
|
|
88
|
+
renameSync: fs.renameSync.bind(fs),
|
|
89
|
+
copyFileSync: fs.copyFileSync.bind(fs),
|
|
90
|
+
realpathSync: fs.realpathSync.bind(fs),
|
|
91
|
+
createWriteStream: fs.createWriteStream.bind(fs),
|
|
92
|
+
createReadStream: fs.createReadStream.bind(fs),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// --- Async (promises) originals ---
|
|
96
|
+
const origP = {
|
|
97
|
+
access: fs.promises.access.bind(fs.promises),
|
|
98
|
+
readFile: fs.promises.readFile.bind(fs.promises),
|
|
99
|
+
readdir: fs.promises.readdir.bind(fs.promises),
|
|
100
|
+
stat: fs.promises.stat.bind(fs.promises),
|
|
101
|
+
lstat: fs.promises.lstat.bind(fs.promises),
|
|
102
|
+
writeFile: fs.promises.writeFile.bind(fs.promises),
|
|
103
|
+
appendFile: fs.promises.appendFile.bind(fs.promises),
|
|
104
|
+
mkdir: fs.promises.mkdir.bind(fs.promises),
|
|
105
|
+
rm: fs.promises.rm.bind(fs.promises),
|
|
106
|
+
unlink: fs.promises.unlink.bind(fs.promises),
|
|
107
|
+
rename: fs.promises.rename.bind(fs.promises),
|
|
108
|
+
copyFile: fs.promises.copyFile.bind(fs.promises),
|
|
109
|
+
open: fs.promises.open.bind(fs.promises),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Resolve any path (relative or absolute) to absolute before checking fake root
|
|
113
|
+
function abs(p) {
|
|
114
|
+
return typeof p === 'string' ? path.resolve(p) : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function rpath(fakePath) {
|
|
118
|
+
return toRemotePath(fakePath, fakeRoot);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Build a minimal fs.Stats-like object from the remote /stat response
|
|
122
|
+
function makeStats(info) {
|
|
123
|
+
return {
|
|
124
|
+
isFile: function() { return info.isFile; },
|
|
125
|
+
isDirectory: function() { return info.isDirectory; },
|
|
126
|
+
isSymbolicLink: function() { return false; },
|
|
127
|
+
isBlockDevice: function() { return false; },
|
|
128
|
+
isCharacterDevice: function() { return false; },
|
|
129
|
+
isFIFO: function() { return false; },
|
|
130
|
+
isSocket: function() { return false; },
|
|
131
|
+
size: info.size,
|
|
132
|
+
mtimeMs: info.mtimeMs,
|
|
133
|
+
mtime: new Date(info.mtimeMs),
|
|
134
|
+
mode: info.isDirectory ? 0o40755 : 0o100644,
|
|
135
|
+
nlink: 1,
|
|
136
|
+
uid: 0, gid: 0, dev: 0, ino: 0, rdev: 0, blksize: 4096, blocks: 0,
|
|
137
|
+
atime: new Date(info.mtimeMs), ctime: new Date(info.mtimeMs), birthtime: new Date(info.mtimeMs),
|
|
138
|
+
atimeMs: info.mtimeMs, ctimeMs: info.mtimeMs, birthtimeMs: info.mtimeMs,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Sync patches ────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
fs.readdirSync = function patchedReaddirSync(p, opts) {
|
|
145
|
+
const s = abs(p) || p.toString();
|
|
146
|
+
if (!isFakePath(s, fakeRoot)) return orig.readdirSync(p, opts);
|
|
147
|
+
try {
|
|
148
|
+
const withFileTypes = opts && opts.withFileTypes;
|
|
149
|
+
const qs = '/list?path=' + encodeURIComponent(rpath(s)) + (withFileTypes ? '&withFileTypes=1' : '');
|
|
150
|
+
const parsed = JSON.parse(httpGet(port, token, qs));
|
|
151
|
+
if (!withFileTypes) return parsed;
|
|
152
|
+
return parsed.map(function(e) {
|
|
153
|
+
return {
|
|
154
|
+
name: e.name,
|
|
155
|
+
isFile: function() { return e.isFile; },
|
|
156
|
+
isDirectory: function() { return e.isDirectory; },
|
|
157
|
+
isSymbolicLink: function() { return e.isSymbolicLink; },
|
|
158
|
+
isBlockDevice: function() { return false; },
|
|
159
|
+
isCharacterDevice: function() { return false; },
|
|
160
|
+
isFIFO: function() { return false; },
|
|
161
|
+
isSocket: function() { return false; },
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
} catch (e) { rethrowRemote(e, s); }
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
fs.readFileSync = function patchedReadFileSync(p, opts) {
|
|
168
|
+
const s = abs(p);
|
|
169
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.readFileSync(p, opts);
|
|
170
|
+
try {
|
|
171
|
+
const body = httpGet(port, token, '/read?path=' + encodeURIComponent(rpath(s)));
|
|
172
|
+
const enc = typeof opts === 'string' ? opts : (opts && opts.encoding);
|
|
173
|
+
return enc ? body : Buffer.from(body, 'utf8');
|
|
174
|
+
} catch (e) { rethrowRemote(e, s); }
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
fs.existsSync = function patchedExistsSync(p) {
|
|
178
|
+
const s = abs(p);
|
|
179
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.existsSync(p);
|
|
180
|
+
try {
|
|
181
|
+
return httpGet(port, token, '/exists?path=' + encodeURIComponent(rpath(s))).trim() === '1';
|
|
182
|
+
} catch (e) {
|
|
183
|
+
if (e.message && (e.message.includes('403') || e.message.includes('ENOENT'))) return false;
|
|
184
|
+
throw e;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
fs.accessSync = function patchedAccessSync(p, mode) {
|
|
189
|
+
const s = abs(p);
|
|
190
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.accessSync(p, mode);
|
|
191
|
+
try {
|
|
192
|
+
const exists = httpGet(port, token, '/exists?path=' + encodeURIComponent(rpath(s))).trim() === '1';
|
|
193
|
+
if (!exists) throw makeEnoent(s, 'access');
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (e.code === 'ENOENT') throw e;
|
|
196
|
+
if (e.message && e.message.includes('403')) throw makeEnoent(s, 'access');
|
|
197
|
+
throw e;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
fs.statSync = function patchedStatSync(p, opts) {
|
|
202
|
+
const s = abs(p);
|
|
203
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.statSync(p, opts);
|
|
204
|
+
try {
|
|
205
|
+
return makeStats(JSON.parse(httpGet(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
|
|
206
|
+
} catch (e) { rethrowRemote(e, s); }
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
fs.lstatSync = function patchedLstatSync(p, opts) {
|
|
210
|
+
const s = abs(p);
|
|
211
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.lstatSync(p, opts);
|
|
212
|
+
try {
|
|
213
|
+
return makeStats(JSON.parse(httpGet(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
|
|
214
|
+
} catch (e) { rethrowRemote(e, s); }
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
fs.writeFileSync = function patchedWriteFileSync(p, data, opts) {
|
|
218
|
+
const s = abs(p);
|
|
219
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.writeFileSync(p, data, opts);
|
|
220
|
+
const content = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
|
|
221
|
+
httpPost(port, token, '/write?path=' + encodeURIComponent(rpath(s)), content);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
fs.mkdirSync = function patchedMkdirSync(p, opts) {
|
|
225
|
+
const s = abs(p);
|
|
226
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.mkdirSync(p, opts);
|
|
227
|
+
httpPost(port, token, '/mkdir?path=' + encodeURIComponent(rpath(s)), '');
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
fs.unlinkSync = function patchedUnlinkSync(p) {
|
|
231
|
+
const s = abs(p);
|
|
232
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.unlinkSync(p);
|
|
233
|
+
httpPost(port, token, '/delete?path=' + encodeURIComponent(rpath(s)), '');
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
fs.renameSync = function patchedRenameSync(from, to) {
|
|
237
|
+
const fromS = abs(from);
|
|
238
|
+
const toS = abs(to);
|
|
239
|
+
if (!fromS || !isFakePath(fromS, fakeRoot)) return orig.renameSync(from, to);
|
|
240
|
+
httpPost(port, token,
|
|
241
|
+
'/rename?from=' + encodeURIComponent(rpath(fromS)) + '&to=' + encodeURIComponent(toS ? rpath(toS) : to.toString()),
|
|
242
|
+
'');
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
fs.appendFileSync = function patchedAppendFileSync(p, data, opts) {
|
|
246
|
+
const s = abs(p);
|
|
247
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.appendFileSync(p, data, opts);
|
|
248
|
+
const content = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
|
|
249
|
+
httpPost(port, token, '/append?path=' + encodeURIComponent(rpath(s)), content);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
fs.rmSync = function patchedRmSync(p, opts) {
|
|
253
|
+
const s = abs(p);
|
|
254
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.rmSync(p, opts);
|
|
255
|
+
const recursive = (opts && opts.recursive) ? '1' : '0';
|
|
256
|
+
httpPost(port, token, '/rm?path=' + encodeURIComponent(rpath(s)) + '&recursive=' + recursive, '');
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
fs.copyFileSync = function patchedCopyFileSync(src, dest) {
|
|
260
|
+
const srcS = abs(src);
|
|
261
|
+
const destS = abs(dest);
|
|
262
|
+
if (!srcS || !isFakePath(srcS, fakeRoot)) return orig.copyFileSync(src, dest);
|
|
263
|
+
httpPost(port, token,
|
|
264
|
+
'/copy?from=' + encodeURIComponent(rpath(srcS)) + '&to=' + encodeURIComponent(destS ? rpath(destS) : dest.toString()),
|
|
265
|
+
'');
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// realpathSync: fake paths are already real absolute paths (no symlinks in skeleton)
|
|
269
|
+
fs.realpathSync = function patchedRealpathSync(p, opts) {
|
|
270
|
+
const s = abs(p);
|
|
271
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.realpathSync(p, opts);
|
|
272
|
+
return s;
|
|
273
|
+
};
|
|
274
|
+
fs.realpathSync.native = function(p, opts) {
|
|
275
|
+
const s = abs(p);
|
|
276
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.realpathSync.native ? orig.realpathSync.native(p, opts) : orig.realpathSync(p, opts);
|
|
277
|
+
return s;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// createReadStream: read entire file from remote, pipe as stream
|
|
281
|
+
fs.createReadStream = function patchedCreateReadStream(p, opts) {
|
|
282
|
+
const s = typeof p === 'string' ? abs(p) : null;
|
|
283
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.createReadStream(p, opts);
|
|
284
|
+
const { Readable } = require('stream');
|
|
285
|
+
const stream = new Readable({ read() {} });
|
|
286
|
+
// Fetch async, push into stream
|
|
287
|
+
httpGetAsync(port, token, '/read?path=' + encodeURIComponent(rpath(s)))
|
|
288
|
+
.then(function(body) { stream.push(body); stream.push(null); })
|
|
289
|
+
.catch(function(e) { stream.destroy(e); });
|
|
290
|
+
return stream;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// createWriteStream: buffer writes, POST on finish
|
|
294
|
+
fs.createWriteStream = function patchedCreateWriteStream(p, opts) {
|
|
295
|
+
const s = typeof p === 'string' ? abs(p) : null;
|
|
296
|
+
if (!s || !isFakePath(s, fakeRoot)) return orig.createWriteStream(p, opts);
|
|
297
|
+
const { Writable } = require('stream');
|
|
298
|
+
const chunks = [];
|
|
299
|
+
return new Writable({
|
|
300
|
+
write(chunk, enc, cb) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, enc)); cb(); },
|
|
301
|
+
final(cb) {
|
|
302
|
+
const content = Buffer.concat(chunks).toString('utf8');
|
|
303
|
+
try { httpPost(port, token, '/write?path=' + encodeURIComponent(rpath(s)), content); cb(); }
|
|
304
|
+
catch(e) { cb(e); }
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// ── Async (fs.promises) patches ─────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
fs.promises.access = async function patchedAccess(p, mode) {
|
|
312
|
+
const s = abs(p);
|
|
313
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.access(p, mode);
|
|
314
|
+
try {
|
|
315
|
+
const exists = (await httpGetAsync(port, token, '/exists?path=' + encodeURIComponent(rpath(s)))).trim() === '1';
|
|
316
|
+
if (!exists) throw makeEnoent(s, 'access');
|
|
317
|
+
} catch (e) {
|
|
318
|
+
if (e.code === 'ENOENT') throw e;
|
|
319
|
+
if (e.statusCode === 403 || (e.message && e.message.includes('403'))) throw makeEnoent(s, 'access');
|
|
320
|
+
throw e;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
fs.promises.readFile = async function patchedReadFile(p, opts) {
|
|
325
|
+
const s = typeof p === 'string' ? abs(p) : null;
|
|
326
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.readFile(p, opts);
|
|
327
|
+
try {
|
|
328
|
+
const body = await httpGetAsync(port, token, '/read?path=' + encodeURIComponent(rpath(s)));
|
|
329
|
+
const enc = typeof opts === 'string' ? opts : (opts && opts.encoding);
|
|
330
|
+
return enc ? body : Buffer.from(body, 'utf8');
|
|
331
|
+
} catch (e) {
|
|
332
|
+
if (e.statusCode === 403 || (e.message && e.message.includes('403'))) throw makeEnoent(s);
|
|
333
|
+
if (e.message && e.message.includes('EISDIR')) throw makeEisdir(s);
|
|
334
|
+
throw e;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
fs.promises.readdir = async function patchedReaddir(p, opts) {
|
|
339
|
+
const s = typeof p === 'string' ? abs(p) : p.toString();
|
|
340
|
+
if (!isFakePath(s, fakeRoot)) return origP.readdir(p, opts);
|
|
341
|
+
try {
|
|
342
|
+
const withFileTypes = opts && opts.withFileTypes;
|
|
343
|
+
const qs = '/list?path=' + encodeURIComponent(rpath(s)) + (withFileTypes ? '&withFileTypes=1' : '');
|
|
344
|
+
const parsed = JSON.parse(await httpGetAsync(port, token, qs));
|
|
345
|
+
if (!withFileTypes) return parsed;
|
|
346
|
+
return parsed.map(function(e) {
|
|
347
|
+
return {
|
|
348
|
+
name: e.name,
|
|
349
|
+
isFile: function() { return e.isFile; },
|
|
350
|
+
isDirectory: function() { return e.isDirectory; },
|
|
351
|
+
isSymbolicLink: function() { return e.isSymbolicLink; },
|
|
352
|
+
isBlockDevice: function() { return false; },
|
|
353
|
+
isCharacterDevice: function() { return false; },
|
|
354
|
+
isFIFO: function() { return false; },
|
|
355
|
+
isSocket: function() { return false; },
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
} catch (e) {
|
|
359
|
+
if (e.statusCode === 403 || (e.message && e.message.includes('403'))) throw makeEnoent(s);
|
|
360
|
+
throw e;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
fsp.readdir = fs.promises.readdir;
|
|
364
|
+
|
|
365
|
+
fs.promises.stat = async function patchedStat(p, opts) {
|
|
366
|
+
const s = abs(p);
|
|
367
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.stat(p, opts);
|
|
368
|
+
try {
|
|
369
|
+
return makeStats(JSON.parse(await httpGetAsync(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
|
|
370
|
+
} catch (e) {
|
|
371
|
+
if (e.statusCode === 403 || (e.message && e.message.includes('403'))) throw makeEnoent(s);
|
|
372
|
+
throw e;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
fs.promises.lstat = async function patchedLstat(p, opts) {
|
|
377
|
+
const s = abs(p);
|
|
378
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.lstat(p, opts);
|
|
379
|
+
try {
|
|
380
|
+
return makeStats(JSON.parse(await httpGetAsync(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
|
|
381
|
+
} catch (e) {
|
|
382
|
+
if (e.statusCode === 403 || (e.message && e.message.includes('403'))) throw makeEnoent(s);
|
|
383
|
+
throw e;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
fs.promises.writeFile = async function patchedWriteFile(p, data, opts) {
|
|
388
|
+
const s = typeof p === 'string' ? abs(p) : null;
|
|
389
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.writeFile(p, data, opts);
|
|
390
|
+
const content = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
|
|
391
|
+
await new Promise(function(resolve, reject) {
|
|
392
|
+
const body = Buffer.from(content, 'utf8');
|
|
393
|
+
const req = http.request(
|
|
394
|
+
{ method: 'POST', hostname: '127.0.0.1', port: port,
|
|
395
|
+
path: '/write?path=' + encodeURIComponent(rpath(s)),
|
|
396
|
+
headers: { 'x-token': token, 'Content-Length': body.length } },
|
|
397
|
+
function(res) {
|
|
398
|
+
res.resume();
|
|
399
|
+
res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); });
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
req.on('error', reject);
|
|
403
|
+
req.end(body);
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
fs.promises.mkdir = async function patchedMkdir(p, opts) {
|
|
408
|
+
const s = abs(p);
|
|
409
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.mkdir(p, opts);
|
|
410
|
+
await new Promise(function(resolve, reject) {
|
|
411
|
+
const req = http.request(
|
|
412
|
+
{ method: 'POST', hostname: '127.0.0.1', port: port,
|
|
413
|
+
path: '/mkdir?path=' + encodeURIComponent(rpath(s)),
|
|
414
|
+
headers: { 'x-token': token, 'Content-Length': 0 } },
|
|
415
|
+
function(res) {
|
|
416
|
+
res.resume();
|
|
417
|
+
res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); });
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
req.on('error', reject);
|
|
421
|
+
req.end();
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
fs.promises.unlink = async function patchedUnlink(p) {
|
|
426
|
+
const s = abs(p);
|
|
427
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.unlink(p);
|
|
428
|
+
await new Promise(function(resolve, reject) {
|
|
429
|
+
const req = http.request(
|
|
430
|
+
{ method: 'POST', hostname: '127.0.0.1', port: port,
|
|
431
|
+
path: '/delete?path=' + encodeURIComponent(rpath(s)),
|
|
432
|
+
headers: { 'x-token': token, 'Content-Length': 0 } },
|
|
433
|
+
function(res) { res.resume(); res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); }); }
|
|
434
|
+
);
|
|
435
|
+
req.on('error', reject); req.end();
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
fs.promises.appendFile = async function patchedAppendFile(p, data, opts) {
|
|
440
|
+
const s = typeof p === 'string' ? abs(p) : null;
|
|
441
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.appendFile(p, data, opts);
|
|
442
|
+
const content = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
|
|
443
|
+
const buf = Buffer.from(content, 'utf8');
|
|
444
|
+
await new Promise(function(resolve, reject) {
|
|
445
|
+
const req = http.request(
|
|
446
|
+
{ method: 'POST', hostname: '127.0.0.1', port: port,
|
|
447
|
+
path: '/append?path=' + encodeURIComponent(rpath(s)),
|
|
448
|
+
headers: { 'x-token': token, 'Content-Length': buf.length } },
|
|
449
|
+
function(res) { res.resume(); res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); }); }
|
|
450
|
+
);
|
|
451
|
+
req.on('error', reject); req.end(buf);
|
|
452
|
+
});
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
fs.promises.rm = async function patchedRm(p, opts) {
|
|
456
|
+
const s = abs(p);
|
|
457
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.rm(p, opts);
|
|
458
|
+
const recursive = (opts && opts.recursive) ? '1' : '0';
|
|
459
|
+
await new Promise(function(resolve, reject) {
|
|
460
|
+
const req = http.request(
|
|
461
|
+
{ method: 'POST', hostname: '127.0.0.1', port: port,
|
|
462
|
+
path: '/rm?path=' + encodeURIComponent(rpath(s)) + '&recursive=' + recursive,
|
|
463
|
+
headers: { 'x-token': token, 'Content-Length': 0 } },
|
|
464
|
+
function(res) { res.resume(); res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); }); }
|
|
465
|
+
);
|
|
466
|
+
req.on('error', reject); req.end();
|
|
467
|
+
});
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
fs.promises.copyFile = async function patchedCopyFile(src, dest) {
|
|
471
|
+
const srcS = abs(src);
|
|
472
|
+
const destS = abs(dest);
|
|
473
|
+
if (!srcS || !isFakePath(srcS, fakeRoot)) return origP.copyFile(src, dest);
|
|
474
|
+
await new Promise(function(resolve, reject) {
|
|
475
|
+
const req = http.request(
|
|
476
|
+
{ method: 'POST', hostname: '127.0.0.1', port: port,
|
|
477
|
+
path: '/copy?from=' + encodeURIComponent(rpath(srcS)) + '&to=' + encodeURIComponent(destS ? rpath(destS) : String(dest)),
|
|
478
|
+
headers: { 'x-token': token, 'Content-Length': 0 } },
|
|
479
|
+
function(res) { res.resume(); res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); }); }
|
|
480
|
+
);
|
|
481
|
+
req.on('error', reject); req.end();
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// fs.promises.open: return a minimal FileHandle for reading
|
|
486
|
+
fs.promises.open = async function patchedOpen(p, flags, mode) {
|
|
487
|
+
const s = typeof p === 'string' ? abs(p) : null;
|
|
488
|
+
if (!s || !isFakePath(s, fakeRoot)) return origP.open(p, flags, mode);
|
|
489
|
+
// Fetch the file content eagerly; return a minimal FileHandle-like object
|
|
490
|
+
const content = await httpGetAsync(port, token, '/read?path=' + encodeURIComponent(rpath(s)));
|
|
491
|
+
const buf = Buffer.from(content, 'utf8');
|
|
492
|
+
let pos = 0;
|
|
493
|
+
return {
|
|
494
|
+
readFile: async function(opts) {
|
|
495
|
+
const enc = typeof opts === 'string' ? opts : (opts && opts.encoding);
|
|
496
|
+
return enc ? content : buf;
|
|
497
|
+
},
|
|
498
|
+
read: async function(buffer, offset, length, position) {
|
|
499
|
+
const start = position != null ? position : pos;
|
|
500
|
+
const copied = buf.copy(buffer, offset, start, start + length);
|
|
501
|
+
pos = start + copied;
|
|
502
|
+
return { bytesRead: copied, buffer };
|
|
503
|
+
},
|
|
504
|
+
close: async function() {},
|
|
505
|
+
[Symbol.asyncDispose]: async function() {},
|
|
506
|
+
};
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// watch / watchFile: inotify can't cross machines, so we poll the remote
|
|
510
|
+
// /stat endpoint and emit 'change' when mtimeMs shifts.
|
|
511
|
+
const origWatch = fs.watch.bind(fs);
|
|
512
|
+
const origWatchFile = fs.watchFile ? fs.watchFile.bind(fs) : null;
|
|
513
|
+
const origUnwatchFile = fs.unwatchFile ? fs.unwatchFile.bind(fs) : null;
|
|
514
|
+
|
|
515
|
+
// Registry for watchFile pollers keyed by resolved path, so unwatchFile can stop them.
|
|
516
|
+
const watchFilePollers = new Map();
|
|
517
|
+
|
|
518
|
+
fs.watch = function patchedWatch(filename, opts, listener) {
|
|
519
|
+
const s = abs(typeof filename === 'string' ? filename : null);
|
|
520
|
+
if (!s || !isFakePath(s, fakeRoot)) return origWatch(filename, opts, listener);
|
|
521
|
+
|
|
522
|
+
if (typeof opts === 'function') { listener = opts; opts = {}; }
|
|
523
|
+
|
|
524
|
+
const { EventEmitter } = require('events');
|
|
525
|
+
const watcher = new EventEmitter();
|
|
526
|
+
if (listener) watcher.on('change', listener);
|
|
527
|
+
|
|
528
|
+
// file mtimes: '' → file itself, or filename → entry inside directory
|
|
529
|
+
var mtimes = new Map();
|
|
530
|
+
var isDir = null;
|
|
531
|
+
var closed = false;
|
|
532
|
+
|
|
533
|
+
function pollFile() {
|
|
534
|
+
return httpGetAsync(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))
|
|
535
|
+
.then(function(body) {
|
|
536
|
+
var info = JSON.parse(body);
|
|
537
|
+
var prev = mtimes.get('');
|
|
538
|
+
if (prev !== undefined && info.mtimeMs !== prev) {
|
|
539
|
+
watcher.emit('change', 'change', path.basename(s));
|
|
540
|
+
}
|
|
541
|
+
mtimes.set('', info.mtimeMs);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function pollDir() {
|
|
546
|
+
// Single request returns all entries with their mtimes
|
|
547
|
+
return httpGetAsync(port, token, '/liststat?path=' + encodeURIComponent(rpath(s)))
|
|
548
|
+
.then(function(body) {
|
|
549
|
+
var entries = JSON.parse(body);
|
|
550
|
+
entries.forEach(function(e) {
|
|
551
|
+
if (!e.isFile) return;
|
|
552
|
+
var prev = mtimes.get(e.name);
|
|
553
|
+
if (prev !== undefined && e.mtimeMs !== prev) {
|
|
554
|
+
watcher.emit('change', 'change', e.name);
|
|
555
|
+
}
|
|
556
|
+
mtimes.set(e.name, e.mtimeMs);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function poll() {
|
|
562
|
+
if (closed) return;
|
|
563
|
+
var work;
|
|
564
|
+
if (isDir === null) {
|
|
565
|
+
work = httpGetAsync(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))
|
|
566
|
+
.then(function(body) {
|
|
567
|
+
isDir = JSON.parse(body).isDirectory;
|
|
568
|
+
return isDir ? pollDir() : pollFile();
|
|
569
|
+
});
|
|
570
|
+
} else {
|
|
571
|
+
work = isDir ? pollDir() : pollFile();
|
|
572
|
+
}
|
|
573
|
+
work.catch(function() {}).then(function() { if (!closed) setTimeout(poll, 1000); });
|
|
574
|
+
}
|
|
575
|
+
poll();
|
|
576
|
+
|
|
577
|
+
watcher.close = function() { closed = true; };
|
|
578
|
+
watcher.ref = function() { return watcher; };
|
|
579
|
+
watcher.unref = function() { return watcher; };
|
|
580
|
+
return watcher;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
fs.watchFile = function patchedWatchFile(filename, opts, listener) {
|
|
584
|
+
const s = abs(typeof filename === 'string' ? filename : null);
|
|
585
|
+
if (!s || !isFakePath(s, fakeRoot)) {
|
|
586
|
+
return origWatchFile ? origWatchFile(filename, opts, listener) : undefined;
|
|
587
|
+
}
|
|
588
|
+
if (typeof opts === 'function') { listener = opts; opts = {}; }
|
|
589
|
+
const interval = (opts && opts.interval) || 5007;
|
|
590
|
+
|
|
591
|
+
var lastStat = null;
|
|
592
|
+
var closed = false;
|
|
593
|
+
|
|
594
|
+
function poll() {
|
|
595
|
+
if (closed) return;
|
|
596
|
+
httpGetAsync(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))
|
|
597
|
+
.then(function(body) {
|
|
598
|
+
var info = JSON.parse(body);
|
|
599
|
+
var cur = makeStats(info);
|
|
600
|
+
if (lastStat !== null && cur.mtimeMs !== lastStat.mtimeMs) {
|
|
601
|
+
if (listener) listener(cur, lastStat);
|
|
602
|
+
}
|
|
603
|
+
lastStat = cur;
|
|
604
|
+
})
|
|
605
|
+
.catch(function() {})
|
|
606
|
+
.then(function() { if (!closed) setTimeout(poll, interval); });
|
|
607
|
+
}
|
|
608
|
+
poll();
|
|
609
|
+
|
|
610
|
+
watchFilePollers.set(s, function stop() { closed = true; });
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
fs.unwatchFile = function patchedUnwatchFile(filename, listener) {
|
|
614
|
+
const s = abs(typeof filename === 'string' ? filename : null);
|
|
615
|
+
if (!s || !isFakePath(s, fakeRoot)) {
|
|
616
|
+
return origUnwatchFile ? origUnwatchFile(filename, listener) : undefined;
|
|
617
|
+
}
|
|
618
|
+
const stop = watchFilePollers.get(s);
|
|
619
|
+
if (stop) { stop(); watchFilePollers.delete(s); }
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// Mirror all async patches onto the standalone 'fs/promises' module object too.
|
|
623
|
+
// ES modules that do `import { readFile } from 'node:fs/promises'` get static
|
|
624
|
+
// bindings from this object — patching it ensures they see our interceptors.
|
|
625
|
+
fsp.access = fs.promises.access;
|
|
626
|
+
fsp.readFile = fs.promises.readFile;
|
|
627
|
+
fsp.readdir = fs.promises.readdir;
|
|
628
|
+
fsp.stat = fs.promises.stat;
|
|
629
|
+
fsp.lstat = fs.promises.lstat;
|
|
630
|
+
fsp.writeFile = fs.promises.writeFile;
|
|
631
|
+
fsp.appendFile = fs.promises.appendFile;
|
|
632
|
+
fsp.mkdir = fs.promises.mkdir;
|
|
633
|
+
fsp.rm = fs.promises.rm;
|
|
634
|
+
fsp.unlink = fs.promises.unlink;
|
|
635
|
+
fsp.copyFile = fs.promises.copyFile;
|
|
636
|
+
fsp.open = fs.promises.open;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
module.exports = { patchFs };
|