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/test/integration/clusterio.js b/test/integration/clusterio.js index f0d15f83..aa13282d 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -140,6 +140,25 @@ describe("Integration of Clusterio", function() { }); }); + describe("copy-static", function() { + it("should copy the files from dist/web/static", async function() { + const dir = "temp/test/copy-static"; + await fs.rm(dir, { force: true, recursive: true, maxRetries: 10 }); + await execController("copy-static copy-static"); + const controllerFiles = await fs.readdir("packages/controller/dist/web/static"); + const globalChatFiles = await fs.readdir("plugins/global_chat/dist/web/static"); + const targetFiles = await fs.readdir(dir); + assert( + controllerFiles.every(name => targetFiles.includes(name)), + "Missing files from controller static folder", + ); + assert( + globalChatFiles.every(name => targetFiles.includes(name)), + "Missing files from global_chat's static folder", + ); + }); + }); + describe("config", function() { it("can read the config file", async function() { await execController("config list");