From 4d5288ed3316a04efdca4ee5f2f1f202afffbd2b Mon Sep 17 00:00:00 2001 From: fly6516 Date: Fri, 3 Jan 2025 03:38:42 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=B7=BB=E5=8A=A0=E4=BA=86?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E4=BB=93=E5=BA=93=E6=B5=8B=E8=AF=95=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 62 +++++++- package.json | 1 + src/actions/enhancedmultiFileRunner.ts | 201 +++++++++++++++++++++++++ src/actions/multiFileRunner.ts | 2 + 4 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 src/actions/enhancedmultiFileRunner.ts diff --git a/package-lock.json b/package-lock.json index 8c4eb3a..b923699 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "docker-compiler-nextjs", "version": "0.1.0", "dependencies": { + "axios": "^1.7.9", "dockerode": "^4.0.2", "next": "15.1.3", "react": "^19.0.0", @@ -1731,6 +1732,12 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "dev": true, @@ -1753,6 +1760,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "dev": true, @@ -2252,6 +2270,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "dev": true, @@ -2592,6 +2622,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", @@ -3697,7 +3736,6 @@ "version": "1.15.9", "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "dev": true, "funding": [ { "type": "individual", @@ -3737,6 +3775,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", @@ -5266,7 +5318,6 @@ "version": "1.52.0", "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5276,7 +5327,6 @@ "version": "2.1.35", "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -6184,6 +6234,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.2.tgz", diff --git a/package.json b/package.json index 1505e17..2a3a1f8 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "lint": "next lint" }, "dependencies": { + "axios": "^1.7.9", "dockerode": "^4.0.2", "next": "15.1.3", "react": "^19.0.0", diff --git a/src/actions/enhancedmultiFileRunner.ts b/src/actions/enhancedmultiFileRunner.ts new file mode 100644 index 0000000..686bef4 --- /dev/null +++ b/src/actions/enhancedmultiFileRunner.ts @@ -0,0 +1,201 @@ +'use server'; + +import Docker from 'dockerode'; +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; + +const docker = new Docker(); + +let giteaRepoUrl: string | undefined; +let giteaToken: string | undefined; +let testDataPath: string = 'testdata'; // 默认测试数据目录 + +/** + * 设置 Gitea 仓库 URL + * @param url 仓库 URL + */ +export const setGiteaRepoUrl = async (url: string) => { + if (!url || !url.startsWith('http')) { + throw new Error('Invalid repository URL. Please provide a valid URL starting with "http".'); + } + giteaRepoUrl = url; +}; + +/** + * 设置 Gitea Token + * @param token 仓库 API 访问令牌 + */ +export const setGiteaToken = async (token: string) => { + if (!token || token.trim().length === 0) { + throw new Error('Invalid token. Please provide a non-empty token.'); + } + giteaToken = token; +}; + +/** + * 设置测试数据在仓库中的路径 + * @param path 测试数据目录路径 + */ +export const setTestDataPath = async (path: string) => { + if (!path || path.trim().length === 0) { + throw new Error('Invalid test data path. Please provide a non-empty path.'); + } + testDataPath = path; +}; + +/** + * 从 Gitea 仓库中下载指定目录的文件 + * @param dir 目录路径 + * @param tempDir 本地存储的临时目录 + */ +const fetchFilesFromGitea = async (dir: string, tempDir: string) => { + if (!giteaRepoUrl || !giteaToken) { + throw new Error('Gitea repository URL or token is not set. Please configure them using setGiteaRepoUrl and setGiteaToken.'); + } + + const targetDir = path.join(tempDir, dir); + fs.mkdirSync(targetDir, { recursive: true }); + + try { + const response = await axios.get(`${giteaRepoUrl}/contents/${dir}`, { + headers: { + Authorization: `token ${giteaToken}`, + }, + }); + + const files = response.data; + if (!Array.isArray(files)) { + throw new Error(`Invalid directory structure for ${dir} in repository.`); + } + + for (const file of files) { + if (file.type === 'file') { + const fileResponse = await axios.get(file.download_url, { + headers: { + Authorization: `token ${giteaToken}`, + }, + responseType: 'arraybuffer', + }); + + const filePath = path.join(targetDir, file.name); + fs.writeFileSync(filePath, fileResponse.data); + } + } + } catch (error) { + throw new Error(`Failed to fetch files from '${dir}': ${(error as Error).message}`); + } +}; + +/** + * 比较运行结果与正确答案文件 + * @param resultFilePath 运行结果文件路径 + * @param expectedFilePath 正确答案文件路径 + * @returns 是否一致的比较结果 + */ +const compareResults = (resultFilePath: string, expectedFilePath: string): boolean => { + const result = fs.readFileSync(resultFilePath, 'utf-8').trim(); + const expected = fs.readFileSync(expectedFilePath, 'utf-8').trim(); + return result === expected; +}; + +/** + * 执行多文件代码并测试结果 + * @param params 包含代码文件列表、语言、测试数据文件名和正确答案文件名的参数 + * @returns 运行结果或比较结果 + */ +export const runMultiFileCodeWithOptionalTestData = async (params: { + files: { name: string; content: string }[]; + language: string; + testDataFiles?: string[]; + expectedAnswerFiles?: string[]; + resultOutputFile?: string; +}) => { + const { files, language, testDataFiles, expectedAnswerFiles, resultOutputFile } = params; + + if (!files || files.length === 0 || !language) { + throw new Error('Files and language are required.'); + } + + const supportedLanguages = ['c', 'java', 'python']; + if (!supportedLanguages.includes(language)) { + throw new Error(`Language '${language}' is not supported. Only 'c', 'java', and 'python' are supported.`); + } + + const compileAndRunCmd = + language === 'c' + ? 'gcc *.c -o main && ./main' + : language === 'java' + ? 'javac *.java && java Main' + : 'python3 script.py'; + + const tempDir = path.join('/tmp', `${Date.now()}`); + fs.mkdirSync(tempDir, { recursive: true }); + + try { + // 写入代码文件 + for (const file of files) { + const filePath = path.join(tempDir, file.name); + fs.writeFileSync(filePath, file.content); + } + + // 检查是否设置了 Gitea 仓库信息 + if (giteaRepoUrl && giteaToken) { + try { + await fetchFilesFromGitea(testDataPath, tempDir); + } catch (error) { + throw new Error(`Failed to fetch test data from '${testDataPath}': ${(error as Error).message}`); + } + } else { + console.info('No Gitea repository connection set. Proceeding without testdata.'); + } + + // 创建 Docker 容器 + const container = await docker.createContainer({ + Image: 'fly6516.synology.me:8080/multilang:latest', + Cmd: ['sh', '-c', compileAndRunCmd], + AttachStdout: true, + AttachStderr: true, + Tty: false, + HostConfig: { + Binds: [`${tempDir}:/workspace`], // 挂载临时目录到容器 + }, + WorkingDir: '/workspace', + }); + + await container.start(); + + const logs = await container.logs({ stdout: true, stderr: true, follow: true }); + + let output = ''; + logs.on('data', (data: Buffer) => { + output += data.toString().replace(/[\x00-\x1F\x7F]/g, ''); // 去除不可见字符 + }); + + const exitCode = await container.wait(); + await container.remove(); + + if (exitCode.StatusCode !== 0) { + return { error: `Execution failed with exit code ${exitCode.StatusCode}. Output: ${output}` }; + } + + // 如果指定了结果输出文件和正确答案文件,执行比较 + if (resultOutputFile && expectedAnswerFiles) { + const resultFilePath = path.join(tempDir, resultOutputFile); + + const comparisonResults = expectedAnswerFiles.map((expectedFile) => { + const expectedFilePath = path.join(tempDir, testDataPath, expectedFile); + const isMatch = compareResults(resultFilePath, expectedFilePath); + return { expectedFile, isMatch }; + }); + + return { output, comparisonResults }; + } + + return { output }; + } catch (error) { + return { error: `An error occurred: ${(error as Error).message}` }; + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}; diff --git a/src/actions/multiFileRunner.ts b/src/actions/multiFileRunner.ts index 5e317cd..ad4496e 100644 --- a/src/actions/multiFileRunner.ts +++ b/src/actions/multiFileRunner.ts @@ -1,3 +1,5 @@ +'use server'; + import Docker from 'dockerode'; import fs from 'fs'; import path from 'path';