LANCommander/LANCommander.UI/Components/CodeInput/FileDropHandler.ts
Pat Hartl ca713f6d3f Rework .reg importing in script editor
Script editor now supports drag and drop for .ps1, .reg, and other text files. .ps1 files will be handled by replacing the contents of the text editor. .reg files will be converted to PowerShell commands and inserted into the script editor. Any other text file will be inserted as an escaped string.
2026-06-10 20:19:06 -05:00

244 lines
7.1 KiB
TypeScript

declare const monaco: any;
export function SetupFileDropHandler(): void {
if (typeof monaco === "undefined")
return;
const editors = monaco.editor.getEditors();
const editor = editors[editors.length - 1];
if (!editor)
return;
const domNode = editor.getDomNode();
if (!domNode)
return;
domNode.addEventListener("dragover", (e: DragEvent) => {
if (e.dataTransfer?.types?.includes("Files")) {
e.preventDefault();
e.stopPropagation();
}
});
domNode.addEventListener("drop", (e: DragEvent) => {
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
e.preventDefault();
e.stopPropagation();
handleFile(editor, files[0]);
}
});
document.addEventListener("paste", (e: ClipboardEvent) => {
if (!editor.hasWidgetFocus())
return;
if (!e.clipboardData?.files.length)
return;
e.preventDefault();
e.stopPropagation();
handleFile(editor, e.clipboardData.files[0]);
}, true);
}
const MAX_FILE_SIZE = 1024 * 1024; // 1 MB
function IsTextFile(file: File): boolean {
if (file.type.startsWith("text/")) return true;
const textExtensions = [
".reg", ".ps1", ".txt", ".cfg", ".ini", ".conf",
".json", ".xml", ".yaml", ".yml", ".csv", ".log",
".bat", ".cmd", ".sh", ".py", ".lua",
];
return textExtensions.includes(getFileExtension(file.name));
}
function handleFile(editor: any, file: File): void {
if (file.size > MAX_FILE_SIZE)
return;
if (!IsTextFile(file))
return;
const reader = new FileReader();
reader.onload = () => {
const content = reader.result as string;
const ext = getFileExtension(file.name);
if (ext === ".reg") {
const psCommands = ParseRegToPowerShell(content);
insertTextAtCursor(editor, psCommands);
} else if (ext === ".ps1") {
editor.setValue(content);
} else {
const hereString = `@'\n${content}\n'@`;
insertTextAtCursor(editor, hereString);
}
};
reader.readAsText(file);
}
function getFileExtension(filename: string): string {
const idx = filename.lastIndexOf(".");
return idx >= 0 ? filename.substring(idx).toLowerCase() : "";
}
function insertTextAtCursor(editor: any, text: string): void {
const selection = editor.getSelection();
editor.executeEdits("fileImport", [{
range: selection,
text: text,
forceMoveMarkers: true,
}]);
}
interface RegEntry {
type: "key" | "sz" | "dword" | "binary";
path: string;
property?: string;
value?: any;
}
function ParseRegToPowerShell(regContent: string): string {
const entries = ParseRegFile(regContent);
const lines: string[] = [];
for (const entry of entries) {
switch (entry.type) {
case "key":
if (lines.length > 0)
lines.push("");
lines.push(`New-Item -Path "registry::\\${entry.path}"`);
break;
case "sz": {
const escaped = (entry.value as string).replace(/"/g, '""""');
lines.push(`New-ItemProperty -Path "registry::\\${entry.path}" -Name "${entry.property}" -Value "${escaped}" -Force`);
break;
}
case "dword":
lines.push(`New-ItemProperty -Path "registry::\\${entry.path}" -Name "${entry.property}" -Value ${entry.value} -Force`);
break;
case "binary": {
const bytes = entry.value as number[];
const chunks: string[] = [];
for (let i = 0; i < bytes.length; i += 32) {
const chunk = bytes.slice(i, i + 32);
chunks.push(chunk.map(b => "0x" + b.toString(16).toUpperCase().padStart(2, "0")).join(", "));
}
const formatted = chunks.join("\n\t");
lines.push(`New-ItemProperty -Path "registry::\\${entry.path}" -Name "${entry.property}" -PropertyType Binary -Value ([byte[]]@(\n\t${formatted}\n)) -Force`);
break;
}
}
}
return lines.join("\n");
}
function ParseRegFile(content: string): RegEntry[] {
const entries: RegEntry[] = [];
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const joined = normalized.replace(/\\\n\s*/g, "");
const rawLines = joined.split("\n");
let currentPath = "";
for (const line of rawLines) {
const trimmed = line.trim();
// Skip empty lines, comments, and headers
if (!trimmed || trimmed.startsWith(";") ||
trimmed === "Windows Registry Editor Version 5.00" ||
trimmed === "REGEDIT4") {
continue;
}
// Key section: [HKEY_...]
const keyMatch = trimmed.match(/^\[(.+)\]$/);
if (keyMatch) {
const path = keyMatch[1];
if (path.startsWith("-"))
continue;
currentPath = path;
entries.push({ type: "key", path: currentPath });
continue;
}
if (!currentPath)
continue;
// Parse value lines
let name: string;
let valueStr: string;
if (trimmed.startsWith("@=")) {
name = "@";
valueStr = trimmed.substring(2);
} else {
const valueMatch = trimmed.match(/^"(.+?)"=(.*)$/);
if (!valueMatch)
continue;
name = valueMatch[1].replace(/\\\\/g, "\\").replace(/\\"/g, '"');
valueStr = valueMatch[2];
}
// Skip delete values
if (valueStr === "-")
continue;
// String value: "..."
if (valueStr.startsWith('"') && valueStr.endsWith('"')) {
const str = valueStr.slice(1, -1).replace(/\\\\/g, "\\").replace(/\\"/g, '"');
entries.push({ type: "sz", path: currentPath, property: name, value: str });
continue;
}
// DWORD: dword:XXXXXXXX
const dwordMatch = valueStr.match(/^dword:([0-9a-fA-F]{1,8})$/);
if (dwordMatch) {
const val = parseInt(dwordMatch[1], 16);
entries.push({ type: "dword", path: currentPath, property: name, value: val });
continue;
}
// Binary/hex types: hex:XX,XX,... or hex(N):XX,XX,...
const hexMatch = valueStr.match(/^hex(?:\([0-9a-fA-F]+\))?:(.*)$/);
if (hexMatch) {
const hexStr = hexMatch[1].replace(/\s/g, "");
const bytes = hexStr.split(",")
.filter(s => s.length > 0)
.map(s => parseInt(s, 16));
entries.push({ type: "binary", path: currentPath, property: name, value: bytes });
}
}
return entries;
}