diff --git a/docs/configuration.md b/docs/configuration.md index 2285e319..39013cfc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -60,6 +60,17 @@ This should be a full URL ending with a /. Defaults to null meaning assume localhost. +### controller.static_url + +Used to override where static assets such as the web bundle is loaded from. +This can be used if you want to put the static assets on a separate cdn url like `https://cdn.example.com/static/` while keeping the API hosted directly by clusterio. +Due to a limitation on how the webpack assets are built the pathname component of the url have to be `/static/`. + +See also the `clusteriocontroller copy-static` command for how to obtain the static asset files that need to be hosted. + +Defaults to null meaning use the internally hosted assets. + + ### controller.tls_certificate Path to TLS certificate to use for the HTTPS server when controller.https_port is configured. diff --git a/packages/controller/README.md b/packages/controller/README.md index a953686e..822aff13 100644 --- a/packages/controller/README.md +++ b/packages/controller/README.md @@ -104,6 +104,16 @@ This can be used both in the web interface and as the `controller_token` to conn Creates a clusterioctl config for the given user with url and token set up for connecting to the cluster. +### `copy-static [target]` + +Copies the static web assets to the given target folder (which defaults to `./static/`). +Useful in case you want to host the assets on a CDN or separate web server and want provide the files directly instead of fetching them from the controller. + +Note that mod pack exports will create additional static files that also have to be hosted, these are written to the `./static/` folder in the clusterio installation directory. + +See also the `controller.public_url` config option for setting where static assets are loaded from. + + ### `run` Runs the controller. diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 851f1236..44d92502 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -211,6 +211,40 @@ async function handleBootstrapCommand( } } +async function handleCopyStaticCommand( + args: any, + pluginInfos: lib.PluginNodeEnvInfo[], +) { + await fs.cp( + path.join(__dirname, "..", "web", "static"), + args.target, + { + recursive: true, + mode: fs.constants.COPYFILE_FICLONE, + }, + ); + logger.info("Copied main web interface files"); + for (const pluginInfo of pluginInfos) { + try { + await fs.cp( + pluginInfo.webStaticPath, + args.target, + { + recursive: true, + mode: fs.constants.COPYFILE_FICLONE, + }, + ); + } catch (err: any) { + if (err.code === "ENOENT") { + logger.info(`Skipped web interface files for ${pluginInfo.name}: ${err.message}`); + } else { + logger.error(`Unexpected error copying web interface files for ${pluginInfo.name}:\n${err.stack}`); + process.exitCode = 1; + } + } + logger.info(`Copied web interface files for ${pluginInfo.name}`); + } +} export interface ControllerArgs { [x: string]: unknown; @@ -284,6 +318,13 @@ async function initialize(): Promise { }) .demandCommand(1, "You need to specify a command to run"); }) + .command("copy-static [target]", "Copy static Web UI files to a folder", yargs => { + yargs.positional("target", { + describe: "Target directory to copy files to", + type: "string", + default: "static", + }); + }) .command("run", "Run controller", yargs => { yargs.option("can-restart", { type: "boolean", nargs: 0, default: false, @@ -399,6 +440,8 @@ async function initialize(): Promise { await lib.handleConfigCommand(args, controllerConfig, controllerConfigLock); } else if (command === "bootstrap") { await handleBootstrapCommand(args, controllerConfig, controllerConfigLock); + } else if (command === "copy-static") { + await handleCopyStaticCommand(args, pluginInfos); } else if (shouldRun) { await controllerConfigLock.acquire(); // Hold the lock until process exit } diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index cd63d9cf..bcbdd007 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -355,6 +355,8 @@ export default class Controller { this.onSystemMetricsIntervalChanged(); } else if (field === "controller.trusted_proxies") { this.trustedProxies = this.parseTrustedProxies(); + } else if (field === "controller.static_url") { + this.app.locals.staticRoot = this.config.get("controller.static_url"); } lib.invokeHook(this.plugins, "onControllerConfigFieldChanged", field, curr, prev); }); @@ -389,6 +391,7 @@ export default class Controller { } this.app.locals.mainBundle = manifest["main.js"] || "no_web_build"; } + this.app.locals.staticRoot = this.config.get("controller.static_url"); // Load plugins await this.loadPlugins(); @@ -866,7 +869,7 @@ export default class Controller { ); } - static addAppRoutes(app: Application, pluginInfos: any[]) { + static addAppRoutes(app: Application, pluginInfos: lib.PluginNodeEnvInfo[]) { app.use((req: Request, res: Response, next) => { const startNs = process.hrtime.bigint(); stream.finished(res, () => { @@ -915,9 +918,7 @@ export default class Controller { app.get(route, Controller.serveWeb(route)); } - let pluginPackagePath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json")); - let webPath = path.join(path.dirname(pluginPackagePath), "dist", "web", "static"); - app.use("/static", express.static(webPath, staticOptions)); + app.use("/static", express.static(pluginInfo.webStaticPath, staticOptions)); } } @@ -1231,7 +1232,7 @@ export default class Controller { return function(req: Request, res: Response, next: NextFunction) { let depth = routeDepth + Number(req.path.slice(-1) === "/"); let webRoot = "../".repeat(depth) || "./"; - let staticRoot = `${webRoot}static/`; + let staticRoot = res.app.locals.staticRoot ?? `${webRoot}static/`; let mainBundle: string = ""; if (res.app.locals.mainBundle) { mainBundle = res.app.locals.mainBundle; diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index bcd3eaaa..a4e1da70 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -22,6 +22,7 @@ declare global { export interface Locals { controller: Controller, mainBundle: string, + staticRoot: string | null, devPlugins: Map, streams: Map } diff --git a/packages/controller/web/index.html b/packages/controller/web/index.html index 5a5a8af0..4bf444cd 100644 --- a/packages/controller/web/index.html +++ b/packages/controller/web/index.html @@ -29,7 +29,7 @@