Files
cronmaster/app/_server/actions/scripts/index.ts

325 lines
8.8 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { writeFile, readFile, unlink, mkdir } from "fs/promises";
import path from "path";
import { existsSync } from "fs";
import { exec } from "child_process";
import { promisify } from "util";
import { SCRIPTS_DIR } from "@/app/_consts/file";
import { loadAllScripts, Script } from "@/app/_utils/scripts-utils";
import { MAKE_SCRIPT_EXECUTABLE, RUN_SCRIPT } from "@/app/_consts/commands";
import { isDocker, getHostScriptsPath } from "@/app/_server/actions/global";
const execAsync = promisify(exec);
export const getScriptPathForCron = async (
filename: string
): Promise<string> => {
const docker = await isDocker();
if (docker) {
const hostScriptsPath = await getHostScriptsPath();
if (hostScriptsPath) {
return `bash ${path.join(hostScriptsPath, filename)}`;
}
console.warn("Could not determine host scripts path, using container path");
}
return `bash ${path.join(process.cwd(), SCRIPTS_DIR, filename)}`;
};
export const getHostScriptPath = async (filename: string): Promise<string> => {
return `bash ${path.join(process.cwd(), SCRIPTS_DIR, filename)}`;
};
export const normalizeLineEndings = async (content: string): Promise<string> => {
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
};
const sanitizeScriptName = (name: string): string => {
return name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.substring(0, 50);
};
const generateUniqueFilename = async (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;
};
const ensureScriptsDirectory = async () => {
const scriptsDir = path.join(process.cwd(), SCRIPTS_DIR);
if (!existsSync(scriptsDir)) {
await mkdir(scriptsDir, { recursive: true });
}
};
const ensureHostScriptsDirectory = async () => {
const hostScriptsDir = path.join(process.cwd(), SCRIPTS_DIR);
if (!existsSync(hostScriptsDir)) {
await mkdir(hostScriptsDir, { recursive: true });
}
};
const saveScriptFile = async (filename: string, content: string) => {
await ensureScriptsDirectory();
const scriptPath = path.join(process.cwd(), SCRIPTS_DIR, filename);
await writeFile(scriptPath, content, "utf8");
try {
await execAsync(MAKE_SCRIPT_EXECUTABLE(scriptPath));
} catch (error) {
console.error(`Failed to set execute permissions on ${scriptPath}:`, error);
}
};
const deleteScriptFile = async (filename: string) => {
const scriptPath = path.join(process.cwd(), SCRIPTS_DIR, filename);
if (existsSync(scriptPath)) {
await unlink(scriptPath);
}
};
export const fetchScripts = async (): Promise<Script[]> => {
return await loadAllScripts();
};
export const createScript = async (
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 normalizedContent = await normalizeLineEndings(content);
const fullContent = metadataHeader + normalizedContent;
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 const updateScript = async (
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 normalizedContent = await normalizeLineEndings(content);
const fullContent = metadataHeader + normalizedContent;
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 const deleteScript = async (
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 const cloneScript = async (
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 normalizedContent = await normalizeLineEndings(originalContent);
const fullContent = metadataHeader + normalizedContent;
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 const getScriptContent = async (filename: string): Promise<string> => {
try {
const scriptPath = path.join(process.cwd(), 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 const executeScript = async (
filename: string
): Promise<{
success: boolean;
output: string;
error: string;
}> => {
try {
await ensureHostScriptsDirectory();
const hostScriptPath = await getHostScriptPath(filename);
if (!existsSync(hostScriptPath)) {
return {
success: false,
output: "",
error: "Script file not found",
};
}
const { stdout, stderr } = await execAsync(RUN_SCRIPT(hostScriptPath), {
timeout: 30000,
});
return {
success: true,
output: stdout,
error: stderr,
};
} catch (error: any) {
return {
success: false,
output: "",
error: error.message || "Unknown error",
};
}
};