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": {
|
||||
"@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",
|
||||
|
|
|
|||
|
|
@ -202,9 +202,7 @@ router.get("/whitelist", async (req, res) => {
|
|||
res.json(data);
|
||||
} catch (error) {
|
||||
console.error("[Admin] 화이트리스트 조회 오류:", error);
|
||||
res
|
||||
.status(500)
|
||||
.json({
|
||||
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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue