mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 04:43:36 +00:00
Fix Windows file delete command (#1005)
This commit is contained in:
@@ -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,
|
execWithSudo,
|
||||||
type SSHSession,
|
type SSHSession,
|
||||||
} from "./file-manager-session.js";
|
} from "./file-manager-session.js";
|
||||||
import { isWindowsSftpPath } from "./transfer-paths.js";
|
import { buildDeleteCommand } from "./file-manager-operation-commands.js";
|
||||||
|
|
||||||
type FileOperationRoutesDeps = {
|
type FileOperationRoutesDeps = {
|
||||||
sshSessions: Record<string, SSHSession>;
|
sshSessions: Record<string, SSHSession>;
|
||||||
@@ -375,22 +375,10 @@ export function registerFileOperationRoutes(
|
|||||||
});
|
});
|
||||||
sshConn.lastActive = Date.now();
|
sshConn.lastActive = Date.now();
|
||||||
|
|
||||||
const isWindowsPath = isWindowsSftpPath(itemPath);
|
const { command: deleteCommand, commandWithSuccess } = buildDeleteCommand(
|
||||||
let deleteCommand: string;
|
itemPath,
|
||||||
if (isWindowsPath) {
|
Boolean(isDirectory),
|
||||||
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 executeDelete = (useSudo: boolean): Promise<void> => {
|
const executeDelete = (useSudo: boolean): Promise<void> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -421,10 +409,7 @@ export function registerFileOperationRoutes(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
execChannel(
|
execChannel(sshConn, commandWithSuccess, (err, stream) => {
|
||||||
sshConn,
|
|
||||||
`${deleteCommand} && echo "SUCCESS"`,
|
|
||||||
(err, stream) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
fileLogger.error("SSH deleteItem error:", err);
|
fileLogger.error("SSH deleteItem error:", err);
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
@@ -502,8 +487,7 @@ export function registerFileOperationRoutes(
|
|||||||
.json({ error: `Stream error: ${streamErr.message}` });
|
.json({ error: `Stream error: ${streamErr.message}` });
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user