Skip to content

Commit 076ea11

Browse files
committed
feat: US-020 - Add SFTP large file transfer and rename e2e-docker fixture
1 parent b106eba commit 076ea11

5 files changed

Lines changed: 116 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"entry": "src/index.js",
3+
"expectation": "pass",
4+
"services": ["ssh"]
5+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "e2e-docker-ssh2-sftp-large",
3+
"private": true,
4+
"type": "commonjs",
5+
"dependencies": {
6+
"ssh2": "1.17.0"
7+
}
8+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
const { createHash } = require("crypto");
2+
const { Client } = require("ssh2");
3+
4+
// Generate deterministic 1MB payload
5+
function generatePayload(size) {
6+
const buf = Buffer.alloc(size);
7+
for (let i = 0; i < size; i++) {
8+
buf[i] = i & 0xff;
9+
}
10+
return buf;
11+
}
12+
13+
function hashBuffer(buf) {
14+
return createHash("sha256").update(buf).digest("hex");
15+
}
16+
17+
async function main() {
18+
const PAYLOAD_SIZE = 1024 * 1024; // 1MB
19+
const payload = generatePayload(PAYLOAD_SIZE);
20+
const uploadHash = hashBuffer(payload);
21+
22+
const result = await new Promise((resolve, reject) => {
23+
const conn = new Client();
24+
25+
conn.on("ready", () => {
26+
conn.sftp((err, sftp) => {
27+
if (err) return reject(err);
28+
29+
const uploadPath = "/home/testuser/upload/large-test.bin";
30+
const renamedPath = "/home/testuser/upload/large-renamed.bin";
31+
32+
// Upload via createWriteStream
33+
const ws = sftp.createWriteStream(uploadPath);
34+
ws.on("error", reject);
35+
ws.end(payload, () => {
36+
// Stat uploaded file
37+
sftp.stat(uploadPath, (err, uploadStats) => {
38+
if (err) return reject(err);
39+
40+
// Rename the file
41+
sftp.rename(uploadPath, renamedPath, (err) => {
42+
if (err) return reject(err);
43+
44+
// Download via createReadStream
45+
const rs = sftp.createReadStream(renamedPath);
46+
const chunks = [];
47+
rs.on("data", (chunk) => chunks.push(chunk));
48+
rs.on("error", reject);
49+
rs.on("end", () => {
50+
const downloaded = Buffer.concat(chunks);
51+
const downloadHash = hashBuffer(downloaded);
52+
53+
// Cleanup
54+
sftp.unlink(renamedPath, (err) => {
55+
conn.end();
56+
if (err) return reject(err);
57+
resolve({
58+
uploadSize: PAYLOAD_SIZE,
59+
uploadHash,
60+
statSize: uploadStats.size,
61+
downloadSize: downloaded.length,
62+
downloadHash,
63+
hashMatch: uploadHash === downloadHash,
64+
renamed: true,
65+
});
66+
});
67+
});
68+
});
69+
});
70+
});
71+
});
72+
});
73+
74+
conn.on("error", reject);
75+
76+
conn.connect({
77+
host: process.env.SSH_HOST,
78+
port: Number(process.env.SSH_PORT),
79+
username: "testuser",
80+
password: "testpass",
81+
});
82+
});
83+
84+
console.log(JSON.stringify(result));
85+
}
86+
87+
main().catch((err) => {
88+
console.error(err.message);
89+
process.exit(1);
90+
});

progress.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2719,3 +2719,14 @@ PRD: ralph/kernel-hardening (46 stories)
27192719
- For error path fixtures, only serialize err.message (and err.level for ssh2) — err.code/err.errno are missing in sandbox
27202720
- ssh2 auth failure emits error with level="client-authentication" — useful for distinguishing auth errors from connection errors
27212721
---
2722+
2723+
## 2026-03-19 - US-020
2724+
- Added SFTP large file transfer and rename e2e-docker fixture
2725+
- Fixture generates deterministic 1MB payload (byte pattern: i & 0xFF), uploads via createWriteStream, renames via sftp.rename(), downloads via createReadStream, verifies integrity via SHA-256 hash comparison
2726+
- Files created: packages/secure-exec/tests/e2e-docker/ssh2-sftp-large/{package.json,fixture.json,src/index.js}
2727+
- Host and sandbox produce identical output; parity test passes (~4.2s)
2728+
- **Learnings for future iterations:**
2729+
- createWriteStream/createReadStream for SFTP work through the sandbox net bridge without issues — TCP buffer management handles 1MB+ transfers
2730+
- sftp.rename() works through the sandbox with no additional bridge work needed
2731+
- Deterministic payload generation (byte pattern loop) is simpler and more reliable than random data for parity testing
2732+
---

scripts/ralph/prd.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,8 @@
357357
"Tests pass"
358358
],
359359
"priority": 20,
360-
"passes": false,
361-
"notes": "The current SFTP fixture only transfers 18 bytes. Larger transfers stress the sandbox's TCP buffer management, stream backpressure handling, and memory limits. Also tests createReadStream (streaming) which exercises different buffer management than readFile (buffered)."
360+
"passes": true,
361+
"notes": "Completed. Fixture ssh2-sftp-large generates deterministic 1MB payload, uploads via createWriteStream, renames via sftp.rename(), downloads via createReadStream, verifies integrity via SHA-256 hash comparison. Host and sandbox produce identical output."
362362
},
363363
{
364364
"id": "US-021",

0 commit comments

Comments
 (0)