228 lines
7.0 KiB
TypeScript
228 lines
7.0 KiB
TypeScript
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<void> {
|
|
await fsp.mkdir(targetPath, { recursive: true })
|
|
}
|
|
|
|
async function ensureJsonFile<T>(targetPath: string, defaultValue: T): Promise<void> {
|
|
if (!fs.existsSync(targetPath)) {
|
|
await fsp.writeFile(targetPath, `${JSON.stringify(defaultValue, null, 2)}\n`, 'utf8')
|
|
}
|
|
}
|
|
|
|
export async function ensureProjectFiles(): Promise<void> {
|
|
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<T>(targetPath: string, fallback: T): Promise<T> {
|
|
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<void> {
|
|
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<RootManifest> {
|
|
await ensureProjectFiles()
|
|
return readJsonFile<RootManifest>(manifestRootPath, defaultRootManifest)
|
|
}
|
|
|
|
export async function saveRootManifest(manifest: RootManifest): Promise<void> {
|
|
await writeJsonFile(manifestRootPath, manifest)
|
|
}
|
|
|
|
export async function loadAccounts(): Promise<AccountEntry[]> {
|
|
await ensureProjectFiles()
|
|
return readJsonFile<AccountEntry[]>(accountPath, defaultAccount)
|
|
}
|
|
|
|
export async function listManifestFiles(): Promise<string[]> {
|
|
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<DashboardPackEntry[]> {
|
|
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<PackDefinition | null> {
|
|
await ensureProjectFiles()
|
|
const safeKey = sanitizePackKey(packKey)
|
|
const filePath = path.join(manifestDir, `${safeKey}.json`)
|
|
if (!fs.existsSync(filePath)) {
|
|
return null
|
|
}
|
|
|
|
return readJsonFile<PackDefinition>(filePath, defaultPackDefinition)
|
|
}
|
|
|
|
export async function savePackDefinition(packKey: string, payload: PackDefinition): Promise<void> {
|
|
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<string> {
|
|
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<void> {
|
|
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<string> {
|
|
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>): 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']
|
|
}
|
|
}
|