new era
This commit is contained in:
1 parent
e30f89697b
commit
3e24d4a7bb
26 files changed
+4224
-4
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { NextRequest } from "next/server";
|
||||
import { ok, fail, handle, readJson } from "@/server/api";
|
||||
import { assertDeploymentEditable } from "@/server/managed";
|
||||
import { readNetwork, profileById } from "@/server/network-config";
|
||||
import { networkFilePath } from "@/server/network-files";
|
||||
import { assertNetworkIdle } from "@/server/network";
|
||||
import { inspectContainer } from "@/server/docker";
|
||||
import { withServerOperation } from "@/server/operation";
|
||||
import { serverName } from "@/server/network-render";
|
||||
export const dynamic = "force-dynamic";
|
||||
export async function GET(req: NextRequest) {
|
||||
return handle(async () => {
|
||||
const p = profileById(readNetwork(), req.nextUrl.searchParams.get("id"));
|
||||
const relative = req.nextUrl.searchParams.get("path") || "";
|
||||
const file = networkFilePath(p.id, relative);
|
||||
if (fs.statSync(file).isDirectory())
|
||||
return ok({
|
||||
entries: fs
|
||||
.readdirSync(file, { withFileTypes: true })
|
||||
.map((e) => ({
|
||||
name: e.name,
|
||||
directory: e.isDirectory(),
|
||||
link: e.isSymbolicLink(),
|
||||
})),
|
||||
});
|
||||
if (fs.statSync(file).size > 2 * 1024 * 1024)
|
||||
return fail("File exceeds the 2 MB editor limit");
|
||||
const data = fs.readFileSync(file);
|
||||
if (data.includes(0)) return fail("Binary files cannot be edited as text");
|
||||
return ok({ content: data.toString("utf8") });
|
||||
});
|
||||
}
|
||||
export async function POST(req: NextRequest) {
|
||||
return handle(() =>
|
||||
withServerOperation(async () => {
|
||||
assertDeploymentEditable();
|
||||
assertNetworkIdle();
|
||||
const isUpload = req.headers
|
||||
.get("content-type")
|
||||
?.includes("multipart/form-data");
|
||||
if (Number(req.headers.get("content-length") || 0) > 100 * 1024 * 1024)
|
||||
return fail("Upload limit is 100 MB");
|
||||
const form = isUpload ? await req.formData() : null;
|
||||
const body = form
|
||||
? { id: form.get("id"), path: form.get("path"), content: undefined }
|
||||
: await readJson<{ id: string; path: string; content: string }>(req);
|
||||
const p = profileById(readNetwork(), body.id);
|
||||
if ((await inspectContainer(serverName(p.id))).running)
|
||||
return fail("Stop this server before editing or uploading files", 409);
|
||||
const file = networkFilePath(p.id, body.path as string);
|
||||
// Terrain, player state, executable scripts and registry files are not text-editable.
|
||||
const allowed = /\.(json|ya?ml|toml|conf|properties|txt|cfg)$/i;
|
||||
let data: Buffer;
|
||||
if (form) {
|
||||
const upload = form.get("file");
|
||||
if (!(upload instanceof File) || upload.size > 100 * 1024 * 1024)
|
||||
return fail("Choose a file under 100 MB");
|
||||
const rel = path.relative(networkFilePath(p.id, ""), file);
|
||||
if (
|
||||
!/^(mods|plugins)\/[^/]+\.jar$/.test(rel) &&
|
||||
!/^world\/datapacks\/[^/]+\.zip$/.test(rel)
|
||||
)
|
||||
return fail(
|
||||
"Upload JARs into mods/plugins or ZIPs into world/datapacks",
|
||||
);
|
||||
if (fs.existsSync(file))
|
||||
return fail("A file already exists with this name");
|
||||
data = Buffer.from(await upload.arrayBuffer());
|
||||
} else {
|
||||
if (
|
||||
!allowed.test(file) ||
|
||||
typeof body.content !== "string" ||
|
||||
Buffer.byteLength(body.content) > 2 * 1024 * 1024
|
||||
)
|
||||
return fail("Choose a supported configuration file under 2 MB");
|
||||
data = Buffer.from(body.content);
|
||||
}
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, data, { flag: form ? "wx" : "w" });
|
||||
try {
|
||||
fs.chownSync(file, 1000, 1000);
|
||||
} catch {}
|
||||
return ok({ saved: true });
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { ok, fail, handle, readJson } from "@/server/api";
|
||||
import { env } from "@/server/env";
|
||||
import { readEnvFile } from "@/server/envfile";
|
||||
import { assertDeploymentEditable } from "@/server/managed";
|
||||
import { inspectContainer, powerContainer } from "@/server/docker";
|
||||
import {
|
||||
readNetwork,
|
||||
writeNetwork,
|
||||
validateProfile,
|
||||
validateProxy,
|
||||
profileById,
|
||||
NetworkInputError,
|
||||
} from "@/server/network-config";
|
||||
import { serverName, proxyName } from "@/server/network-render";
|
||||
import {
|
||||
networkBusy,
|
||||
assertNetworkIdle,
|
||||
reconcileJobs,
|
||||
checkPorts,
|
||||
queueNetworkJob,
|
||||
deployProfile,
|
||||
deployProxy,
|
||||
stopProfile,
|
||||
restartProfile,
|
||||
snapshotProfile,
|
||||
restoreProfile,
|
||||
profileConsole,
|
||||
profileLogs,
|
||||
profileBackups,
|
||||
} from "@/server/network";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
function networkHandle(fn: () => Promise<ReturnType<typeof ok>>) {
|
||||
return handle(async () => {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
if (e instanceof NetworkInputError) return fail(e.message, 400);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
export async function GET(req: NextRequest) {
|
||||
return networkHandle(async () => {
|
||||
const config = reconcileJobs(readNetwork());
|
||||
const id = req.nextUrl.searchParams.get("id");
|
||||
const kind = req.nextUrl.searchParams.get("kind");
|
||||
if (id) {
|
||||
profileById(config, id);
|
||||
if (kind === "logs") return ok(await profileLogs(id));
|
||||
if (kind === "backups") return ok(profileBackups(id));
|
||||
}
|
||||
const entries = await Promise.all(
|
||||
[
|
||||
...config.profiles.map((p) => [p.id, serverName(p.id)]),
|
||||
["proxy", proxyName],
|
||||
].map(async ([id, name]) => {
|
||||
try {
|
||||
return [id, await inspectContainer(name)] as const;
|
||||
} catch (e) {
|
||||
return [
|
||||
id,
|
||||
{
|
||||
exists: false,
|
||||
running: false,
|
||||
status: "unavailable",
|
||||
error: (e as Error).message,
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return ok({
|
||||
...config,
|
||||
managed: env.managed,
|
||||
runtime: env.containerRuntime,
|
||||
states: Object.fromEntries(entries),
|
||||
primaryPort: Number(readEnvFile().MC_PORT || 25565),
|
||||
busy: networkBusy(),
|
||||
});
|
||||
});
|
||||
}
|
||||
export async function POST(req: NextRequest) {
|
||||
return networkHandle(async () => {
|
||||
assertDeploymentEditable();
|
||||
assertNetworkIdle();
|
||||
const body = await readJson<Record<string, unknown>>(req);
|
||||
if (!body || typeof body !== "object") return fail("Invalid request");
|
||||
const config = readNetwork();
|
||||
if (body.op === "save") {
|
||||
const existing = body.id ? profileById(config, body.id) : undefined;
|
||||
if (!existing && config.profiles.length >= 32)
|
||||
return fail("Maximum 32 profiles per panel");
|
||||
const p = validateProfile(body.profile, existing);
|
||||
config.profiles = [...config.profiles.filter((s) => s.id !== p.id), p];
|
||||
if (!config.proxy.lobby && p.route === "proxy") config.proxy.lobby = p.id;
|
||||
checkPorts(config);
|
||||
writeNetwork(config);
|
||||
return ok(p);
|
||||
}
|
||||
if (body.op === "proxy-save") {
|
||||
config.proxy = validateProxy(body.proxy, config);
|
||||
checkPorts(config);
|
||||
writeNetwork(config);
|
||||
return ok(config.proxy);
|
||||
}
|
||||
if (body.op === "proxy-apply") {
|
||||
if (body.confirm !== "apply proxy")
|
||||
return fail(
|
||||
"Confirm applying the proxy; existing connections will close",
|
||||
);
|
||||
return ok(queueNetworkJob("proxy", "apply proxy", deployProxy));
|
||||
}
|
||||
if (body.op === "proxy-stop") {
|
||||
if (body.confirm !== "stop proxy")
|
||||
return fail("Confirm stopping the gateway");
|
||||
return ok(
|
||||
queueNetworkJob("proxy", "stop proxy", async () => {
|
||||
if ((await inspectContainer(proxyName)).running)
|
||||
await powerContainer(proxyName, "stop");
|
||||
return "Gateway stopped. World servers and their data remain available.";
|
||||
}),
|
||||
);
|
||||
}
|
||||
const p = profileById(config, body.id);
|
||||
if (body.op === "command")
|
||||
return ok(await profileConsole(p.id, body.command as string));
|
||||
if (body.confirm !== p.name)
|
||||
return fail(`Confirm by entering the server name: ${p.name}`);
|
||||
switch (body.op) {
|
||||
case "deploy":
|
||||
return ok(
|
||||
queueNetworkJob(p.id, "deploy", (progress) =>
|
||||
deployProfile(p.id, progress),
|
||||
),
|
||||
);
|
||||
case "stop":
|
||||
return ok(queueNetworkJob(p.id, "stop", () => stopProfile(p.id)));
|
||||
case "restart":
|
||||
return ok(queueNetworkJob(p.id, "restart", () => restartProfile(p.id)));
|
||||
case "snapshot":
|
||||
return ok(
|
||||
queueNetworkJob(p.id, "snapshot", (progress) =>
|
||||
snapshotProfile(p.id, progress),
|
||||
),
|
||||
);
|
||||
case "restore": {
|
||||
if (typeof body.backup !== "string") return fail("Choose a backup");
|
||||
const name = body.backup;
|
||||
return ok(
|
||||
queueNetworkJob(p.id, "restore", (progress) =>
|
||||
restoreProfile(p.id, name, progress),
|
||||
),
|
||||
);
|
||||
}
|
||||
default:
|
||||
return fail("Unknown network action");
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user