Fix Windows file delete command (#1005)

This commit is contained in:
ZacharyZcR
2026-07-10 08:02:01 +08:00
committed by GitHub
parent 017be33974
commit b9a995c9cb
3 changed files with 163 additions and 95 deletions
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { buildDeleteCommand } from "./file-manager-operation-commands.js";
describe("buildDeleteCommand", () => {
it("builds a PowerShell 5.1 compatible delete command for Windows files", () => {
const command = buildDeleteCommand(
"/C:/Users/Administrator/test.txt",
false,
);
expect(command.command).toBe(
"Remove-Item -LiteralPath 'C:\\Users\\Administrator\\test.txt' -Force -ErrorAction Stop",
);
expect(command.commandWithSuccess).toBe(
`${command.command}; if ($?) { Write-Output "SUCCESS" }`,
);
expect(command.commandWithSuccess).not.toContain("&&");
});
it("adds recursive deletion for Windows directories", () => {
const command = buildDeleteCommand("C:/Temp/Folder", true);
expect(command.command).toBe(
"Remove-Item -LiteralPath 'C:\\Temp\\Folder' -Recurse -Force -ErrorAction Stop",
);
});
it("escapes single quotes in Windows literal paths", () => {
const command = buildDeleteCommand("/C:/Temp/O'Brien.txt", false);
expect(command.command).toBe(
"Remove-Item -LiteralPath 'C:\\Temp\\O''Brien.txt' -Force -ErrorAction Stop",
);
});
it("keeps POSIX delete commands using shell success chaining", () => {
const command = buildDeleteCommand("/tmp/O'Brien.txt", false);
expect(command.command).toBe("rm -f '/tmp/O'\"'\"'Brien.txt'");
expect(command.commandWithSuccess).toBe(
`${command.command} && echo "SUCCESS"`,
);
});
});
@@ -0,0 +1,39 @@
import { isWindowsSftpPath, sftpPathToLocalPath } from "./transfer-paths.js";
export interface DeleteCommand {
command: string;
commandWithSuccess: string;
}
function quotePosixPath(path: string): string {
return `'${path.replace(/'/g, "'\"'\"'")}'`;
}
function quotePowerShellLiteral(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
export function buildDeleteCommand(
itemPath: string,
isDirectory: boolean,
): DeleteCommand {
if (isWindowsSftpPath(itemPath)) {
const path = quotePowerShellLiteral(sftpPathToLocalPath(itemPath));
const command = isDirectory
? `Remove-Item -LiteralPath ${path} -Recurse -Force -ErrorAction Stop`
: `Remove-Item -LiteralPath ${path} -Force -ErrorAction Stop`;
return {
command,
commandWithSuccess: `${command}; if ($?) { Write-Output "SUCCESS" }`,
};
}
const path = quotePosixPath(itemPath);
const command = isDirectory ? `rm -rf ${path}` : `rm -f ${path}`;
return {
command,
commandWithSuccess: `${command} && echo "SUCCESS"`,
};
}
@@ -6,7 +6,7 @@ import {
execWithSudo,
type SSHSession,
} from "./file-manager-session.js";
import { isWindowsSftpPath } from "./transfer-paths.js";
import { buildDeleteCommand } from "./file-manager-operation-commands.js";
type FileOperationRoutesDeps = {
sshSessions: Record<string, SSHSession>;
@@ -375,22 +375,10 @@ export function registerFileOperationRoutes(
});
sshConn.lastActive = Date.now();
const isWindowsPath = isWindowsSftpPath(itemPath);
let deleteCommand: string;
if (isWindowsPath) {
const winPath = itemPath
.replace(/\//g, "\\")
.replace(/^\\([A-Za-z]:\\)/, "$1");
const escapedWinPath = winPath.replace(/"/g, '""');
deleteCommand = isDirectory
? `rd /s /q "${escapedWinPath}"`
: `del /f /q "${escapedWinPath}"`;
} else {
const escapedPath = itemPath.replace(/'/g, "'\"'\"'");
deleteCommand = isDirectory
? `rm -rf '${escapedPath}'`
: `rm -f '${escapedPath}'`;
}
const { command: deleteCommand, commandWithSuccess } = buildDeleteCommand(
itemPath,
Boolean(isDirectory),
);
const executeDelete = (useSudo: boolean): Promise<void> => {
return new Promise((resolve) => {
@@ -421,10 +409,7 @@ export function registerFileOperationRoutes(
return;
}
execChannel(
sshConn,
`${deleteCommand} && echo "SUCCESS"`,
(err, stream) => {
execChannel(sshConn, commandWithSuccess, (err, stream) => {
if (err) {
fileLogger.error("SSH deleteItem error:", err);
res.status(500).json({ error: err.message });
@@ -502,8 +487,7 @@ export function registerFileOperationRoutes(
.json({ error: `Stream error: ${streamErr.message}` });
resolve();
});
},
);
});
});
};