From 70e5e5eddfec386c02d7bf34680bdbaa5f02fed2 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 3 Oct 2024 01:38:27 +0800 Subject: [PATCH] :sparkles: Sync, reload command --- .idea/inspectionProfiles/Project_Default.xml | 1 + cli/.roadsignrc | 9 ++ cli/bun.lockb | Bin 13767 -> 14567 bytes cli/index.ts | 8 +- cli/package.json | 2 + cli/src/cmd/deploy.ts | 71 ++++++++++----- cli/src/cmd/reload.ts | 53 +++++++++++ cli/src/cmd/sync.ts | 87 +++++++++++++++++++ cli/src/utils/config-local.ts | 60 +++++++++++++ cli/test/static-files.toml | 9 ++ pkg/sideload/regions.go | 7 +- 11 files changed, 282 insertions(+), 25 deletions(-) create mode 100644 cli/.roadsignrc create mode 100644 cli/src/cmd/reload.ts create mode 100644 cli/src/cmd/sync.ts create mode 100644 cli/src/utils/config-local.ts create mode 100644 cli/test/static-files.toml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index cb83045..7b0a9ce 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/cli/.roadsignrc b/cli/.roadsignrc new file mode 100644 index 0000000..daf0e70 --- /dev/null +++ b/cli/.roadsignrc @@ -0,0 +1,9 @@ +{ + "deployments": [ + { + "path": "test/static-files", + "region": "static-files", + "site": "static-files-des" + } + ] +} \ No newline at end of file diff --git a/cli/bun.lockb b/cli/bun.lockb index 8d696639569706ce4c8a05b09140da463cea88eb..fbf857bdb34dbe97e3bc55a41887992f875943fb 100755 GIT binary patch delta 2634 zcmd5;X-rgC6n^iq%^McMb%sTUZH56E7>3PCff^+hB$aB#SSyY&;wZw1U}`l&tu?Zk zpa&CTt5{8p)~4DZ7AsnpsGzlVY1D+e#U@s4tTqNUso3w#yqD0%CjHg)CikBEopaCf z&RxzsyX4-o7K?fN&7$)O!{$$^_wG(?`D5wFr$5;juwaA5Ib&;+-0ZVDxHwaAH{+(_ zadkmP5Aciuf>2*oZFhqKj9TC<(L?)zS$+X{B^M|Q zo5g|5uKRLU{Pp8)F^Gz zC#!xfzp!EWgD5&6%@!Av?3E)6$XymJ`ca5ihMcD$Uq<3u>cxsX;7!L0kuHH}rK#RI zqKg_Z9->~1Pe}I3ktcfzLJC&-P>4^4yaI@IB)8QwK$G3*l6Qur^(L)vww#X~vtu6u zQ*H&~B~V8gcY(5jyxk}g*-r=Z2CWnL+oKi6 zP|Aui3ZE(CSOqg>%y*cn37s2L#&PKEC|MYHLJu7@1D0x_Dt z(iMv_bXZ?3#&(2+UK2HYaY^~5>ad5-6g$Erp81k1bb)rd@~HP1X>3eZ-QY`4N4%zLJU&}(>T8ErT3JS>8=2OnUJm^zSl<@bjc-7L>zD1mA*?Xz7rEs&j%7XFf?TcQ$KH0Hk_NA|K%Q!W z1wkmLM6*roC%aiMZlhIZeVAIABckp^AD5#J4~=ENS)Zm>@-M2a2P+5upP_;dM}l4xHI$Q-m^2h7dJY|x*zCrk d{8ucqTGA~wOnegvr~0y7=54m{7=;)^UVMN delta 2169 zcmc&#T}%{L6rMZ$?92kTMU)*D0T)= zzxmEN_uMn*{{7VcWmBK-x%H@Bo}KP|+c7lp>CPi>HaT527k-?be9tkk-BbAO;-O$b zGFo1LZFX;3o=MC;S(09F?Wi}%GEWG8GTf~&uz?u7f3XP#DDD{jo!42;nod55SbPh41? zMd%7du#rD5s1~47%o3T2-82V}&!O82ouCKMrBWn5ENW;3{TZ4={|$K(!fJk;B$dG{ zf&2+UwHA!W$VSu;!L}IeL3~hNh$C-erRqZsd8!10Qhf!CT`(MJd;wMo7H=>a)h!3J zv8Sk_h%GEm(+K*TG>2X#j}jJ}DT4kuji8^PIVG%Sqq^HMF3y|*jAJSWi-MUy+HMOb z{LGA+Z5xG;BKEMnSE2n$mGb)vodY~nXx?5aHjpPNEO)Ck58aqb-sBCNunZClmO*+7 zf>KGT5N?e4acLD3wB=By6Upf`n5jdn6SSqs`bB9F_8aYoKy zZHzBP9_Nbbmte0kQ7#M|C?CR(ZU{F<<^@b*?LU_Qac~BVmM+mivfZi$OIg1s+IQrc zzkVn0RB;*=xeIa6JpR*cY~eNJl4FM&&EmW~*1Bion|*k#aa|ZK2R-X>lK9RuzSqVO zSr~CS2CqH)8EXwVF7oMgfr}ox^H#Bq=U4;bJ;%G;PX2mg{xT zE{?dnjCsp+T!Pm1{^)@CxXrcYJ6M7`#!AvOHG4Iu^-k4^!SAaUhuUB-M`XVw4b!_` zO`NAO^oB3p_hu}YiCL3g+t039oSF425~qkj}@3f&Ld=%w `]] + examples: [ + ["Deploying to RoadSign", `deploy `], + ["Deploying to RoadSign with .roadsignrc file", `deploy `] + ] } server = Option.String({ required: true }) - site = Option.String({ required: true }) - upstream = Option.String({ required: true }) - input = Option.String({ required: true }) + region = Option.String({ required: false }) + site = Option.String({ required: false }) + input = Option.String({ required: false }) - async execute() { - const config = await RsConfig.getInstance() - - const server = config.config.servers.find(item => item.label === this.server) + async deploy(serverLabel: string, region: string, site: string, input: string) { + const cfg = await RsConfig.getInstance() + const server = cfg.config.servers.find(item => item.label === serverLabel) if (server == null) { this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`)) return } - if (!fs.existsSync(this.input)) { + if (!fs.existsSync(input)) { this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} was not found.\n`)) return } let isDirectory = false - if (fs.statSync(this.input).isDirectory()) { - if (this.input.endsWith("/")) { - this.input = this.input.slice(0, -1) - } - this.input += "/*" + if (fs.statSync(input).isDirectory()) { + input = path.join(input, "*") const compressPrefStart = performance.now() - const compressSpinner = ora(`Compressing ${chalk.bold(this.input)}...`).start() + const compressSpinner = ora(`Compressing ${chalk.bold(input)}...`).start() const destName = `${Date.now()}-roadsign-archive.zip` - child_process.execSync(`zip -rj ${destName} ${this.input}`) + child_process.execSync(`zip -rj ${destName} ${input}`) const compressPrefTook = performance.now() - compressPrefStart compressSpinner.succeed(`Compressing completed in ${(compressPrefTook / 1000).toFixed(2)}s 🎉`) - this.input = destName + input = destName isDirectory = true } - const destBreadcrumb = [this.site, this.upstream].join(" ➜ ") + const destBreadcrumb = [region, site].join(" ➜ ") const spinner = ora(`Deploying ${chalk.bold(destBreadcrumb)} to ${chalk.bold(this.server)}...`).start() const prefStart = performance.now() try { const payload = new FormData() - payload.set("attachments", await fs.openAsBlob(this.input), isDirectory ? "dist.zip" : path.basename(this.input)) - const res = await fetch(`${server.url}/webhooks/publish/${this.site}/${this.upstream}?mimetype=application/zip`, { + payload.set("attachments", await fs.openAsBlob(input), isDirectory ? "dist.zip" : path.basename(input)) + const res = await fetch(`${server.url}/webhooks/publish/${region}/${site}?mimetype=application/zip`, { method: "PUT", body: payload, headers: { @@ -76,10 +76,37 @@ export class DeployCommand extends Command { this.context.stdout.write(`Failed to deploy to remote: ${e}\n`) spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`) } finally { - if (isDirectory && this.input.endsWith(".zip")) { - fs.unlinkSync(this.input) + if (isDirectory && input.endsWith(".zip")) { + fs.unlinkSync(input) } } + } + + async execute() { + if (this.region && this.site && this.input) { + await this.deploy(this.server, this.region, this.site, this.input) + } else { + let localCfg: RsLocalConfig + try { + localCfg = await RsLocalConfig.getInstance() + } catch (e) { + this.context.stdout.write(chalk.red(`Unable to load .roadsignrc: ${e}\n`)) + return + } + + if (!localCfg.config.deployments) { + this.context.stdout.write(chalk.red(`No deployments found in .roadsignrc, exiting...\n`)) + return + } + + let idx = 0 + for (const deployment of localCfg.config.deployments ?? []) { + this.context.stdout.write(chalk.cyan(`Deploying ${idx + 1} out of ${localCfg.config.deployments.length} deployments...\n`)) + await this.deploy(this.server, deployment.region, deployment.site, deployment.path) + } + + this.context.stdout.write(chalk.green(`All deployments has been deployed!\n`)) + } process.exit(0) } diff --git a/cli/src/cmd/reload.ts b/cli/src/cmd/reload.ts new file mode 100644 index 0000000..a229ca9 --- /dev/null +++ b/cli/src/cmd/reload.ts @@ -0,0 +1,53 @@ +import { RsConfig } from "../utils/config.ts" +import { Command, Option, type Usage } from "clipanion" +import chalk from "chalk" +import ora from "ora" +import * as fs from "node:fs" +import { createAuthHeader } from "../utils/auth.ts" +import { RsLocalConfig } from "../utils/config-local.ts" + +export class ReloadCommand extends Command { + static paths = [[`reload`]] + static usage: Usage = { + category: `Building`, + description: `Reload configuration on RoadSign`, + details: `Reload configuration on remote RoadSign to make changes applied.`, + examples: [ + ["Reload an connected server", `reload `], + ] + } + + server = Option.String({ required: true }) + + async execute() { + const cfg = await RsConfig.getInstance() + const server = cfg.config.servers.find(item => item.label === this.server) + if (server == null) { + this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`)) + return + } + + const spinner = ora(`Reloading server ${chalk.bold(this.server)}...`).start() + + const prefStart = performance.now() + + try { + const res = await fetch(`${server.url}/cgi/reload`, { + method: "POST", + headers: { + Authorization: createAuthHeader(server.credential) + } + }) + if (res.status !== 200) { + throw new Error(await res.text()) + } + const prefTook = performance.now() - prefStart + spinner.succeed(`Reloading completed in ${(prefTook / 1000).toFixed(2)}s 🎉`) + } catch (e) { + this.context.stdout.write(`Failed to reload remote: ${e}\n`) + spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`) + } + + process.exit(0) + } +} \ No newline at end of file diff --git a/cli/src/cmd/sync.ts b/cli/src/cmd/sync.ts new file mode 100644 index 0000000..b7ff4c7 --- /dev/null +++ b/cli/src/cmd/sync.ts @@ -0,0 +1,87 @@ +import { RsConfig } from "../utils/config.ts" +import { Command, Option, type Usage } from "clipanion" +import chalk from "chalk" +import ora from "ora" +import * as fs from "node:fs" +import { createAuthHeader } from "../utils/auth.ts" +import { RsLocalConfig } from "../utils/config-local.ts" + +export class SyncCommand extends Command { + static paths = [[`sync`]] + static usage: Usage = { + category: `Building`, + description: `Sync configuration to RoadSign over Sideload`, + details: `Update remote RoadSign configuration with local ones.`, + examples: [ + ["Sync to RoadSign", `sync `], + ["Sync to RoadSign with .roadsignrc file", `sync `] + ] + } + + server = Option.String({ required: true }) + region = Option.String({ required: false }) + input = Option.String({ required: false }) + + async sync(serverLabel: string, region: string, input: string) { + const cfg = await RsConfig.getInstance() + const server = cfg.config.servers.find(item => item.label === serverLabel) + if (server == null) { + this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`)) + return + } + + if (!fs.existsSync(input)) { + this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} was not found.\n`)) + return + } + if (!fs.statSync(input).isFile()) { + this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} is not a file.\n`)) + return + } + + const spinner = ora(`Syncing ${chalk.bold(region)} to ${chalk.bold(this.server)}...`).start() + + const prefStart = performance.now() + + try { + const res = await fetch(`${server.url}/webhooks/sync/${region}`, { + method: "PUT", + body: fs.readFileSync(input, "utf8"), + headers: { + Authorization: createAuthHeader(server.credential) + } + }) + if (res.status !== 200) { + throw new Error(await res.text()) + } + const prefTook = performance.now() - prefStart + spinner.succeed(`Syncing completed in ${(prefTook / 1000).toFixed(2)}s 🎉`) + } catch (e) { + this.context.stdout.write(`Failed to sync to remote: ${e}\n`) + spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`) + } + } + + async execute() { + if (this.region && this.input) { + await this.sync(this.server, this.region, this.input) + } else { + let localCfg: RsLocalConfig + try { + localCfg = await RsLocalConfig.getInstance() + } catch (e) { + this.context.stdout.write(chalk.red(`Unable to load .roadsignrc: ${e}\n`)) + return + } + + if (!localCfg.config.sync) { + this.context.stdout.write(chalk.red(`No sync configuration found in .roadsignrc, exiting...\n`)) + return + } + + await this.sync(this.server, localCfg.config.sync.region, localCfg.config.sync.configPath) + } + + process.exit(0) + } +} \ No newline at end of file diff --git a/cli/src/utils/config-local.ts b/cli/src/utils/config-local.ts new file mode 100644 index 0000000..32274e2 --- /dev/null +++ b/cli/src/utils/config-local.ts @@ -0,0 +1,60 @@ +import * as path from "node:path" +import * as fs from "node:fs/promises" + +interface RsLocalConfigData { + sync?: RsLocalConfigSyncData + deployments?: RsLocalConfigDeploymentData[] +} + +interface RsLocalConfigSyncData { + configPath: string + region: string +} + +interface RsLocalConfigDeploymentData { + path: string + region: string + site: string + autoBuild?: RsLocalConfigDeploymentAutoBuildData +} + +interface RsLocalConfigDeploymentAutoBuildData { + command: string + environment?: string[] +} + +class RsLocalConfig { + private static instance: RsLocalConfig + + public config: RsLocalConfigData = {} + + private constructor() { + } + + public static async getInstance(): Promise { + if (!RsLocalConfig.instance) { + RsLocalConfig.instance = new RsLocalConfig() + await RsLocalConfig.instance.readConfig() + } + return RsLocalConfig.instance + } + + public async readConfig() { + const basepath = process.cwd() + const filepath = path.join(basepath, ".roadsignrc") + if (!await fs.exists(filepath)) { + throw new Error(`.roadsignrc file was not found at ${filepath}`) + } + + const data = await fs.readFile(filepath, "utf8") + this.config = JSON.parse(data) + } + + public async writeConfig() { + const basepath = process.cwd() + const filepath = path.join(basepath, ".roadsignrc") + await fs.writeFile(filepath, JSON.stringify(this.config)) + } +} + +export { RsLocalConfig, type RsLocalConfigData } \ No newline at end of file diff --git a/cli/test/static-files.toml b/cli/test/static-files.toml new file mode 100644 index 0000000..5927820 --- /dev/null +++ b/cli/test/static-files.toml @@ -0,0 +1,9 @@ +id = "static-files-num2" + +[[locations]] +id = "static-files-loc-num2" +hosts = ["127.0.0.1:8000"] +paths = ["/"] +[[locations.destinations]] +id = "static-files-des-num2" +uri = "files://../data/static-files" diff --git a/pkg/sideload/regions.go b/pkg/sideload/regions.go index 33e753d..dba4f44 100644 --- a/pkg/sideload/regions.go +++ b/pkg/sideload/regions.go @@ -38,8 +38,11 @@ func doSync(c *fiber.Ctx) error { if file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755); err != nil { return fiber.NewError(fiber.ErrInternalServerError.Code, err.Error()) } else { - raw, _ := toml.Marshal(req) - file.Write(raw) + var testOut map[string]any + if err := toml.Unmarshal([]byte(req), &testOut); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid configuration: %v", err)) + } + _, _ = file.Write([]byte(req)) defer file.Close() }