mirror of
https://github.com/fccview/cronmaster.git
synced 2025-12-30 17:38:08 -05:00
325 lines
8.8 KiB
TypeScript
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",
|
|
};
|
|
}
|
|
};
|