pi-bridge 0.3.2 → 0.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bridge",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Transparent filesystem bridge that makes pi run natively on a remote machine over SSH",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const fsp = require('fs/promises');
5
5
  const http = require('http');
6
6
  const path = require('path');
7
+ const { fileURLToPath } = require('url');
7
8
  const { isFakePath, toRemotePath } = require('./path-mapper');
8
9
  const { httpGet, httpPost } = require('./http-client');
9
10
 
@@ -97,6 +98,10 @@ function patchFs(fakeRoot, port, token) {
97
98
  renameSync: fs.renameSync.bind(fs),
98
99
  copyFileSync: fs.copyFileSync.bind(fs),
99
100
  realpathSync: fs.realpathSync.bind(fs),
101
+ mkdtempSync: fs.mkdtempSync.bind(fs),
102
+ rmdirSync: fs.rmdirSync.bind(fs),
103
+ chmodSync: fs.chmodSync.bind(fs),
104
+ cpSync: fs.cpSync ? fs.cpSync.bind(fs) : undefined,
100
105
  createWriteStream: fs.createWriteStream.bind(fs),
101
106
  createReadStream: fs.createReadStream.bind(fs),
102
107
  };
@@ -116,11 +121,35 @@ function patchFs(fakeRoot, port, token) {
116
121
  rename: fs.promises.rename.bind(fs.promises),
117
122
  copyFile: fs.promises.copyFile.bind(fs.promises),
118
123
  open: fs.promises.open.bind(fs.promises),
119
- };
120
-
121
- // Resolve any path (relative or absolute) to absolute before checking fake root
124
+ mkdtemp: fs.promises.mkdtemp.bind(fs.promises),
125
+ realpath: fs.promises.realpath.bind(fs.promises),
126
+ rmdir: fs.promises.rmdir.bind(fs.promises),
127
+ chmod: fs.promises.chmod.bind(fs.promises),
128
+ cp: fs.promises.cp ? fs.promises.cp.bind(fs.promises) : undefined,
129
+ };
130
+
131
+ // Resolve any path (relative or absolute) to absolute before checking fake root.
132
+ // file: URLs are accepted too: jiti's bundled ESM resolver probes package
133
+ // mains with fs.statSync(new URL(...)), which must reach the remote.
134
+ // Buffer paths deliberately pass through: Node 20's JS rmSync walks children
135
+ // as Buffers via the public fs.*, and routing those would make removeFakeDir
136
+ // list and delete files on the remote.
122
137
  function abs(p) {
123
- return typeof p === 'string' ? path.resolve(p) : null;
138
+ if (typeof p === 'string') return path.resolve(p);
139
+ if (p instanceof URL) return p.protocol === 'file:' ? path.resolve(fileURLToPath(p)) : null;
140
+ return null;
141
+ }
142
+
143
+ // Shared by statSync/lstatSync. Honours { throwIfNoEntry: false }, which the
144
+ // ESM resolver relies on to probe candidate files without try/catch.
145
+ function statRemoteSync(s, opts) {
146
+ try {
147
+ return makeStats(JSON.parse(httpGet(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
148
+ } catch (e) {
149
+ const missing = e && e.message && (e.message.includes('ENOENT') || e.message.includes('403'));
150
+ if (missing && opts && opts.throwIfNoEntry === false) return undefined;
151
+ rethrowRemote(e, s);
152
+ }
124
153
  }
125
154
 
126
155
  function rpath(fakePath) {
@@ -220,17 +249,13 @@ function patchFs(fakeRoot, port, token) {
220
249
  fs.statSync = function patchedStatSync(p, opts) {
221
250
  const s = abs(p);
222
251
  if (!s || !isFakePath(s, fakeRoot)) return orig.statSync(p, opts);
223
- try {
224
- return makeStats(JSON.parse(httpGet(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
225
- } catch (e) { rethrowRemote(e, s); }
252
+ return statRemoteSync(s, opts);
226
253
  };
227
254
 
228
255
  fs.lstatSync = function patchedLstatSync(p, opts) {
229
256
  const s = abs(p);
230
257
  if (!s || !isFakePath(s, fakeRoot)) return orig.lstatSync(p, opts);
231
- try {
232
- return makeStats(JSON.parse(httpGet(port, token, '/stat?path=' + encodeURIComponent(rpath(s)))));
233
- } catch (e) { rethrowRemote(e, s); }
258
+ return statRemoteSync(s, opts);
234
259
  };
235
260
 
236
261
  fs.writeFileSync = function patchedWriteFileSync(p, data, opts) {
@@ -246,6 +271,58 @@ function patchFs(fakeRoot, port, token) {
246
271
  httpPost(port, token, '/mkdir?path=' + encodeURIComponent(rpath(s)), '');
247
272
  };
248
273
 
274
+ /**
275
+ * The remote runs the real mkdtempSync and returns only the generated
276
+ * directory name, so uniqueness keeps Node's semantics and no remote path has
277
+ * to be mapped back to a local one.
278
+ */
279
+ fs.mkdtempSync = function patchedMkdtempSync(prefix, opts) {
280
+ const s = abs(prefix);
281
+ if (!s || !isFakePath(s, fakeRoot)) return orig.mkdtempSync(prefix, opts);
282
+ const name = httpPost(port, token, '/mkdtemp?path=' + encodeURIComponent(rpath(s)), '');
283
+ return path.join(path.dirname(s), String(name).trim());
284
+ };
285
+
286
+ fs.rmdirSync = function patchedRmdirSync(p, opts) {
287
+ const s = abs(p);
288
+ if (!s || !isFakePath(s, fakeRoot)) return orig.rmdirSync(p, opts);
289
+ const recursive = (opts && opts.recursive) ? '1' : '0';
290
+ httpPost(port, token, '/rm?path=' + encodeURIComponent(rpath(s)) + '&recursive=' + recursive, '');
291
+ };
292
+
293
+ fs.chmodSync = function patchedChmodSync(p, mode) {
294
+ const s = abs(p);
295
+ if (!s || !isFakePath(s, fakeRoot)) return orig.chmodSync(p, mode);
296
+ // Octal on the wire: String(0o755) is "493", which the server would have to
297
+ // guess at. toString(8) keeps the intent explicit for numbers and strings.
298
+ const octal = typeof mode === 'number' ? mode.toString(8) : String(mode).replace(/^0o?/i, '');
299
+ httpPost(port, token, '/chmod?path=' + encodeURIComponent(rpath(s)) + '&mode=' + octal, '');
300
+ };
301
+
302
+ // Recursive copy: the remote /copy route handles one file, so directories are
303
+ // walked here through the already-patched calls.
304
+ fs.cpSync = function patchedCpSync(src, dest, opts) {
305
+ const srcS = abs(src);
306
+ const destS = abs(dest);
307
+ const involved = (srcS && isFakePath(srcS, fakeRoot)) || (destS && isFakePath(destS, fakeRoot));
308
+ if (!involved) {
309
+ if (!orig.cpSync) throw new Error('fs.cpSync is not available in this Node version');
310
+ return orig.cpSync(src, dest, opts);
311
+ }
312
+ const stats = fs.statSync(srcS);
313
+ if (!stats.isDirectory()) {
314
+ fs.copyFileSync(srcS, destS);
315
+ return;
316
+ }
317
+ if (!(opts && opts.recursive)) {
318
+ throw Object.assign(new Error('EISDIR: illegal operation on a directory, cp ' + srcS), { code: 'EISDIR' });
319
+ }
320
+ fs.mkdirSync(destS, { recursive: true });
321
+ for (const entry of fs.readdirSync(srcS)) {
322
+ fs.cpSync(path.join(srcS, entry), path.join(destS, entry), opts);
323
+ }
324
+ };
325
+
249
326
  fs.unlinkSync = function patchedUnlinkSync(p) {
250
327
  const s = abs(p);
251
328
  if (!s || !isFakePath(s, fakeRoot)) return orig.unlinkSync(p);
@@ -298,7 +375,7 @@ function patchFs(fakeRoot, port, token) {
298
375
 
299
376
  // createReadStream: read entire file from remote, pipe as stream
300
377
  fs.createReadStream = function patchedCreateReadStream(p, opts) {
301
- const s = typeof p === 'string' ? abs(p) : null;
378
+ const s = abs(p);
302
379
  if (!s || !isFakePath(s, fakeRoot)) return orig.createReadStream(p, opts);
303
380
  const { Readable } = require('stream');
304
381
  const stream = new Readable({ read() {} });
@@ -311,7 +388,7 @@ function patchFs(fakeRoot, port, token) {
311
388
 
312
389
  // createWriteStream: buffer writes, POST on finish
313
390
  fs.createWriteStream = function patchedCreateWriteStream(p, opts) {
314
- const s = typeof p === 'string' ? abs(p) : null;
391
+ const s = abs(p);
315
392
  if (!s || !isFakePath(s, fakeRoot)) return orig.createWriteStream(p, opts);
316
393
  const { Writable } = require('stream');
317
394
  const chunks = [];
@@ -341,7 +418,7 @@ function patchFs(fakeRoot, port, token) {
341
418
  };
342
419
 
343
420
  fs.promises.readFile = async function patchedReadFile(p, opts) {
344
- const s = typeof p === 'string' ? abs(p) : null;
421
+ const s = abs(p);
345
422
  if (!s || !isFakePath(s, fakeRoot)) return origP.readFile(p, opts);
346
423
  try {
347
424
  const body = await httpGetAsync(port, token, '/read?path=' + encodeURIComponent(rpath(s)));
@@ -355,7 +432,7 @@ function patchFs(fakeRoot, port, token) {
355
432
  };
356
433
 
357
434
  fs.promises.readdir = async function patchedReaddir(p, opts) {
358
- const s = typeof p === 'string' ? abs(p) : p.toString();
435
+ const s = abs(p) || p.toString();
359
436
  if (!isFakePath(s, fakeRoot)) return origP.readdir(p, opts);
360
437
  try {
361
438
  const withFileTypes = opts && opts.withFileTypes;
@@ -404,7 +481,7 @@ function patchFs(fakeRoot, port, token) {
404
481
  };
405
482
 
406
483
  fs.promises.writeFile = async function patchedWriteFile(p, data, opts) {
407
- const s = typeof p === 'string' ? abs(p) : null;
484
+ const s = abs(p);
408
485
  if (!s || !isFakePath(s, fakeRoot)) return origP.writeFile(p, data, opts);
409
486
  const content = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
410
487
  await new Promise(function(resolve, reject) {
@@ -456,7 +533,7 @@ function patchFs(fakeRoot, port, token) {
456
533
  };
457
534
 
458
535
  fs.promises.appendFile = async function patchedAppendFile(p, data, opts) {
459
- const s = typeof p === 'string' ? abs(p) : null;
536
+ const s = abs(p);
460
537
  if (!s || !isFakePath(s, fakeRoot)) return origP.appendFile(p, data, opts);
461
538
  const content = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
462
539
  const buf = Buffer.from(content, 'utf8');
@@ -501,9 +578,59 @@ function patchFs(fakeRoot, port, token) {
501
578
  });
502
579
  };
503
580
 
581
+ fs.promises.rename = async function patchedRename(from, to) {
582
+ const fromS = abs(from);
583
+ const toS = abs(to);
584
+ if (!fromS || !isFakePath(fromS, fakeRoot)) return origP.rename(from, to);
585
+ await new Promise(function(resolve, reject) {
586
+ const req = http.request(
587
+ { method: 'POST', hostname: '127.0.0.1', port: port,
588
+ path: '/rename?from=' + encodeURIComponent(rpath(fromS)) + '&to=' + encodeURIComponent(toS ? rpath(toS) : String(to)),
589
+ headers: { 'x-token': token, 'Content-Length': 0 } },
590
+ function(res) { res.resume(); res.on('end', function() { res.statusCode < 300 ? resolve() : reject(new Error('HTTP ' + res.statusCode)); }); }
591
+ );
592
+ req.on('error', reject); req.end();
593
+ });
594
+ };
595
+
596
+ fs.promises.mkdtemp = async function patchedMkdtemp(prefix, opts) {
597
+ const s = abs(prefix);
598
+ if (!s || !isFakePath(s, fakeRoot)) return origP.mkdtemp(prefix, opts);
599
+ return fs.mkdtempSync(prefix, opts);
600
+ };
601
+
602
+ fs.promises.realpath = async function patchedRealpath(p, opts) {
603
+ const s = abs(p);
604
+ if (!s || !isFakePath(s, fakeRoot)) return origP.realpath(p, opts);
605
+ return s;
606
+ };
607
+
608
+ fs.promises.rmdir = async function patchedRmdir(p, opts) {
609
+ const s = abs(p);
610
+ if (!s || !isFakePath(s, fakeRoot)) return origP.rmdir(p, opts);
611
+ return fs.promises.rm(p, Object.assign({}, opts));
612
+ };
613
+
614
+ fs.promises.chmod = async function patchedChmod(p, mode) {
615
+ const s = abs(p);
616
+ if (!s || !isFakePath(s, fakeRoot)) return origP.chmod(p, mode);
617
+ fs.chmodSync(p, mode);
618
+ };
619
+
620
+ fs.promises.cp = async function patchedCp(src, dest, opts) {
621
+ const srcS = abs(src);
622
+ const destS = abs(dest);
623
+ const involved = (srcS && isFakePath(srcS, fakeRoot)) || (destS && isFakePath(destS, fakeRoot));
624
+ if (!involved) {
625
+ if (!origP.cp) throw new Error('fs.promises.cp is not available in this Node version');
626
+ return origP.cp(src, dest, opts);
627
+ }
628
+ fs.cpSync(src, dest, opts);
629
+ };
630
+
504
631
  // fs.promises.open: return a minimal FileHandle for reading
505
632
  fs.promises.open = async function patchedOpen(p, flags, mode) {
506
- const s = typeof p === 'string' ? abs(p) : null;
633
+ const s = abs(p);
507
634
  if (!s || !isFakePath(s, fakeRoot)) return origP.open(p, flags, mode);
508
635
  // Fetch the file content eagerly; return a minimal FileHandle-like object
509
636
  const content = await httpGetAsync(port, token, '/read?path=' + encodeURIComponent(rpath(s)));
@@ -535,7 +662,7 @@ function patchFs(fakeRoot, port, token) {
535
662
  const watchFilePollers = new Map();
536
663
 
537
664
  fs.watch = function patchedWatch(filename, opts, listener) {
538
- const s = abs(typeof filename === 'string' ? filename : null);
665
+ const s = abs(filename);
539
666
  if (!s || !isFakePath(s, fakeRoot)) return origWatch(filename, opts, listener);
540
667
 
541
668
  if (typeof opts === 'function') { listener = opts; opts = {}; }
@@ -600,7 +727,7 @@ function patchFs(fakeRoot, port, token) {
600
727
  };
601
728
 
602
729
  fs.watchFile = function patchedWatchFile(filename, opts, listener) {
603
- const s = abs(typeof filename === 'string' ? filename : null);
730
+ const s = abs(filename);
604
731
  if (!s || !isFakePath(s, fakeRoot)) {
605
732
  return origWatchFile ? origWatchFile(filename, opts, listener) : undefined;
606
733
  }
@@ -630,7 +757,7 @@ function patchFs(fakeRoot, port, token) {
630
757
  };
631
758
 
632
759
  fs.unwatchFile = function patchedUnwatchFile(filename, listener) {
633
- const s = abs(typeof filename === 'string' ? filename : null);
760
+ const s = abs(filename);
634
761
  if (!s || !isFakePath(s, fakeRoot)) {
635
762
  return origUnwatchFile ? origUnwatchFile(filename, listener) : undefined;
636
763
  }
@@ -651,6 +778,12 @@ function patchFs(fakeRoot, port, token) {
651
778
  fsp.mkdir = fs.promises.mkdir;
652
779
  fsp.rm = fs.promises.rm;
653
780
  fsp.unlink = fs.promises.unlink;
781
+ fsp.rename = fs.promises.rename;
782
+ fsp.mkdtemp = fs.promises.mkdtemp;
783
+ fsp.realpath = fs.promises.realpath;
784
+ fsp.rmdir = fs.promises.rmdir;
785
+ fsp.chmod = fs.promises.chmod;
786
+ fsp.cp = fs.promises.cp;
654
787
  fsp.copyFile = fs.promises.copyFile;
655
788
  fsp.open = fs.promises.open;
656
789
  }
@@ -123,6 +123,32 @@ function createHandlers() {
123
123
  return;
124
124
  }
125
125
 
126
+ if (req.method === 'POST' && pathname === '/mkdtemp') {
127
+ const prefix = url.searchParams.get('path');
128
+ if (!prefix) return sendError(res, 400, 'Missing path parameter');
129
+ // Real mkdtemp, so uniqueness is the kernel's problem rather than a
130
+ // check-then-create race. Only the generated name is returned; the
131
+ // caller already knows the directory.
132
+ const created = fs.mkdtempSync(prefix);
133
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
134
+ res.end(require('path').basename(created));
135
+ return;
136
+ }
137
+
138
+ if (req.method === 'POST' && pathname === '/chmod') {
139
+ const p = url.searchParams.get('path');
140
+ const mode = url.searchParams.get('mode');
141
+ if (!p) return sendError(res, 400, 'Missing path parameter');
142
+ if (!mode) return sendError(res, 400, 'Missing mode parameter');
143
+ // The client always sends octal digits, so the wire value cannot be
144
+ // mistaken for a decimal bitmask.
145
+ if (!/^[0-7]{3,4}$/.test(mode)) return sendError(res, 400, 'Invalid mode parameter');
146
+ fs.chmodSync(p, parseInt(mode, 8));
147
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
148
+ res.end('ok');
149
+ return;
150
+ }
151
+
126
152
  if (req.method === 'POST' && pathname === '/delete') {
127
153
  const p = url.searchParams.get('path');
128
154
  if (!p) return sendError(res, 400, 'Missing path parameter');