mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-29 18:11:40 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28ffdc5e9b |
No files matched your search
@@ -1656,21 +1656,27 @@ async function runRemoteUploadTransaction(client, localPath, remotePath, options
|
||||
signal,
|
||||
));
|
||||
assertCanPromote();
|
||||
// Apply the old mode to the stage before promotion. A failed chmod must not
|
||||
// replace an executable final with a non-executable file and report success.
|
||||
await restoreRemoteMode(client, encodedStagedPath, plan.existingMode, {
|
||||
bestEffort: false,
|
||||
});
|
||||
assertCanPromote();
|
||||
await runCancelablePreflight(() => assertStagedPromotionTargetSafe(
|
||||
client,
|
||||
encodedPath,
|
||||
remotePath,
|
||||
plan.destinationExisted,
|
||||
plan.destinationSnapshot,
|
||||
signal,
|
||||
));
|
||||
assertCanPromote();
|
||||
// Mode restore is a remote chmod on the stage. Recheck the destination
|
||||
// only after that round trip. A non-finite existingMode (new files, and
|
||||
// targets whose mode could not be captured) makes restore a no-op, so a
|
||||
// second lstat would be a serial RTT on every such upload.
|
||||
if (Number.isFinite(plan.existingMode)) {
|
||||
// Apply the old mode to the stage before promotion. A failed chmod must not
|
||||
// replace an executable final with a non-executable file and report success.
|
||||
await restoreRemoteMode(client, encodedStagedPath, plan.existingMode, {
|
||||
bestEffort: false,
|
||||
});
|
||||
assertCanPromote();
|
||||
await runCancelablePreflight(() => assertStagedPromotionTargetSafe(
|
||||
client,
|
||||
encodedPath,
|
||||
remotePath,
|
||||
plan.destinationExisted,
|
||||
plan.destinationSnapshot,
|
||||
signal,
|
||||
));
|
||||
assertCanPromote();
|
||||
}
|
||||
commitPromotion();
|
||||
await renameRemotePath(client, encodedStagedPath, encodedPath, encodedBackupPath, {
|
||||
stagePath: stagedLogical,
|
||||
|
||||
@@ -571,6 +571,104 @@ test("existing destinations stage then restore mode after rename", async (t) =>
|
||||
assert.equal(remoteMeta.get("/usr/local/bin/tool")?.mode & 0o777, 0o755);
|
||||
});
|
||||
|
||||
test("new-file staged upload lstats the destination once before rename", async (t) => {
|
||||
const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-new-file-lstat-"));
|
||||
t.after(async () => {
|
||||
await fs.promises.rm(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
tempDirBridge.init?.({ getPath: () => tempRoot });
|
||||
|
||||
const localPath = path.join(tempRoot, "payload.bin");
|
||||
const payload = Buffer.from("new-file");
|
||||
await fs.promises.writeFile(localPath, payload);
|
||||
const remotePath = "/home/alice/new-file.bin";
|
||||
|
||||
const { channel, remoteFiles, chmodCalls } = createSessionChannel();
|
||||
const originalLstat = channel.lstat.bind(channel);
|
||||
let destinationLstats = 0;
|
||||
channel.lstat = (targetPath, callback) => {
|
||||
if (String(targetPath) === remotePath) destinationLstats += 1;
|
||||
originalLstat(targetPath, callback);
|
||||
};
|
||||
|
||||
const sftpClients = new Map();
|
||||
sftpBridge.init({
|
||||
electronModule: { webContents: { fromId: () => null } },
|
||||
sessions: new Map([["session-new-file-lstat", { conn: { sftp: (cb) => cb(null, channel) } }]]),
|
||||
sftpClients,
|
||||
});
|
||||
const opened = await sftpBridge.openSftpForSession(null, {
|
||||
sessionId: "session-new-file-lstat",
|
||||
fileProtocol: "sftp",
|
||||
});
|
||||
|
||||
const result = await sftpBridge.uploadLocalToSftp(null, {
|
||||
sftpId: opened.sftpId,
|
||||
localPath,
|
||||
remotePath,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.deepEqual(remoteFiles.get(remotePath), payload);
|
||||
assert.deepEqual(chmodCalls, []);
|
||||
// Plan lstat plus one pre-rename check. existingMode is null, so the
|
||||
// post-restore lstat would be a no-op round trip.
|
||||
assert.equal(destinationLstats, 2);
|
||||
});
|
||||
|
||||
test("existing-file staged upload rechecks the destination after mode restore", async (t) => {
|
||||
const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-existing-file-lstat-"));
|
||||
t.after(async () => {
|
||||
await fs.promises.rm(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
tempDirBridge.init?.({ getPath: () => tempRoot });
|
||||
|
||||
const localPath = path.join(tempRoot, "tool.bin");
|
||||
const payload = Buffer.from("replaced-tool");
|
||||
await fs.promises.writeFile(localPath, payload);
|
||||
const remotePath = "/usr/local/bin/tool";
|
||||
|
||||
const { channel, remoteFiles, remoteMeta, chmodCalls } = createSessionChannel();
|
||||
remoteFiles.set(remotePath, Buffer.from("old-tool"));
|
||||
remoteMeta.set(remotePath, { mode: 0o100755, mtime: 1, uid: 1000, gid: 1000 });
|
||||
const originalLstat = channel.lstat.bind(channel);
|
||||
let destinationLstats = 0;
|
||||
channel.lstat = (targetPath, callback) => {
|
||||
if (String(targetPath) === remotePath) destinationLstats += 1;
|
||||
originalLstat(targetPath, callback);
|
||||
};
|
||||
|
||||
const sftpClients = new Map();
|
||||
sftpBridge.init({
|
||||
electronModule: { webContents: { fromId: () => null } },
|
||||
sessions: new Map([["session-existing-file-lstat", { conn: { sftp: (cb) => cb(null, channel) } }]]),
|
||||
sftpClients,
|
||||
});
|
||||
const opened = await sftpBridge.openSftpForSession(null, {
|
||||
sessionId: "session-existing-file-lstat",
|
||||
fileProtocol: "sftp",
|
||||
});
|
||||
|
||||
const result = await sftpBridge.uploadLocalToSftp(null, {
|
||||
sftpId: opened.sftpId,
|
||||
localPath,
|
||||
remotePath,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.deepEqual(remoteFiles.get(remotePath), payload);
|
||||
assert.equal(remoteMeta.get(remotePath)?.mode & 0o777, 0o755);
|
||||
assert.ok(
|
||||
chmodCalls.some((call) => String(call.targetPath).includes(".netcatty-upload-")),
|
||||
`expected staged chmod, got ${JSON.stringify(chmodCalls)}`,
|
||||
);
|
||||
// Plan lstat, pre-restore check, and the post-chmod recheck that covers the
|
||||
// mode-restore round trip.
|
||||
assert.equal(destinationLstats, 3);
|
||||
});
|
||||
|
||||
test("mode restore failure leaves the existing destination untouched", async (t) => {
|
||||
const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "netcatty-stage-mode-fail-"));
|
||||
t.after(async () => {
|
||||
|
||||
Reference in new issue
Block a user