import fs from 'node:fs' import fsp from 'node:fs/promises' import path from 'node:path' import { accountPath, fileDir, manifestDir, manifestRootPath } from './paths' import { AccountEntry, DashboardPackEntry, PackDefinition, PackListEntry, RootManifest } from './types' const defaultRootManifest: RootManifest = { packs: [ { name: 'Sample Pack', file: 'sample-pack' } ] } const defaultAccount: AccountEntry[] = [ { id: 'admin', password: 'change-me' } ] const defaultPackDefinition: PackDefinition = { mcVersion: '1.20.1', serverMinRam: 2048, serverMaxRam: 4096, clientMinRam: 4096, clientRecommendedRam: 8192, packPath: 'sample-pack.zip', description: '새 서버팩', configEditableFiles: ['server.properties', 'bukkit.yml'] } async function ensureDir(targetPath: string): Promise { await fsp.mkdir(targetPath, { recursive: true }) } async function ensureJsonFile(targetPath: string, defaultValue: T): Promise { if (!fs.existsSync(targetPath)) { await fsp.writeFile(targetPath, `${JSON.stringify(defaultValue, null, 2)}\n`, 'utf8') } } export async function ensureProjectFiles(): Promise { await ensureDir(manifestDir) await ensureDir(fileDir) await ensureJsonFile(manifestRootPath, defaultRootManifest) await ensureJsonFile(accountPath, defaultAccount) const samplePackPath = path.join(manifestDir, 'sample-pack.json') await ensureJsonFile(samplePackPath, defaultPackDefinition) } async function readJsonFile(targetPath: string, fallback: T): Promise { try { const raw = await fsp.readFile(targetPath, 'utf8') return JSON.parse(raw) as T } catch { return fallback } } async function writeJsonFile(targetPath: string, payload: unknown): Promise { await fsp.writeFile(targetPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8') } function sanitizePackKey(name: string): string { const trimmed = name.trim().replace(/\.json$/i, '') const normalized = trimmed.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-') return normalized.length > 0 ? normalized : 'new' } export async function loadRootManifest(): Promise { await ensureProjectFiles() return readJsonFile(manifestRootPath, defaultRootManifest) } export async function saveRootManifest(manifest: RootManifest): Promise { await writeJsonFile(manifestRootPath, manifest) } export async function loadAccounts(): Promise { await ensureProjectFiles() return readJsonFile(accountPath, defaultAccount) } export async function listManifestFiles(): Promise { await ensureProjectFiles() const entries = await fsp.readdir(manifestDir, { withFileTypes: true }) return entries .filter((entry) => entry.isFile() && entry.name.endsWith('.json')) .map((entry) => entry.name.replace(/\.json$/i, '')) .sort((left, right) => left.localeCompare(right)) } export async function listDashboardPacks(): Promise { const [manifestFiles, rootManifest] = await Promise.all([ listManifestFiles(), loadRootManifest() ]) return manifestFiles.map((file) => { const registeredPack = rootManifest.packs.find((entry) => entry.file === file) return { file, name: registeredPack?.name ?? file, registered: registeredPack != null } }) } export async function loadPackDefinition(packKey: string): Promise { await ensureProjectFiles() const safeKey = sanitizePackKey(packKey) const filePath = path.join(manifestDir, `${safeKey}.json`) if (!fs.existsSync(filePath)) { return null } return readJsonFile(filePath, defaultPackDefinition) } export async function savePackDefinition(packKey: string, payload: PackDefinition): Promise { const safeKey = sanitizePackKey(packKey) await writeJsonFile(path.join(manifestDir, `${safeKey}.json`), payload) } function nextAvailableNewKey(existing: string[]): string { if (!existing.includes('new')) { return 'new' } let index = 2 while (existing.includes(`new${index}`)) { index += 1 } return `new${index}` } export async function createNewPack(): Promise { const existing = await listManifestFiles() const packKey = nextAvailableNewKey(existing) await savePackDefinition(packKey, { ...defaultPackDefinition, description: `새 서버팩 (${packKey})` }) const manifest = await loadRootManifest() manifest.packs.push({ name: `새 서버팩 (${packKey})`, file: packKey }) await saveRootManifest(manifest) return packKey } export async function deletePacks(packKeys: string[]): Promise { const targetKeys = new Set(packKeys.map((entry) => sanitizePackKey(entry))) const manifest = await loadRootManifest() manifest.packs = manifest.packs.filter((entry) => !targetKeys.has(entry.file)) await saveRootManifest(manifest) await Promise.all( [...targetKeys].map(async (packKey) => { const filePath = path.join(manifestDir, `${packKey}.json`) if (fs.existsSync(filePath)) { await fsp.unlink(filePath) } }) ) } export async function updatePack( currentKey: string, nextName: string, nextKey: string, definition: PackDefinition ): Promise { const safeCurrentKey = sanitizePackKey(currentKey) const safeNextKey = sanitizePackKey(nextKey) const currentPath = path.join(manifestDir, `${safeCurrentKey}.json`) const nextPath = path.join(manifestDir, `${safeNextKey}.json`) if (safeCurrentKey !== safeNextKey && fs.existsSync(nextPath)) { throw new Error('같은 이름의 JSON 파일이 이미 존재합니다.') } await savePackDefinition(safeNextKey, definition) if (safeCurrentKey !== safeNextKey && fs.existsSync(currentPath)) { await fsp.unlink(currentPath) } const manifest = await loadRootManifest() const targetIndex = manifest.packs.findIndex((entry) => entry.file === safeCurrentKey) const nextEntry: PackListEntry = { name: nextName.trim(), file: safeNextKey } if (targetIndex >= 0) { manifest.packs[targetIndex] = nextEntry } else { manifest.packs.push(nextEntry) } await saveRootManifest(manifest) return safeNextKey } export function normalizePackDefinition(input: Partial): PackDefinition { return { mcVersion: String(input.mcVersion ?? '1.20.1').trim() || '1.20.1', serverMinRam: Number(input.serverMinRam ?? 2048), serverMaxRam: Number(input.serverMaxRam ?? 4096), clientMinRam: Number(input.clientMinRam ?? 4096), clientRecommendedRam: Number(input.clientRecommendedRam ?? 8192), packPath: String(input.packPath ?? '').trim(), description: String(input.description ?? '').trim(), files: Array.isArray(input.files) ? input.files.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0) : undefined, configEditableFiles: Array.isArray(input.configEditableFiles) ? input.configEditableFiles.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0) : ['server.properties', 'bukkit.yml'] } }