Compare commits
No commits in common. "2375707515db39b6905213046400db1f076b2484" and "612e36e4ea8b5061e94aacd9e0cab03e9a73e0aa" have entirely different histories.
2375707515
...
612e36e4ea
62
package-lock.json
generated
62
package-lock.json
generated
@ -8,7 +8,6 @@
|
|||||||
"name": "docker-compiler-nextjs",
|
"name": "docker-compiler-nextjs",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"next": "15.1.3",
|
"next": "15.1.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@ -1732,12 +1731,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -1760,17 +1753,6 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -2270,18 +2252,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -2622,15 +2592,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||||
@ -3736,6 +3697,7 @@
|
|||||||
"version": "1.15.9",
|
"version": "1.15.9",
|
||||||
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@ -3775,20 +3737,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -5318,6 +5266,7 @@
|
|||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@ -5327,6 +5276,7 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
@ -6234,12 +6184,6 @@
|
|||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.2.tgz",
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"next": "15.1.3",
|
"next": "15.1.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
@ -1,205 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import Docker from 'dockerode';
|
|
||||||
import fs from 'fs/promises'; // 使用异步版本的 fs 模块
|
|
||||||
import path from 'path';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const docker = new Docker();
|
|
||||||
const DOCKER_IMAGE_URL = process.env.DOCKER_IMAGE_URL || 'fly6516.synology.me:8080/multilang:latest'; // 使用环境变量
|
|
||||||
|
|
||||||
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);
|
|
||||||
await fs.mkdir(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.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 并行下载文件
|
|
||||||
await Promise.all(files.filter(file => file.type === 'file').map(async (file) => {
|
|
||||||
const fileResponse = await axios.get(file.download_url, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `token ${giteaToken}`,
|
|
||||||
},
|
|
||||||
responseType: 'arraybuffer',
|
|
||||||
});
|
|
||||||
|
|
||||||
const filePath = path.join(targetDir, file.name);
|
|
||||||
await fs.writeFile(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 = async (resultFilePath: string, expectedFilePath: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const result = (await fs.readFile(resultFilePath, 'utf-8')).trim();
|
|
||||||
const expected = (await fs.readFile(expectedFilePath, 'utf-8')).trim();
|
|
||||||
return result === expected;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to read files for comparison: ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行多文件代码并测试结果
|
|
||||||
* @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()}`);
|
|
||||||
await fs.mkdir(tempDir, { recursive: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 写入代码文件
|
|
||||||
await Promise.all(files.map(async (file) => {
|
|
||||||
const filePath = path.join(tempDir, file.name);
|
|
||||||
await fs.writeFile(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: DOCKER_IMAGE_URL,
|
|
||||||
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 && expectedAnswerFiles.length > 0) {
|
|
||||||
const resultFilePath = path.join(tempDir, resultOutputFile);
|
|
||||||
|
|
||||||
const comparisonResults = await Promise.all(expectedAnswerFiles.map(async (expectedFile) => {
|
|
||||||
const expectedFilePath = path.join(tempDir, testDataPath, expectedFile);
|
|
||||||
const isMatch = await compareResults(resultFilePath, expectedFilePath);
|
|
||||||
return { expectedFile, isMatch };
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { output, comparisonResults };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { output };
|
|
||||||
} catch (error) {
|
|
||||||
return { error: `An error occurred: ${(error as Error).message}` };
|
|
||||||
} finally {
|
|
||||||
await fs.rm(tempDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,5 +1,3 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import Docker from 'dockerode';
|
import Docker from 'dockerode';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
Loading…
Reference in New Issue
Block a user