feat: 모드팩 업로드 API 구현
- POST /api/admin/modpacks: .mrpack 파일 업로드 - adm-zip으로 modrinth.index.json 파싱 - 모드팩 이름, 버전, MC 버전, 모드 로더 자동 추출 - mods/resourcepacks/shaderpacks 콘텐츠 파싱 - S3에 파일 저장, DB에 메타데이터 저장
This commit is contained in:
parent
b9dd596652
commit
e586520b90
2 changed files with 136 additions and 7 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.400.0",
|
"@aws-sdk/client-s3": "^3.400.0",
|
||||||
|
"adm-zip": "^0.5.16",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
|
|
|
||||||
|
|
@ -202,9 +202,7 @@ router.get("/whitelist", async (req, res) => {
|
||||||
res.json(data);
|
res.json(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Admin] 화이트리스트 조회 오류:", error);
|
console.error("[Admin] 화이트리스트 조회 오류:", error);
|
||||||
res
|
res.status(500).json({
|
||||||
.status(500)
|
|
||||||
.json({
|
|
||||||
enabled: false,
|
enabled: false,
|
||||||
players: [],
|
players: [],
|
||||||
error: "서버에 연결할 수 없습니다",
|
error: "서버에 연결할 수 없습니다",
|
||||||
|
|
@ -345,4 +343,134 @@ function formatFileSize(bytes) {
|
||||||
return bytes + " B";
|
return bytes + " B";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/admin/modpacks - 모드팩 업로드
|
||||||
|
* multipart/form-data: file (.mrpack), changelog (옵션)
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
"/modpacks",
|
||||||
|
express.raw({ type: "multipart/form-data", limit: "500mb" }),
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const boundary = req.headers["content-type"]?.split("boundary=")[1];
|
||||||
|
if (!boundary) {
|
||||||
|
return res.status(400).json({ error: "Invalid content-type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = req.body.toString("binary");
|
||||||
|
const parts = body.split(`--${boundary}`);
|
||||||
|
|
||||||
|
let fileData = null;
|
||||||
|
let fileName = "";
|
||||||
|
let changelog = "";
|
||||||
|
|
||||||
|
// multipart 파싱
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.includes('name="file"')) {
|
||||||
|
const match = part.match(/filename="([^"]+)"/);
|
||||||
|
if (match) fileName = match[1];
|
||||||
|
const dataStart = part.indexOf("\r\n\r\n") + 4;
|
||||||
|
const dataEnd = part.lastIndexOf("\r\n");
|
||||||
|
fileData = Buffer.from(part.slice(dataStart, dataEnd), "binary");
|
||||||
|
} else if (part.includes('name="changelog"')) {
|
||||||
|
const dataStart = part.indexOf("\r\n\r\n") + 4;
|
||||||
|
const dataEnd = part.lastIndexOf("\r\n");
|
||||||
|
changelog = part.slice(dataStart, dataEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileData || !fileName.endsWith(".mrpack")) {
|
||||||
|
return res.status(400).json({ error: ".mrpack 파일이 필요합니다" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// .mrpack은 ZIP 파일. modrinth.index.json 파싱
|
||||||
|
const { unzipSync } = await import("zlib");
|
||||||
|
const AdmZip = (await import("adm-zip")).default;
|
||||||
|
const zip = new AdmZip(fileData);
|
||||||
|
const indexEntry = zip.getEntry("modrinth.index.json");
|
||||||
|
|
||||||
|
if (!indexEntry) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "modrinth.index.json을 찾을 수 없습니다" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexJson = JSON.parse(indexEntry.getData().toString("utf8"));
|
||||||
|
const modpackName = indexJson.name || "Unknown Modpack";
|
||||||
|
const modpackVersion = indexJson.versionId || "1.0.0";
|
||||||
|
const minecraftVersion = indexJson.dependencies?.minecraft || null;
|
||||||
|
const modLoader =
|
||||||
|
Object.keys(indexJson.dependencies || {}).find(
|
||||||
|
(k) => k !== "minecraft"
|
||||||
|
) || null;
|
||||||
|
|
||||||
|
// contents 파싱 (mods, resourcepacks, shaderpacks)
|
||||||
|
const contents = { mods: [], resourcepacks: [], shaderpacks: [] };
|
||||||
|
for (const file of indexJson.files || []) {
|
||||||
|
const path = file.path;
|
||||||
|
// 파일명에서 이름과 버전 추출 시도 (예: Create-0.5.1.jar)
|
||||||
|
const fileName = path
|
||||||
|
.split("/")
|
||||||
|
.pop()
|
||||||
|
.replace(/\.(jar|zip)$/, "");
|
||||||
|
const match = fileName.match(/^(.+?)[-_](\d+[\d.]+\d*)$/);
|
||||||
|
const name = match ? match[1].replace(/[-_]/g, " ") : fileName;
|
||||||
|
const version = match ? match[2] : "";
|
||||||
|
|
||||||
|
if (path.startsWith("mods/")) {
|
||||||
|
contents.mods.push({ name, version });
|
||||||
|
} else if (path.startsWith("resourcepacks/")) {
|
||||||
|
contents.resourcepacks.push({ name, version });
|
||||||
|
} else if (path.startsWith("shaderpacks/")) {
|
||||||
|
contents.shaderpacks.push({ name, version });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3에 업로드
|
||||||
|
const s3Key = `modpacks/${modpackName.replace(
|
||||||
|
/[^a-zA-Z0-9가-힣]/g,
|
||||||
|
"_"
|
||||||
|
)}/${modpackVersion}.mrpack`;
|
||||||
|
const { uploadToS3 } = await import("../lib/s3.js");
|
||||||
|
await uploadToS3("minecraft", s3Key, fileData, "application/zip");
|
||||||
|
|
||||||
|
// DB에 저장
|
||||||
|
const [result] = await pool.query(
|
||||||
|
`INSERT INTO modpacks (name, version, minecraft_version, mod_loader, changelog, file_key, file_size, contents_json)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
changelog = VALUES(changelog),
|
||||||
|
file_key = VALUES(file_key),
|
||||||
|
file_size = VALUES(file_size),
|
||||||
|
contents_json = VALUES(contents_json),
|
||||||
|
updated_at = CURRENT_TIMESTAMP`,
|
||||||
|
[
|
||||||
|
modpackName,
|
||||||
|
modpackVersion,
|
||||||
|
minecraftVersion,
|
||||||
|
modLoader,
|
||||||
|
changelog,
|
||||||
|
s3Key,
|
||||||
|
fileData.length,
|
||||||
|
JSON.stringify(contents),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[Admin] 모드팩 업로드 완료: ${modpackName} v${modpackVersion}`
|
||||||
|
);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
id: result.insertId,
|
||||||
|
name: modpackName,
|
||||||
|
version: modpackVersion,
|
||||||
|
size: formatFileSize(fileData.length),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Admin] 모드팩 업로드 오류:", error);
|
||||||
|
res.status(500).json({ error: "업로드 실패: " + error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue