mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-06 14:30:13 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef3f0362f1 | ||
|
|
62006314e6 | ||
|
|
598d41e9d1 | ||
|
|
fd2be185f1 | ||
|
|
95b9cc5f32 | ||
|
|
0775403b1c | ||
|
|
a31bacbd07 |
No files matched your search
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* asarSafeFs - Local filesystem reads that bypass Electron's asar interception.
|
||||
*
|
||||
* Electron patches node:fs so that ANY local path whose segment ends with
|
||||
* .asar (case-insensitive) is treated as an asar archive instead of reaching
|
||||
* the real filesystem (electron/lib/node/asar-fs-wrapper.ts, asarRe = /\.asar/i).
|
||||
* Uploading a real file named app.asar then fails before any network I/O with
|
||||
* the archive-root error `ENOENT, not found in <path>` (the two spaces come
|
||||
* from the empty inner filePath), while the identical bytes named app.bin
|
||||
* upload fine (#3450).
|
||||
*
|
||||
* The official switch is process.noAsar = true, but it must only be flipped
|
||||
* around the exact fs call that resolves the path, because:
|
||||
* - stat / open resolve asar-ness synchronously at call time, and fd-level
|
||||
* operations (fs.read, handle.stat, fs.fstat) on the returned real fd are
|
||||
* not intercepted again;
|
||||
* - fs.createReadStream resolves asar-ness lazily when the stream opens the
|
||||
* fd, so a synchronous toggle around the call is not enough — open the fd
|
||||
* with a scoped toggle and hand the stream `{ fd }` instead;
|
||||
* - ssh2 fastPut resolves asar-ness through the synchronous `fs.open` inside
|
||||
* fastXfer, so a scoped toggle around the fastPut call covers it.
|
||||
* The toggle is always restored synchronously, so concurrent require() from
|
||||
* app.asar and other asar-aware fs users are unaffected.
|
||||
*/
|
||||
const fs = require("node:fs");
|
||||
|
||||
function withNoAsar(fn) {
|
||||
const previous = process.noAsar;
|
||||
process.noAsar = true;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
process.noAsar = previous;
|
||||
}
|
||||
}
|
||||
|
||||
/** stat() a real local file, ignoring any .asar-looking path segment. */
|
||||
function statLocal(filePath, options) {
|
||||
return withNoAsar(() => fs.promises.stat(filePath, options));
|
||||
}
|
||||
|
||||
/** Open a real local file handle (fd reads afterwards are not intercepted). */
|
||||
function openLocal(filePath, flags = "r") {
|
||||
return withNoAsar(() => fs.promises.open(filePath, flags));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read stream for a real local file. The fd is opened under a scoped noAsar
|
||||
* toggle (fs.openSync) because fs.createReadStream resolves asar-ness lazily
|
||||
* when the stream opens the fd. The stream auto-closes the fd.
|
||||
*/
|
||||
function createLocalReadStream(filePath, options = {}) {
|
||||
return withNoAsar(() => {
|
||||
const fd = fs.openSync(filePath, "r");
|
||||
try {
|
||||
return fs.createReadStream(filePath, { ...options, fd, autoClose: true });
|
||||
} catch (error) {
|
||||
try { fs.closeSync(fd); } catch { /* ignore */ }
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise-returning variant. The fd is opened asynchronously via openLocal
|
||||
* (fs.promises.open) so a slow or unresponsive filesystem (SMB/NFS, removable
|
||||
* media) never blocks the main thread on fs.openSync, and open failures reject
|
||||
* instead of throwing synchronously. Fd-level reads are not intercepted again,
|
||||
* so asar-ness stays bypassed for the whole stream lifetime.
|
||||
*/
|
||||
async function openLocalReadStream(filePath, options = {}) {
|
||||
const handle = await openLocal(filePath, "r");
|
||||
try {
|
||||
return fs.createReadStream(filePath, { ...options, fd: handle, autoClose: true });
|
||||
} catch (error) {
|
||||
// createReadStream throws synchronously on invalid options; the fd must
|
||||
// not leak since the stream was never constructed.
|
||||
try { handle.close(); } catch { /* ignore */ }
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ssh2 fastPut for a real local file. fastXfer performs its local fs.open
|
||||
* synchronously inside the call, so the scoped toggle covers the one patched
|
||||
* call it makes; subsequent reads go through the real fd.
|
||||
*/
|
||||
function fastPutLocal(sftp, localPath, remotePath, options, callback) {
|
||||
return withNoAsar(() => sftp.fastPut(localPath, remotePath, options, callback));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
withNoAsar,
|
||||
statLocal,
|
||||
openLocal,
|
||||
createLocalReadStream,
|
||||
openLocalReadStream,
|
||||
fastPutLocal,
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const {
|
||||
withNoAsar,
|
||||
statLocal,
|
||||
openLocal,
|
||||
createLocalReadStream,
|
||||
openLocalReadStream,
|
||||
fastPutLocal,
|
||||
} = require("./asarSafeFs.cjs");
|
||||
|
||||
test("withNoAsar restores the previous flag synchronously, also on throw", () => {
|
||||
assert.equal(process.noAsar, undefined);
|
||||
const value = withNoAsar(() => process.noAsar);
|
||||
assert.equal(value, true);
|
||||
assert.equal(process.noAsar, undefined);
|
||||
|
||||
const seen = withNoAsar(() => {
|
||||
const inner = process.noAsar;
|
||||
assert.equal(inner, true);
|
||||
return 42;
|
||||
});
|
||||
assert.equal(seen, 42);
|
||||
|
||||
assert.throws(() => withNoAsar(() => {
|
||||
throw new Error("boom");
|
||||
}), /boom/);
|
||||
assert.equal(process.noAsar, undefined);
|
||||
});
|
||||
|
||||
test("statLocal and openLocal read real files whose name ends with .asar", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-asar-"));
|
||||
try {
|
||||
const payload = "real-bytes-not-an-archive";
|
||||
const filePath = path.join(dir, "app.asar");
|
||||
fs.writeFileSync(filePath, payload);
|
||||
|
||||
const st = await statLocal(filePath);
|
||||
assert.equal(st.isFile(), true);
|
||||
assert.equal(st.size, payload.length);
|
||||
|
||||
const handle = await openLocal(filePath, "r");
|
||||
try {
|
||||
const hst = await handle.stat();
|
||||
assert.equal(hst.size, payload.length);
|
||||
const buf = Buffer.alloc(payload.length);
|
||||
await handle.read(buf, 0, payload.length, 0);
|
||||
assert.equal(buf.toString(), payload);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("createLocalReadStream streams real .asar-named files and closes its fd", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-asar-"));
|
||||
try {
|
||||
const payload = "stream-me";
|
||||
const filePath = path.join(dir, "real.ASAR");
|
||||
fs.writeFileSync(filePath, payload);
|
||||
|
||||
const chunks = [];
|
||||
for await (const chunk of createLocalReadStream(filePath)) {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
assert.equal(Buffer.concat(chunks).toString(), payload);
|
||||
assert.equal(process.noAsar, undefined);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("openLocalReadStream streams real .asar-named files and closes its fd", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-asar-"));
|
||||
try {
|
||||
const payload = "open-me-async";
|
||||
const filePath = path.join(dir, "app.asar");
|
||||
fs.writeFileSync(filePath, payload);
|
||||
|
||||
const chunks = [];
|
||||
const stream = await openLocalReadStream(filePath);
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
assert.equal(Buffer.concat(chunks).toString(), payload);
|
||||
assert.equal(process.noAsar, undefined);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("openLocalReadStream opens its fd asynchronously, never via fs.openSync", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-asar-"));
|
||||
try {
|
||||
const filePath = path.join(dir, "app.asar");
|
||||
fs.writeFileSync(filePath, "no-blocking-open");
|
||||
|
||||
const origOpenSync = fs.openSync;
|
||||
let openSyncCalled = false;
|
||||
fs.openSync = (...args) => {
|
||||
openSyncCalled = true;
|
||||
return origOpenSync(...args);
|
||||
};
|
||||
try {
|
||||
const chunks = [];
|
||||
const stream = await openLocalReadStream(filePath);
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
assert.equal(Buffer.concat(chunks).toString(), "no-blocking-open");
|
||||
assert.equal(openSyncCalled, false);
|
||||
} finally {
|
||||
fs.openSync = origOpenSync;
|
||||
}
|
||||
assert.equal(process.noAsar, undefined);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("open errors reject asynchronously and never leak the toggle", async () => {
|
||||
const missing = path.join(os.tmpdir(), `netcatty-asar-missing-${Date.now()}.asar`);
|
||||
await assert.rejects(() => openLocalReadStream(missing), /ENOENT/);
|
||||
assert.equal(process.noAsar, undefined);
|
||||
});
|
||||
|
||||
test("createLocalReadStream throws synchronously for missing files without leaking the toggle", () => {
|
||||
const missing = path.join(os.tmpdir(), `netcatty-asar-missing-${Date.now()}.asar`);
|
||||
assert.throws(() => createLocalReadStream(missing), /ENOENT/);
|
||||
assert.equal(process.noAsar, undefined);
|
||||
});
|
||||
|
||||
test("fastPutLocal calls fastPut synchronously with the given args and restores the flag", () => {
|
||||
const calls = [];
|
||||
const fakeSftp = {
|
||||
fastPut(localPath, remotePath, options, callback) {
|
||||
calls.push({ localPath, remotePath, options, noAsarDuringCall: process.noAsar });
|
||||
callback(null);
|
||||
},
|
||||
};
|
||||
const done = fastPutLocal(fakeSftp, "/tmp/in.asar", "/remote/out", { chunkSize: 1 }, () => {});
|
||||
assert.ok(typeof done?.then === "function" || done === undefined);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].localPath, "/tmp/in.asar");
|
||||
assert.equal(calls[0].remotePath, "/remote/out");
|
||||
assert.deepEqual(calls[0].options, { chunkSize: 1 });
|
||||
assert.equal(calls[0].noAsarDuringCall, true);
|
||||
assert.equal(process.noAsar, undefined);
|
||||
});
|
||||
|
||||
test("fastPutLocal restores the flag when fastPut throws synchronously", () => {
|
||||
const fakeSftp = {
|
||||
fastPut() {
|
||||
assert.equal(process.noAsar, true);
|
||||
throw new Error("channel gone");
|
||||
},
|
||||
};
|
||||
assert.throws(() => fastPutLocal(fakeSftp, "/tmp/in.asar", "/remote/out", {}, () => {}), /channel gone/);
|
||||
assert.equal(process.noAsar, undefined);
|
||||
});
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { statLocal, openLocalReadStream } = require("../asarSafeFs.cjs");
|
||||
const { executeBoundedSshCommand, terminateSshExecStream } = require("../boundedSshExec.cjs");
|
||||
const { invalidateSshTransport } = require("../sshTransportInvalidation.cjs");
|
||||
const {
|
||||
@@ -384,7 +385,8 @@ function createScpBackend(deps = {}) {
|
||||
async function uploadFile(localPath, remotePath, options = {}) {
|
||||
// Always use a fresh size for the SCP wire header so a shrinking/growing
|
||||
// file between enqueue and open cannot desync the remote scp -t peer.
|
||||
const st = await fsModule.promises.stat(localPath);
|
||||
// asarSafeFs: real *.asar sources must not stat/read as archives (#3450).
|
||||
const st = await statLocal(localPath);
|
||||
const hasProvidedReadStream = typeof options.openReadStream === "function";
|
||||
const fileSize = hasProvidedReadStream ? Number(options.fileSize) : st.size;
|
||||
if (!Number.isSafeInteger(fileSize) || fileSize < 0) {
|
||||
@@ -438,13 +440,78 @@ function createScpBackend(deps = {}) {
|
||||
// Arm the final ACK listener before the trailing NUL so a fast remote cannot
|
||||
// race past waitForAck and hang the upload. Race it with the stream so a
|
||||
// mid-stream remote error/cancel does not leave an unhandled rejection.
|
||||
const isCancelled = () => transfer?.cancelled || signal?.aborted;
|
||||
const finalAck = waitForAck(stream, transfer, signal);
|
||||
// Attach a rejection handler immediately: if the local open below rejects
|
||||
// (or the remote stream closes while it is pending), finalAck would
|
||||
// otherwise reject before Promise.all starts awaiting, surfacing as a
|
||||
// process-level unhandledRejection for an ordinary failed upload. The
|
||||
// rejection is also forwarded to the pending-open race below so a remote
|
||||
// disconnect settles a stalled local open instead of leaving the upload
|
||||
// waiting until the (possibly unresponsive) local filesystem answers.
|
||||
let onFinalAckFailure = null;
|
||||
void finalAck.catch((ackError) => {
|
||||
const notify = onFinalAckFailure;
|
||||
onFinalAckFailure = null;
|
||||
if (typeof notify === "function") notify(ackError);
|
||||
});
|
||||
let activeReadStream = null;
|
||||
let activeReadCompletion = Promise.resolve();
|
||||
// Open the local source asynchronously (asarSafeFs) so a slow or
|
||||
// unresponsive filesystem never blocks the main thread; open failures
|
||||
// reject into the outer catch, which aborts the scp stream. Race the
|
||||
// open against cancellation so a stalled open (e.g. an unresponsive
|
||||
// NFS/SMB mount) cannot keep the transfer stuck after the user cancels:
|
||||
// the race rejects immediately, and a file descriptor that only arrives
|
||||
// after cancellation is destroyed (closing its fd) instead of leaking.
|
||||
// The race also settles on an early finalAck rejection (remote close).
|
||||
const openedReadStream = await new Promise((resolve, reject) => {
|
||||
const openPromise = hasProvidedReadStream
|
||||
? Promise.resolve().then(() => options.openReadStream())
|
||||
: openLocalReadStream(localPath, { highWaterMark: 256 * 1024 });
|
||||
let settled = false;
|
||||
let poll = null;
|
||||
const settle = (fn, value) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
onFinalAckFailure = null;
|
||||
if (poll) {
|
||||
clearInterval(poll);
|
||||
poll = null;
|
||||
}
|
||||
fn(value);
|
||||
};
|
||||
openPromise.then(
|
||||
(result) => {
|
||||
const readStream = result?.stream || result;
|
||||
if (isCancelled()) {
|
||||
try { readStream?.destroy?.(); } catch { /* ignore */ }
|
||||
settle(reject, new Error("Transfer cancelled"));
|
||||
return;
|
||||
}
|
||||
if (settled) {
|
||||
// The race already settled (e.g. via remote ACK failure); the
|
||||
// late-arriving descriptor must be destroyed instead of leaking.
|
||||
try { readStream?.destroy?.(); } catch { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
settle(resolve, result);
|
||||
},
|
||||
(err) => settle(reject, err),
|
||||
);
|
||||
const cancel = (err) => settle(reject, err || new Error("Transfer cancelled"));
|
||||
onFinalAckFailure = (ackError) => cancel(ackError);
|
||||
if (isCancelled()) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
// Poll the cancel flag so aborts work even while the open is pending.
|
||||
poll = setInterval(() => {
|
||||
if (isCancelled()) cancel();
|
||||
}, 25);
|
||||
if (typeof poll.unref === "function") poll.unref();
|
||||
});
|
||||
const streamDone = new Promise((resolve, reject) => {
|
||||
const openedReadStream = hasProvidedReadStream
|
||||
? options.openReadStream()
|
||||
: fsModule.createReadStream(localPath, { highWaterMark: 256 * 1024 });
|
||||
const readStream = openedReadStream?.stream || openedReadStream;
|
||||
activeReadCompletion = Promise.resolve(openedReadStream?.completed);
|
||||
activeReadStream = readStream;
|
||||
|
||||
@@ -16,6 +16,12 @@ const {
|
||||
resolveEncodingForRequest,
|
||||
} = require("./sftpBridge.cjs");
|
||||
const { isScpModeClient, getScpBackendForClient } = require("./sftpBridge/scpBackend.cjs");
|
||||
const {
|
||||
statLocal,
|
||||
openLocal,
|
||||
openLocalReadStream,
|
||||
fastPutLocal,
|
||||
} = require("./asarSafeFs.cjs");
|
||||
const { executeBoundedSshCommand } = require("./boundedSshExec.cjs");
|
||||
const { openBoundedSftpChannel } = require("./boundedSftpOpen.cjs");
|
||||
const {
|
||||
@@ -730,14 +736,96 @@ async function hashReadable(readable, options = {}) {
|
||||
|
||||
const EMPTY_SHA256_HEX = crypto.createHash("sha256").update("").digest("hex");
|
||||
|
||||
function hashLocalPrefix(filePath, bytes, options) {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return Promise.resolve(null);
|
||||
if (bytes === 0) return Promise.resolve(EMPTY_SHA256_HEX);
|
||||
return hashReadable(fs.createReadStream(filePath, { start: 0, end: bytes - 1 }), options);
|
||||
function localStreamCancellationError() {
|
||||
const error = new Error("Transfer cancelled");
|
||||
error.code = "ABORT_ERR";
|
||||
return error;
|
||||
}
|
||||
|
||||
function hashLocalFile(filePath, options = {}) {
|
||||
return hashReadable(fs.createReadStream(filePath), options);
|
||||
// Local fingerprint reads go through asarSafeFs, whose fd open is asynchronous
|
||||
// and can stay pending for a long time on an unresponsive filesystem (NFS/SMB,
|
||||
// removable media). Race that open against the cancellation signal so aborting
|
||||
// the transfer rejects immediately instead of waiting for the open to settle;
|
||||
// a stream that only arrives after cancellation is destroyed (closing its fd)
|
||||
// instead of leaking. hashReadable() only covers the post-open stream lifetime.
|
||||
function raceLocalStreamOpenAgainstAbort(openPromise, signal) {
|
||||
if (!signal) return openPromise;
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const destroyLateStream = (stream) => {
|
||||
try { stream?.destroy?.(); } catch { /* ignore */ }
|
||||
};
|
||||
const onAbort = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (lateStream) destroyLateStream(lateStream);
|
||||
reject(localStreamCancellationError());
|
||||
};
|
||||
let lateStream = null;
|
||||
signal.addEventListener?.("abort", onAbort, { once: true });
|
||||
// AbortSignal does not replay its event: if the signal was already aborted
|
||||
// (e.g. while a preceding statLocal() was pending), the listener above will
|
||||
// never fire. Reject immediately so a stalled open cannot leave the request
|
||||
// pending; a stream that arrives later is still destroyed via lateStream.
|
||||
if (signal.aborted) {
|
||||
settled = true;
|
||||
signal.removeEventListener?.("abort", onAbort);
|
||||
reject(localStreamCancellationError());
|
||||
// Still observe the pending open: destroy a late-resolving stream so its
|
||||
// fd is closed, and swallow its rejection so it cannot surface as a
|
||||
// process-level unhandledRejection.
|
||||
openPromise.then(
|
||||
(stream) => { destroyLateStream(stream); },
|
||||
() => { /* already rejected with the cancellation error */ },
|
||||
);
|
||||
return;
|
||||
}
|
||||
openPromise.then(
|
||||
(stream) => {
|
||||
lateStream = stream;
|
||||
if (settled) {
|
||||
destroyLateStream(stream);
|
||||
return;
|
||||
}
|
||||
if (signal.aborted) {
|
||||
settled = true;
|
||||
destroyLateStream(stream);
|
||||
signal.removeEventListener?.("abort", onAbort);
|
||||
reject(localStreamCancellationError());
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener?.("abort", onAbort);
|
||||
resolve(stream);
|
||||
},
|
||||
(error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
signal.removeEventListener?.("abort", onAbort);
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Local source reads go through asarSafeFs so real files named *.asar are not
|
||||
// mistaken for asar archives by Electron's fs wrapper (#3450).
|
||||
async function hashLocalPrefix(filePath, bytes, options) {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return Promise.resolve(null);
|
||||
if (bytes === 0) return Promise.resolve(EMPTY_SHA256_HEX);
|
||||
const stream = await raceLocalStreamOpenAgainstAbort(
|
||||
openLocalReadStream(filePath, { start: 0, end: bytes - 1 }),
|
||||
options?.signal,
|
||||
);
|
||||
return hashReadable(stream, options);
|
||||
}
|
||||
|
||||
async function hashLocalFile(filePath, options = {}) {
|
||||
const stream = await raceLocalStreamOpenAgainstAbort(
|
||||
openLocalReadStream(filePath),
|
||||
options.signal,
|
||||
);
|
||||
return hashReadable(stream, options);
|
||||
}
|
||||
|
||||
async function hashRemotePrefixViaSshCommand(client, remotePath, bytes, options = {}) {
|
||||
@@ -2204,7 +2292,9 @@ async function uploadViaFastPut(localPath, remotePath, sftp, fileSize, transfer,
|
||||
return;
|
||||
}
|
||||
|
||||
sftp.fastPut(localPath, remotePath, {
|
||||
// ssh2 resolves asar-ness via fs.open inside fastXfer (synchronous), so a
|
||||
// scoped noAsar toggle makes real *.asar files readable (#3450).
|
||||
fastPutLocal(sftp, localPath, remotePath, {
|
||||
chunkSize: TRANSFER_CHUNK_SIZE,
|
||||
concurrency: UPLOAD_TRANSFER_CONCURRENCY,
|
||||
step: (transferred, _chunk, total) => {
|
||||
@@ -2274,8 +2364,11 @@ async function uploadFile(
|
||||
transfer,
|
||||
encoding,
|
||||
signal: transfer.signal,
|
||||
openReadStream: () => {
|
||||
const stream = fs.createReadStream(localPath, { highWaterMark: 256 * 1024 });
|
||||
openReadStream: async () => {
|
||||
// asarSafeFs: real *.asar sources must not be read as archives (#3450).
|
||||
// Open the fd asynchronously (fs.promises.open) so a slow or
|
||||
// unresponsive filesystem never blocks the main thread on fs.openSync.
|
||||
const stream = await openLocalReadStream(localPath, { highWaterMark: 256 * 1024 });
|
||||
return { stream, completed: Promise.resolve() };
|
||||
},
|
||||
onProgress: (transferred, total) => sendProgress(transferred, total || fileSize),
|
||||
@@ -2291,7 +2384,7 @@ async function uploadFile(
|
||||
transfer.pauseSupported = Boolean(transfer.resumable);
|
||||
const originalLocalPath = localPath;
|
||||
const initialSource = (transfer.resumable || !transfer.sourceIsOwnedTemp)
|
||||
? await fs.promises.stat(originalLocalPath)
|
||||
? await statLocal(originalLocalPath)
|
||||
: null;
|
||||
|
||||
/** @type {Error | null} */
|
||||
@@ -2307,7 +2400,7 @@ async function uploadFile(
|
||||
|
||||
const finishSuccessfulUpload = async () => {
|
||||
if (initialSource) {
|
||||
const latestSource = await fs.promises.stat(originalLocalPath);
|
||||
const latestSource = await statLocal(originalLocalPath);
|
||||
// Soft size + mtime/ino (no full-file re-hash). Do not claim separate
|
||||
// content proof — same-size rewrites that bump mtime still fail closed.
|
||||
// ignoreCtime: macOS xattr/Spotlight noise must not abort a true match.
|
||||
@@ -4435,7 +4528,8 @@ async function uploadFileConcurrent(
|
||||
try {
|
||||
if (transfer.cancelled) throw new Error("Transfer cancelled");
|
||||
try {
|
||||
localHandle = await fs.promises.open(localPath, "r");
|
||||
// asarSafeFs: real *.asar sources must not be opened as archives (#3450).
|
||||
localHandle = await openLocal(localPath, "r");
|
||||
} catch (error) {
|
||||
failed = true;
|
||||
noTransferFallback = true;
|
||||
@@ -5510,7 +5604,7 @@ async function startTransferNow(event, payload, onProgress) {
|
||||
const SOFT_RESUME_SAMPLE_BYTES = 256 * 1024;
|
||||
const readSourceSoftIdentity = async () => {
|
||||
if (sourceType === "local") {
|
||||
const st = await fs.promises.stat(sourcePath);
|
||||
const st = await statLocal(sourcePath);
|
||||
const sampleBytes = Math.min(SOFT_RESUME_SAMPLE_BYTES, Math.max(0, st.size));
|
||||
const sample = sampleBytes > 0
|
||||
? await hashLocalPrefix(sourcePath, sampleBytes, { signal: transfer.signal })
|
||||
@@ -5778,7 +5872,8 @@ async function startTransferNow(event, payload, onProgress) {
|
||||
|
||||
if (!hasExplicitTotal) {
|
||||
if (sourceType === 'local') {
|
||||
const stat = await fs.promises.stat(sourcePath);
|
||||
// asarSafeFs: local *.asar sources stat as archive roots otherwise (#3450).
|
||||
const stat = await statLocal(sourcePath);
|
||||
fileSize = stat.size;
|
||||
} else if (sourceType === 'sftp') {
|
||||
const client = sftpClients.get(sourceSftpId);
|
||||
|
||||
@@ -608,7 +608,7 @@ test("SCP upload streams the live local path without a pre-hash digest", async (
|
||||
uploadSourcePath = sourcePath;
|
||||
// May still receive a plain file stream helper; must not require a digest.
|
||||
if (typeof options.openReadStream === "function") {
|
||||
const opened = options.openReadStream();
|
||||
const opened = await options.openReadStream();
|
||||
const chunks = [];
|
||||
for await (const chunk of opened.stream) chunks.push(chunk);
|
||||
remoteFiles.set(remotePath, Buffer.concat(chunks));
|
||||
@@ -670,7 +670,7 @@ test("SCP staged streaming preserves executable mode without a local snapshot",
|
||||
uploadedSourcePath = sourcePath;
|
||||
uploadedMode = (await fs.promises.stat(sourcePath)).mode & 0o777;
|
||||
assert.equal(options.fileSize, payload.length);
|
||||
const opened = options.openReadStream();
|
||||
const opened = await options.openReadStream();
|
||||
const chunks = [];
|
||||
for await (const chunk of opened.stream) chunks.push(chunk);
|
||||
remoteFiles.set(remotePath, Buffer.concat(chunks));
|
||||
@@ -744,7 +744,7 @@ test("SCP staged upload rechecks a deleted destination after mode setup", async
|
||||
};
|
||||
},
|
||||
async uploadFile(_sourcePath, remotePath, options) {
|
||||
const opened = options.openReadStream();
|
||||
const opened = await options.openReadStream();
|
||||
const chunks = [];
|
||||
for await (const chunk of opened.stream) chunks.push(chunk);
|
||||
remoteFiles.set(remotePath, Buffer.concat(chunks));
|
||||
@@ -813,7 +813,7 @@ test("SCP staged upload does not promote when AbortSignal cancels mid-stream", a
|
||||
return { type: "file", isDirectory: false, size: remoteFiles.get(remotePath).length };
|
||||
},
|
||||
async uploadFile(_sourcePath, remotePath, options) {
|
||||
const opened = options.openReadStream();
|
||||
const opened = await options.openReadStream();
|
||||
const chunks = [];
|
||||
let first = true;
|
||||
for await (const chunk of opened.stream) {
|
||||
|
||||
Reference in new issue
Block a user