Compare commits

...
Author SHA1 Message Date
Cursor Agentand陈大猫 ab3ff2a151 fix(sftp): omit totalBytes on temp download for external editor
downloadToTempWithProgress passed totalBytes:0, which transferBridge
treats as an explicit empty snapshot and skips the remote body, so
external editors opened empty files (fixes #2787).

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-08-07 08:44:04 +00:00
3 changed files with 73 additions and 3 deletions

No files matched your search

@@ -153,6 +153,8 @@ test("the single temp-download entry routes direct downloads through the shared
cancelled: false,
});
assert.equal(observed.event.sender.id, 1);
// Omit totalBytes so transferBridge discovers remote size. Explicit 0 means
// a planned empty snapshot and would skip the remote body (#2787).
assert.deepEqual(observed.payload, {
transferId: "editor-download-1",
sourcePath: "/remote/report.bin",
@@ -161,8 +163,12 @@ test("the single temp-download entry routes direct downloads through the shared
targetType: "local",
sourceSftpId: "sftp-1",
sourceEncoding: "utf-8",
totalBytes: 0,
});
assert.equal(
Object.prototype.hasOwnProperty.call(observed.payload, "totalBytes"),
false,
"downloadToTempWithProgress must not send totalBytes (especially not 0)",
);
});
test("the retired no-progress temp-download IPC is not registered", async (t) => {
@@ -227,8 +233,12 @@ test("downloadToTempWithProgress proxies transfer work to the terminal worker in
targetType: "local",
sourceSftpId: "worker-sftp-1",
sourceEncoding: "utf-8",
totalBytes: 0,
});
assert.equal(
Object.prototype.hasOwnProperty.call(child.messages[0].payload, "totalBytes"),
false,
"worker-mode downloadToTempWithProgress must not send totalBytes",
);
child.emit("message", {
kind: "response",
requestId: child.messages[0].requestId,
+59
View File
@@ -4462,6 +4462,65 @@ test("zero-byte snapshot is complete when remote has grown (no body open)", asyn
assert.equal(downloaded.length, 0);
});
test("omitted totalBytes discovers remote size and downloads non-empty body", async (t) => {
// Explicit totalBytes:0 is an empty snapshot; omitting totalBytes must STAT
// and READ the remote (external-editor temp download path, #2787).
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-omit-total-"));
const transferId = "download-omit-total-bytes";
const targetPath = path.join(tempDir, "report.bin");
const stagedPath = tempDirBridge.getTransferTempFilePath(transferId, path.basename(targetPath));
t.after(async () => {
await fs.promises.rm(tempDir, { recursive: true, force: true });
await fs.promises.rm(stagedPath, { force: true }).catch(() => {});
});
const expected = Buffer.from("external-editor-contents");
let statCalls = 0;
let bodyOpened = false;
const { sftp: sharedSftp } = createPipelinedDownloadSftp(expected, {
open(_remotePath, flags, callback) {
bodyOpened = true;
if (typeof flags === "function") {
flags(null, Buffer.from("read-handle"));
return;
}
callback(null, Buffer.from("read-handle"));
},
});
const client = {
sftp: sharedSftp,
stat() {
statCalls += 1;
return Promise.resolve({
size: expected.length,
mtimeMs: 1_000,
ctimeMs: 1_000,
mtime: 1,
ctime: 1,
});
},
};
transferBridge.init({ sftpClients: new Map([["source", client]]) });
const result = await transferBridge.startTransfer(
{ sender: createSender() },
{
transferId,
sourcePath: "/remote/report.bin",
targetPath,
sourceType: "sftp",
targetType: "local",
sourceSftpId: "source",
// intentionally omit totalBytes
},
);
assert.equal(result.error, undefined, result.error);
assert.ok(statCalls >= 1, "omitted totalBytes must discover remote size via STAT");
assert.equal(bodyOpened, true, "omitted totalBytes must open remote body for non-empty file");
assert.deepEqual(await fs.promises.readFile(targetPath), expected);
});
test("SFTP uploads fail when remote size does not match local size", async (t) => {
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-transfer-size-test-"));
t.after(async () => {
+2 -1
View File
@@ -1072,6 +1072,8 @@ function createBridgeRegistrar(context) {
};
try {
// Omit totalBytes so transferBridge STATs the remote size. Explicit 0
// means a planned empty snapshot and skips the remote body (#2787).
const payload = {
transferId,
sourcePath: remotePath,
@@ -1080,7 +1082,6 @@ function createBridgeRegistrar(context) {
targetType: "local",
sourceSftpId: sftpId,
sourceEncoding: encoding,
totalBytes: 0,
};
const result = terminalWorkerManager