Files
cronmaster/app/_server/actions/scripts/index.ts
2025-08-21 09:10:58 +01:00

293 lines
7.6 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { writeFile, readFile, unlink, mkdir } from "fs/promises";
import { join } from "path";
import { existsSync } from "fs";
import { exec } from "child_process";
import { promisify } from "util";
import { SCRIPTS_DIR } from "@/app/_utils/scripts";
import { loadAllScripts, type Script } from "@/app/_utils/scriptScanner";
const execAsync = promisify(exec);
export type { Script } from "@/app/_utils/scriptScanner";
function sanitizeScriptName(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.substring(0, 50);
}
async function generateUniqueFilename(baseName: string): Promise<string> {
const scripts = await loadAllScripts();
let filename = `${sanitizeScriptName(baseName)}.sh`;
let counter = 1;
while (scripts.some((script) => script.filename === filename)) {
filename = `${sanitizeScriptName(baseName)}-${counter}.sh`;
counter++;
}
return filename;
}
async function ensureScriptsDirectory() {
if (!existsSync(SCRIPTS_DIR)) {
await mkdir(SCRIPTS_DIR, { recursive: true });
}
}
async function ensureHostScriptsDirectory() {
const isDocker = process.env.DOCKER === "true";
const hostScriptsDir = isDocker
? "/app/scripts"
: join(process.cwd(), "scripts");
if (!existsSync(hostScriptsDir)) {
await mkdir(hostScriptsDir, { recursive: true });
}
}
async function saveScriptFile(filename: string, content: string) {
await ensureScriptsDirectory();
const scriptPath = join(SCRIPTS_DIR, filename);
await writeFile(scriptPath, content, "utf8");
}
async function deleteScriptFile(filename: string) {
const scriptPath = join(SCRIPTS_DIR, filename);
if (existsSync(scriptPath)) {
await unlink(scriptPath);
}
}
export async function fetchScripts(): Promise<Script[]> {
return await loadAllScripts();
}
export async function createScript(
formData: FormData
): Promise<{ success: boolean; message: string; script?: Script }> {
try {
const name = formData.get("name") as string;
const description = formData.get("description") as string;
const content = formData.get("content") as string;
if (!name || !content) {
return { success: false, message: "Name and content are required" };
}
const scriptId = `script_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
const filename = await generateUniqueFilename(name);
const metadataHeader = `# @id: ${scriptId}
# @title: ${name}
# @description: ${description || ""}
`;
const fullContent = metadataHeader + content;
await saveScriptFile(filename, fullContent);
revalidatePath("/");
const newScript: Script = {
id: scriptId,
name,
description: description || "",
filename,
createdAt: new Date().toISOString(),
};
return {
success: true,
message: "Script created successfully",
script: newScript,
};
} catch (error) {
console.error("Error creating script:", error);
return { success: false, message: "Error creating script" };
}
}
export async function updateScript(
formData: FormData
): Promise<{ success: boolean; message: string }> {
try {
const id = formData.get("id") as string;
const name = formData.get("name") as string;
const description = formData.get("description") as string;
const content = formData.get("content") as string;
if (!id || !name || !content) {
return { success: false, message: "ID, name and content are required" };
}
const scripts = await loadAllScripts();
const existingScript = scripts.find((s) => s.id === id);
if (!existingScript) {
return { success: false, message: "Script not found" };
}
const metadataHeader = `# @id: ${id}
# @title: ${name}
# @description: ${description || ""}
`;
const fullContent = metadataHeader + content;
await saveScriptFile(existingScript.filename, fullContent);
revalidatePath("/");
return { success: true, message: "Script updated successfully" };
} catch (error) {
console.error("Error updating script:", error);
return { success: false, message: "Error updating script" };
}
}
export async function deleteScript(
id: string
): Promise<{ success: boolean; message: string }> {
try {
const scripts = await loadAllScripts();
const script = scripts.find((s) => s.id === id);
if (!script) {
return { success: false, message: "Script not found" };
}
await deleteScriptFile(script.filename);
revalidatePath("/");
return { success: true, message: "Script deleted successfully" };
} catch (error) {
console.error("Error deleting script:", error);
return { success: false, message: "Error deleting script" };
}
}
export async function cloneScript(
id: string,
newName: string
): Promise<{ success: boolean; message: string; script?: Script }> {
try {
const scripts = await loadAllScripts();
const originalScript = scripts.find((s) => s.id === id);
if (!originalScript) {
return { success: false, message: "Script not found" };
}
const scriptId = `script_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
const filename = await generateUniqueFilename(newName);
const originalContent = await getScriptContent(originalScript.filename);
const metadataHeader = `# @id: ${scriptId}
# @title: ${newName}
# @description: ${originalScript.description}
`;
const fullContent = metadataHeader + originalContent;
await saveScriptFile(filename, fullContent);
revalidatePath("/");
const newScript: Script = {
id: scriptId,
name: newName,
description: originalScript.description,
filename,
createdAt: new Date().toISOString(),
};
return {
success: true,
message: "Script cloned successfully",
script: newScript,
};
} catch (error) {
console.error("Error cloning script:", error);
return { success: false, message: "Error cloning script" };
}
}
export async function getScriptContent(filename: string): Promise<string> {
try {
const scriptPath = join(SCRIPTS_DIR, filename);
if (existsSync(scriptPath)) {
const content = await readFile(scriptPath, "utf8");
const lines = content.split("\n");
const contentLines: string[] = [];
let inMetadata = true;
for (const line of lines) {
if (line.trim().startsWith("# @")) {
continue;
}
if (line.trim() === "" && inMetadata) {
continue;
}
inMetadata = false;
contentLines.push(line);
}
return contentLines.join("\n").trim();
}
return "";
} catch (error) {
console.error("Error reading script content:", error);
return "";
}
}
export async function executeScript(filename: string): Promise<{
success: boolean;
output: string;
error: string;
}> {
try {
await ensureHostScriptsDirectory();
const isDocker = process.env.DOCKER === "true";
const hostScriptPath = isDocker
? join("/app/scripts", filename)
: join(process.cwd(), "scripts", filename);
if (!existsSync(hostScriptPath)) {
return {
success: false,
output: "",
error: "Script file not found",
};
}
const { stdout, stderr } = await execAsync(`bash "${hostScriptPath}"`, {
timeout: 30000,
});
return {
success: true,
output: stdout,
error: stderr,
};
} catch (error: any) {
return {
success: false,
output: "",
error: error.message || "Unknown error",
};
}
}