feat: 新增webdav

This commit is contained in:
2026-10-16 09:03:23 -07:00
parent 86c96b1a32
commit 94203e5746
15 changed files with 883 additions and 5 deletions
+63
View File
@@ -0,0 +1,63 @@
// 统一启动入口:本地运行 Next CLI,Docker 保留 standalone,两者共管 WebDAV 生命周期。
import { execFile, spawn } from "node:child_process";
import { createRequire } from "node:module";
import { getWebdavConfig } from "../src/server/webdav/config.mjs";
const mode = process.argv[2] || "standalone";
if (!["dev", "start", "standalone"].includes(mode))
throw new Error("启动模式必须为 dev、start 或 standalone");
const local = mode !== "standalone";
const load = createRequire(import.meta.url);
if (local) {
process.env.NODE_ENV = mode === "dev" ? "development" : "production";
// 复用当前 Next 安装中的 env loader,与 Next 的 .env* 优先级和变量展开保持一致。
const loadNext = createRequire(load.resolve("next/package.json"));
loadNext("@next/env").loadEnvConfig(process.cwd(), mode === "dev");
process.env.PORT = process.env.PORT?.trim() || "3200";
process.env.WEBDAV_PORT = process.env.WEBDAV_PORT?.trim() || "3201";
}
const config = getWebdavConfig();
if (config.enabled && config.port === Number(process.env.PORT))
throw new Error("WebDAV 与管理页不能使用相同端口");
const children = [];
let stopping = false;
const stop = (code = 0) => {
if (stopping) return;
stopping = true;
process.exitCode = code;
for (const child of children) {
if (child.exitCode !== null || child.signalCode !== null) continue;
// next dev 还会创建服务进程;Windows 必须结束整个子进程树,避免残留监听端口。
if (process.platform === "win32")
execFile("taskkill", ["/PID", String(child.pid), "/T", "/F"], () => {});
else child.kill("SIGTERM");
}
setTimeout(() => {
for (const child of children) child.kill("SIGKILL");
}, 6000).unref();
};
const start = (file, args = [], env = process.env) => {
const child = spawn(process.execPath, [file, ...args], {
stdio: "inherit",
env
});
children.push(child);
child.on("error", () => stop(1));
child.on("exit", (code, signal) => {
if (!stopping) stop(code || (signal ? 1 : 0));
});
};
if (local)
start(load.resolve("next/dist/bin/next"), [mode, "-p", process.env.PORT]);
else start("server.js");
if (config.enabled) {
const env = { ...process.env };
// 本地无密钥的演示模式沿用网页的 mock 密钥,不改变 Next 进程的演示提示。
if (local && !env.TORBOX_API_KEY?.trim() && !env.ACCESS_TOKEN?.trim())
env.ACCESS_TOKEN = "mock";
start("scripts/webdav.mjs", [], env);
}
process.once("SIGINT", () => stop());
process.once("SIGTERM", () => stop());
+30
View File
@@ -0,0 +1,30 @@
import { createWebDavServer } from "../src/server/webdav/server.mjs";
import { getWebdavConfig } from "../src/server/webdav/config.mjs";
process.env.WEBDAV_PORT ||= "3201";
const config = getWebdavConfig();
if (!config.enabled) process.exit(0);
const server = createWebDavServer({
appUrl: config.appUrl,
username: config.username,
password: process.env.ACCESS_TOKEN,
basePath: config.basePath
});
server.headersTimeout = 10_000;
server.requestTimeout = 30_000;
server.on("error", () => {
console.error("WebDAV 启动失败,请检查监听地址和端口是否被占用");
process.exitCode = 1;
});
server.listen(config.port, config.host, () => {
console.info(`WebDAV 已启动:端口 ${config.port},仅提供只读文件访问`);
});
const close = () => {
server.close();
// 先允许正在进行的传输结束,容器停止时最多等待 5 秒。
setTimeout(() => server.closeAllConnections(), 5000).unref();
};
process.once("SIGINT", close);
process.once("SIGTERM", close);