Add client apply flow and asset uploads

This commit is contained in:
2026-05-08 20:03:07 +09:00
parent 427b708277
commit 4453dbd8f3
13 changed files with 730 additions and 73 deletions

View File

@@ -1,5 +1,10 @@
import { Router } from 'express'
import multer from 'multer'
import path from 'node:path'
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import { fetchReleaseVersions } from '../../shared/mojang'
import { fileDir } from '../../shared/paths'
import {
createNewPack,
deletePacks,
@@ -8,11 +13,14 @@ import {
loadPackDefinition,
loadRootManifest,
normalizePackDefinition,
savePackDefinition,
updatePack
} from '../../shared/store'
import { PackDefinition } from '../../shared/types'
import { requireAuth } from '../middleware/auth'
export const opRouter = Router()
const upload = multer({ storage: multer.memoryStorage() })
function pickFirstValue(value: unknown): string {
if (Array.isArray(value)) {
@@ -21,6 +29,41 @@ function pickFirstValue(value: unknown): string {
return typeof value === 'string' ? value : ''
}
function sanitizeUploadFileName(name: string): string {
return name.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-')
}
function normalizeAssetPathForWeb(filePath: string): string {
return filePath.replace(/\\/g, '/')
}
async function saveUploadedPackAsset(packKey: string, bucket: 'loaders' | 'resourcepacks' | 'shaderpacks', file: Express.Multer.File): Promise<string> {
const safeName = sanitizeUploadFileName(file.originalname)
const relativePath = path.join('uploads', packKey, bucket, `${Date.now()}-${safeName}`)
const absolutePath = path.join(fileDir, relativePath)
await fsp.mkdir(path.dirname(absolutePath), { recursive: true })
await fsp.writeFile(absolutePath, file.buffer)
return normalizeAssetPathForWeb(relativePath)
}
async function mutatePackDefinition(packKey: string, mutate: (pack: PackDefinition) => void): Promise<void> {
const current = await loadPackDefinition(packKey)
if (current == null) {
throw new Error('서버팩 JSON을 찾을 수 없습니다.')
}
const next = normalizePackDefinition(current)
mutate(next)
await savePackDefinition(packKey, next)
}
async function removeUploadedAsset(relativePath: string): Promise<void> {
const absolutePath = path.join(fileDir, relativePath)
if (fs.existsSync(absolutePath)) {
await fsp.unlink(absolutePath)
}
}
opRouter.get('/op', (req, res) => {
if (req.session.userId != null) {
res.redirect('/op/dashboard')
@@ -125,10 +168,18 @@ opRouter.post('/op/dashboard/:packName', requireAuth, async (req, res, next) =>
const packKey = pickFirstValue(req.params.packName)
const nextPackName = pickFirstValue(req.body.displayName).trim() || packKey
const nextJsonKey = pickFirstValue(req.body.fileName).trim() || packKey
const currentDefinition = await loadPackDefinition(packKey)
if (currentDefinition == null) {
throw new Error('서버팩 JSON을 찾을 수 없습니다.')
}
const normalized = normalizePackDefinition({
...currentDefinition,
mcVersion: pickFirstValue(req.body.mcVersion),
recommendedJdkVersion: Number(pickFirstValue(req.body.recommendedJdkVersion)),
loaderType: pickFirstValue(req.body.loaderType) as PackDefinition['loaderType'],
loaderVersion: pickFirstValue(req.body.loaderVersion),
loaderInstallerPath: pickFirstValue(req.body.loaderInstallerPath),
serverMinRam: Number(pickFirstValue(req.body.serverMinRam)),
serverMaxRam: Number(pickFirstValue(req.body.serverMaxRam)),
clientMinRam: Number(pickFirstValue(req.body.clientMinRam)),
@@ -143,3 +194,101 @@ opRouter.post('/op/dashboard/:packName', requireAuth, async (req, res, next) =>
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/loader', requireAuth, upload.single('asset'), async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
if (req.file == null) {
throw new Error('업로드된 로더 파일이 없습니다.')
}
const relativePath = await saveUploadedPackAsset(packKey, 'loaders', req.file)
await mutatePackDefinition(packKey, (pack) => {
pack.loaderInstallerPath = relativePath
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/resource-pack', requireAuth, upload.single('asset'), async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
if (req.file == null) {
throw new Error('업로드된 리소스팩 파일이 없습니다.')
}
const relativePath = await saveUploadedPackAsset(packKey, 'resourcepacks', req.file)
await mutatePackDefinition(packKey, (pack) => {
pack.resourcePackFiles = [...(pack.resourcePackFiles ?? []), relativePath]
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/shader-pack', requireAuth, upload.single('asset'), async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
if (req.file == null) {
throw new Error('업로드된 쉐이더 파일이 없습니다.')
}
const relativePath = await saveUploadedPackAsset(packKey, 'shaderpacks', req.file)
await mutatePackDefinition(packKey, (pack) => {
pack.shaderPackFiles = [...(pack.shaderPackFiles ?? []), relativePath]
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/loader/remove', requireAuth, async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
const targetPath = pickFirstValue(req.body.assetPath)
await removeUploadedAsset(targetPath)
await mutatePackDefinition(packKey, (pack) => {
if (pack.loaderInstallerPath === targetPath) {
pack.loaderInstallerPath = ''
}
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/resource-pack/remove', requireAuth, async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
const targetPath = pickFirstValue(req.body.assetPath)
await removeUploadedAsset(targetPath)
await mutatePackDefinition(packKey, (pack) => {
pack.resourcePackFiles = (pack.resourcePackFiles ?? []).filter((entry) => entry !== targetPath)
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/shader-pack/remove', requireAuth, async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
const targetPath = pickFirstValue(req.body.assetPath)
await removeUploadedAsset(targetPath)
await mutatePackDefinition(packKey, (pack) => {
pack.shaderPackFiles = (pack.shaderPackFiles ?? []).filter((entry) => entry !== targetPath)
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})