feat: 모드팩 업로드 API 구현

- POST /api/admin/modpacks: .mrpack 파일 업로드
- adm-zip으로 modrinth.index.json 파싱
- 모드팩 이름, 버전, MC 버전, 모드 로더 자동 추출
- mods/resourcepacks/shaderpacks 콘텐츠 파싱
- S3에 파일 저장, DB에 메타데이터 저장
This commit is contained in:
caadiq 2025-12-23 16:32:06 +09:00
parent b9dd596652
commit e586520b90
2 changed files with 136 additions and 7 deletions

View file

@ -9,6 +9,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.400.0",
"adm-zip": "^0.5.16",
"bcryptjs": "^2.4.3",
"express": "^4.18.2",
"express-session": "^1.17.3",

View file

@ -202,13 +202,11 @@ router.get("/whitelist", async (req, res) => {
res.json(data);
} catch (error) {
console.error("[Admin] 화이트리스트 조회 오류:", error);
res
.status(500)
.json({
enabled: false,
players: [],
error: "서버에 연결할 수 없습니다",
});
res.status(500).json({
enabled: false,
players: [],
error: "서버에 연결할 수 없습니다",
});
}
});
@ -345,4 +343,134 @@ function formatFileSize(bytes) {
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;