Compare commits

..

No commits in common. "70e5e5eddfec386c02d7bf34680bdbaa5f02fed2" and "dd36e2ab1af51370f91ff5cc2b9b3f18d0f86485" have entirely different histories.

16 changed files with 319 additions and 282 deletions

View File

@ -1,7 +1,6 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ExceptionCaughtLocallyJS" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="ExceptionCaughtLocallyJS" enabled="false" level="WARNING" enabled_by_default="false" />
</profile> </profile>
</component> </component>

View File

@ -1,9 +0,0 @@
{
"deployments": [
{
"path": "test/static-files",
"region": "static-files",
"site": "static-files-des"
}
]
}

Binary file not shown.

View File

@ -1,4 +1,4 @@
import { Builtins, Cli } from "clipanion" import { Cli } from "clipanion"
import figlet from "figlet" import figlet from "figlet"
import chalk from "chalk" import chalk from "chalk"
@ -9,8 +9,6 @@ import { StatusCommand } from "./src/cmd/status.ts"
import { InfoCommand } from "./src/cmd/info.ts" import { InfoCommand } from "./src/cmd/info.ts"
import { ProcessCommand } from "./src/cmd/process-info.ts" import { ProcessCommand } from "./src/cmd/process-info.ts"
import { DeployCommand } from "./src/cmd/deploy.ts" import { DeployCommand } from "./src/cmd/deploy.ts"
import { SyncCommand } from "./src/cmd/sync.ts"
import { ReloadCommand } from "./src/cmd/reload.ts"
const [node, app, ...args] = process.argv const [node, app, ...args] = process.argv
@ -28,8 +26,6 @@ const cli = new Cli({
binaryVersion: `1.0.0` binaryVersion: `1.0.0`
}) })
cli.register(Builtins.VersionCommand)
cli.register(Builtins.HelpCommand)
cli.register(LoginCommand) cli.register(LoginCommand)
cli.register(LogoutCommand) cli.register(LogoutCommand)
cli.register(ListServerCommand) cli.register(ListServerCommand)
@ -37,6 +33,4 @@ cli.register(StatusCommand)
cli.register(InfoCommand) cli.register(InfoCommand)
cli.register(ProcessCommand) cli.register(ProcessCommand)
cli.register(DeployCommand) cli.register(DeployCommand)
cli.register(SyncCommand)
cli.register(ReloadCommand)
cli.runExit(args) cli.runExit(args)

View File

@ -4,7 +4,6 @@
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"@types/cli-progress": "^3.11.6",
"@types/figlet": "^1.5.8" "@types/figlet": "^1.5.8"
}, },
"peerDependencies": { "peerDependencies": {
@ -12,7 +11,6 @@
}, },
"dependencies": { "dependencies": {
"chalk": "^5.3.0", "chalk": "^5.3.0",
"cli-progress": "^3.12.0",
"cli-table3": "^0.6.5", "cli-table3": "^0.6.5",
"clipanion": "^4.0.0-rc.4", "clipanion": "^4.0.0-rc.4",
"figlet": "^1.7.0", "figlet": "^1.7.0",

View File

@ -6,7 +6,6 @@ import * as fs from "node:fs"
import * as child_process from "node:child_process" import * as child_process from "node:child_process"
import * as path from "node:path" import * as path from "node:path"
import { createAuthHeader } from "../utils/auth.ts" import { createAuthHeader } from "../utils/auth.ts"
import { RsLocalConfig } from "../utils/config-local.ts"
export class DeployCommand extends Command { export class DeployCommand extends Command {
static paths = [[`deploy`]] static paths = [[`deploy`]]
@ -14,53 +13,54 @@ export class DeployCommand extends Command {
category: `Building`, category: `Building`,
description: `Deploying App / Static Site onto RoadSign`, description: `Deploying App / Static Site onto RoadSign`,
details: `Deploying an application or hosting a static site via RoadSign, you need preconfigured the RoadSign, or sync the configurations via sync command.`, details: `Deploying an application or hosting a static site via RoadSign, you need preconfigured the RoadSign, or sync the configurations via sync command.`,
examples: [ examples: [["Deploying to RoadSign", `deploy <server> <site> <slug> <file / directory>`]]
["Deploying to RoadSign", `deploy <server> <region> <site> <file / directory>`],
["Deploying to RoadSign with .roadsignrc file", `deploy <server>`]
]
} }
server = Option.String({ required: true }) server = Option.String({ required: true })
region = Option.String({ required: false }) site = Option.String({ required: true })
site = Option.String({ required: false }) upstream = Option.String({ required: true })
input = Option.String({ required: false }) input = Option.String({ required: true })
async deploy(serverLabel: string, region: string, site: string, input: string) { async execute() {
const cfg = await RsConfig.getInstance() const config = await RsConfig.getInstance()
const server = cfg.config.servers.find(item => item.label === serverLabel)
const server = config.config.servers.find(item => item.label === this.server)
if (server == null) { if (server == null) {
this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`)) this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`))
return return
} }
if (!fs.existsSync(input)) { if (!fs.existsSync(this.input)) {
this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} was not found.\n`)) this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} was not found.\n`))
return return
} }
let isDirectory = false let isDirectory = false
if (fs.statSync(input).isDirectory()) { if (fs.statSync(this.input).isDirectory()) {
input = path.join(input, "*") if (this.input.endsWith("/")) {
this.input = this.input.slice(0, -1)
}
this.input += "/*"
const compressPrefStart = performance.now() const compressPrefStart = performance.now()
const compressSpinner = ora(`Compressing ${chalk.bold(input)}...`).start() const compressSpinner = ora(`Compressing ${chalk.bold(this.input)}...`).start()
const destName = `${Date.now()}-roadsign-archive.zip` const destName = `${Date.now()}-roadsign-archive.zip`
child_process.execSync(`zip -rj ${destName} ${input}`) child_process.execSync(`zip -rj ${destName} ${this.input}`)
const compressPrefTook = performance.now() - compressPrefStart const compressPrefTook = performance.now() - compressPrefStart
compressSpinner.succeed(`Compressing completed in ${(compressPrefTook / 1000).toFixed(2)}s 🎉`) compressSpinner.succeed(`Compressing completed in ${(compressPrefTook / 1000).toFixed(2)}s 🎉`)
input = destName this.input = destName
isDirectory = true isDirectory = true
} }
const destBreadcrumb = [region, site].join(" ➜ ") const destBreadcrumb = [this.site, this.upstream].join(" ➜ ")
const spinner = ora(`Deploying ${chalk.bold(destBreadcrumb)} to ${chalk.bold(this.server)}...`).start() const spinner = ora(`Deploying ${chalk.bold(destBreadcrumb)} to ${chalk.bold(this.server)}...`).start()
const prefStart = performance.now() const prefStart = performance.now()
try { try {
const payload = new FormData() const payload = new FormData()
payload.set("attachments", await fs.openAsBlob(input), isDirectory ? "dist.zip" : path.basename(input)) payload.set("attachments", await fs.openAsBlob(this.input), isDirectory ? "dist.zip" : path.basename(this.input))
const res = await fetch(`${server.url}/webhooks/publish/${region}/${site}?mimetype=application/zip`, { const res = await fetch(`${server.url}/webhooks/publish/${this.site}/${this.upstream}?mimetype=application/zip`, {
method: "PUT", method: "PUT",
body: payload, body: payload,
headers: { headers: {
@ -76,37 +76,10 @@ export class DeployCommand extends Command {
this.context.stdout.write(`Failed to deploy to remote: ${e}\n`) this.context.stdout.write(`Failed to deploy to remote: ${e}\n`)
spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`) spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`)
} finally { } finally {
if (isDirectory && input.endsWith(".zip")) { if (isDirectory && this.input.endsWith(".zip")) {
fs.unlinkSync(input) fs.unlinkSync(this.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) process.exit(0)
} }

View File

@ -1,53 +0,0 @@
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>`],
]
}
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)
}
}

View File

@ -1,87 +0,0 @@
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 <server> <region> <file>`],
["Sync to RoadSign with .roadsignrc file", `sync <server>`]
]
}
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)
}
}

View File

@ -1,60 +0,0 @@
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<RsLocalConfig> {
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 }

View File

@ -1,9 +0,0 @@
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"

View File

@ -0,0 +1,89 @@
package conn
import (
"encoding/json"
"fmt"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
"github.com/urfave/cli/v2"
)
var CliCommands = []*cli.Command{
{
Name: "list",
Aliases: []string{"ls"},
Description: "List all connected remote server",
Action: func(ctx *cli.Context) error {
var servers []CliConnection
raw, _ := json.Marshal(viper.Get("servers"))
_ = json.Unmarshal(raw, &servers)
log.Info().Msgf("There are %d server(s) connected in total.", len(servers))
for idx, server := range servers {
log.Info().Msgf("%d) %s: %s", idx+1, server.ID, server.Url)
}
return nil
},
},
{
Name: "connect",
Aliases: []string{"add"},
Description: "Connect and save configuration of remote server",
ArgsUsage: "<id> <server url> <credential>",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() < 3 {
return fmt.Errorf("must have three arguments: <id> <server url> <credential>")
}
c := CliConnection{
ID: ctx.Args().Get(0),
Url: ctx.Args().Get(1),
Credential: ctx.Args().Get(2),
}
if err := c.CheckConnectivity(); err != nil {
return fmt.Errorf("couldn't connect server: %s", err.Error())
} else {
var servers []CliConnection
raw, _ := json.Marshal(viper.Get("servers"))
_ = json.Unmarshal(raw, &servers)
viper.Set("servers", append(servers, c))
if err := viper.WriteConfig(); err != nil {
return err
} else {
log.Info().Msg("Successfully connected a new remote server, enter \"rds ls\" to get more info.")
return nil
}
}
},
},
{
Name: "disconnect",
Aliases: []string{"remove"},
Description: "Remove a remote server configuration",
ArgsUsage: "<id>",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() < 1 {
return fmt.Errorf("must have more one arguments: <server url>")
}
var servers []CliConnection
raw, _ := json.Marshal(viper.Get("servers"))
_ = json.Unmarshal(raw, &servers)
viper.Set("servers", lo.Filter(servers, func(item CliConnection, idx int) bool {
return item.ID != ctx.Args().Get(0)
}))
if err := viper.WriteConfig(); err != nil {
return err
} else {
log.Info().Msg("Successfully disconnected a remote server, enter \"rds ls\" to get more info.")
return nil
}
},
},
}

View File

@ -0,0 +1,42 @@
package conn
import (
"encoding/json"
"fmt"
"strings"
roadsign "git.solsynth.dev/goatworks/roadsign/pkg"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
)
type CliConnection struct {
ID string `json:"id"`
Url string `json:"url"`
Credential string `json:"credential"`
}
func (v CliConnection) CheckConnectivity() error {
client := fiber.Get(v.Url + "/cgi/metadata")
client.BasicAuth("RoadSign CLI", v.Credential)
if status, data, err := client.Bytes(); len(err) > 0 {
return fmt.Errorf("couldn't connect to server: %q", err)
} else if status != 200 {
return fmt.Errorf("server rejected request, may cause by invalid credential")
} else {
var resp fiber.Map
if err := json.Unmarshal(data, &resp); err != nil {
return err
} else if resp["server"] != "RoadSign" {
return fmt.Errorf("remote server isn't roadsign")
} else if resp["version"] != roadsign.AppVersion {
if strings.Contains(roadsign.AppVersion, "#") {
return fmt.Errorf("remote server version mismatch client version, update or downgrade client required")
} else {
log.Warn().Msg("RoadSign CLI didn't complied with vcs information, compatibility was disabled. To enable it, reinstall cli with -buildvcs flag.")
}
}
}
return nil
}

View File

@ -0,0 +1,17 @@
package conn
import (
"encoding/json"
"github.com/samber/lo"
"github.com/spf13/viper"
)
func GetConnection(id string) (CliConnection, bool) {
var servers []CliConnection
raw, _ := json.Marshal(viper.Get("servers"))
_ = json.Unmarshal(raw, &servers)
return lo.Find(servers, func(item CliConnection) bool {
return item.ID == id
})
}

View File

@ -0,0 +1,98 @@
package deploy
import (
"fmt"
"io"
"os"
"strings"
jsoniter "github.com/json-iterator/go"
"git.solsynth.dev/goatworks/roadsign/pkg/cmd/rdc/conn"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
)
var DeployCommands = []*cli.Command{
{
Name: "deploy",
Aliases: []string{"dp"},
ArgsUsage: "<server> <site> <upstream> [path]",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() < 4 {
return fmt.Errorf("must have four arguments: <server> <site> <upstream> <path>")
}
if !strings.HasSuffix(ctx.Args().Get(3), ".zip") {
return fmt.Errorf("input file must be a zip file and ends with .zip")
}
server, ok := conn.GetConnection(ctx.Args().Get(0))
if !ok {
return fmt.Errorf("server was not found, use \"rds connect\" add one first")
} else if err := server.CheckConnectivity(); err != nil {
return fmt.Errorf("couldn't connect server: %s", err.Error())
}
// Send request
log.Info().Msg("Now publishing to remote server...")
url := fmt.Sprintf("/webhooks/publish/%s/%s?mimetype=%s", ctx.Args().Get(1), ctx.Args().Get(2), "application/zip")
client := fiber.Put(server.Url+url).
SendFile(ctx.Args().Get(3), "attachments").
MultipartForm(nil).
BasicAuth("RoadSign CLI", server.Credential)
if status, data, err := client.Bytes(); len(err) > 0 {
return fmt.Errorf("failed to publish to remote: %q", err)
} else if status != 200 {
return fmt.Errorf("server rejected request, status code %d, response %s", status, string(data))
}
log.Info().Msg("Well done! Your site is successfully published! 🎉")
return nil
},
},
{
Name: "sync",
ArgsUsage: "<server> <site> <configuration path>",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() < 3 {
return fmt.Errorf("must have three arguments: <server> <site> <configuration path>")
}
server, ok := conn.GetConnection(ctx.Args().Get(0))
if !ok {
return fmt.Errorf("server was not found, use \"rds connect\" add one first")
} else if err := server.CheckConnectivity(); err != nil {
return fmt.Errorf("couldn't connect server: %s", err.Error())
}
var raw []byte
if file, err := os.Open(ctx.Args().Get(2)); err != nil {
return err
} else {
raw, _ = io.ReadAll(file)
}
url := fmt.Sprintf("/webhooks/sync/%s", ctx.Args().Get(1))
client := fiber.Put(server.Url+url).
JSONEncoder(jsoniter.ConfigCompatibleWithStandardLibrary.Marshal).
JSONDecoder(jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal).
Body(raw).
BasicAuth("RoadSign CLI", server.Credential)
if status, data, err := client.Bytes(); len(err) > 0 {
return fmt.Errorf("failed to sync to remote: %q", err)
} else if status != 200 {
return fmt.Errorf("server rejected request, status code %d, response %s", status, string(data))
}
log.Info().Msg("Well done! Your site configuration is up-to-date! 🎉")
return nil
},
},
}

48
pkg/cmd/rdc/main.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"os"
roadsign "git.solsynth.dev/goatworks/roadsign/pkg"
"git.solsynth.dev/goatworks/roadsign/pkg/cmd/rdc/conn"
"git.solsynth.dev/goatworks/roadsign/pkg/cmd/rdc/deploy"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"github.com/urfave/cli/v2"
)
func init() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
}
func main() {
// Configure settings
viper.AddConfigPath("$HOME")
viper.SetConfigName(".roadsignrc")
viper.SetConfigType("toml")
// Load settings
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
viper.SafeWriteConfig()
viper.ReadInConfig()
} else {
log.Panic().Err(err).Msg("An error occurred when loading settings.")
}
}
// Configure CLI
app := &cli.App{
Name: "RoadSign CLI",
Version: roadsign.AppVersion,
Suggest: true,
Commands: append(append([]*cli.Command{}, conn.CliCommands...), deploy.DeployCommands...),
}
// Run CLI
if err := app.Run(os.Args); err != nil {
log.Fatal().Err(err).Msg("An error occurred when running cli.")
}
}

View File

@ -38,11 +38,8 @@ func doSync(c *fiber.Ctx) error {
if file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755); err != nil { 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()) return fiber.NewError(fiber.ErrInternalServerError.Code, err.Error())
} else { } else {
var testOut map[string]any raw, _ := toml.Marshal(req)
if err := toml.Unmarshal([]byte(req), &testOut); err != nil { file.Write(raw)
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid configuration: %v", err))
}
_, _ = file.Write([]byte(req))
defer file.Close() defer file.Close()
} }