diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index 9d06782..93061f3 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -32,6 +32,8 @@ import User from "./models/user"; import childProcessAsync from "promisify-child-process"; import { Terminal } from "./terminal"; +import "dotenv/config"; + export class DockgeServer { app : Express; httpServer : http.Server; diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/socket-handlers/docker-socket-handler.ts index d4e11c3..88e48b6 100644 --- a/backend/socket-handlers/docker-socket-handler.ts +++ b/backend/socket-handlers/docker-socket-handler.ts @@ -12,7 +12,7 @@ export class DockerSocketHandler extends SocketHandler { socket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => { try { checkLogin(socket); - const stack = this.saveStack(socket, server, name, composeYAML, composeENV, isAdd); + const stack = await this.saveStack(socket, server, name, composeYAML, composeENV, isAdd); await stack.deploy(socket); server.sendStackList(); callback({ @@ -264,7 +264,7 @@ export class DockerSocketHandler extends SocketHandler { }); } - saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown) : Stack { + async saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown) : Promise { // Check types if (typeof(name) !== "string") { throw new ValidationError("Name must be a string"); @@ -280,7 +280,7 @@ export class DockerSocketHandler extends SocketHandler { } const stack = new Stack(server, name, composeYAML, composeENV, false); - stack.save(isAdd); + await stack.save(isAdd); return stack; } diff --git a/backend/stack.ts b/backend/stack.ts index 92ffc48..c0718ce 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -1,8 +1,8 @@ import { DockgeServer } from "./dockge-server"; -import fs from "fs"; +import fs, { promises as fsAsync } from "fs"; import { log } from "./log"; import yaml from "yaml"; -import { DockgeSocket, ValidationError } from "./util-server"; +import { DockgeSocket, fileExists, ValidationError } from "./util-server"; import path from "path"; import { COMBINED_TERMINAL_COLS, @@ -99,6 +99,15 @@ export class Stack { // Check YAML format yaml.parse(this.composeYAML); + + let lines = this.composeENV.split("\n"); + + // Check if the .env is able to pass docker-compose + // Prevent "setenv: The parameter is incorrect" + // It only happens when there is one line and it doesn't contain "=" + if (lines.length === 1 && !lines[0].includes("=") && lines[0].length > 0) { + throw new ValidationError("Invalid .env format"); + } } get composeYAML() : string { @@ -146,29 +155,35 @@ export class Stack { * Save the stack to the disk * @param isAdd */ - save(isAdd : boolean) { + async save(isAdd : boolean) { this.validate(); let dir = this.path; // Check if the name is used if isAdd if (isAdd) { - if (fs.existsSync(dir)) { + if (await fileExists(dir)) { throw new ValidationError("Stack name already exists"); } // Create the stack folder - fs.mkdirSync(dir); + await fsAsync.mkdir(dir); } else { - if (!fs.existsSync(dir)) { + if (!await fileExists(dir)) { throw new ValidationError("Stack not found"); } } // Write or overwrite the compose.yaml - fs.writeFileSync(path.join(dir, this._composeFileName), this.composeYAML); + await fsAsync.writeFile(path.join(dir, this._composeFileName), this.composeYAML); + + const envPath = path.join(dir, ".env"); + // Write or overwrite the .env - fs.writeFileSync(path.join(dir, ".env"), this.composeENV); + // If .env is not existing and the composeENV is empty, we don't need to write it + if (await fileExists(envPath) || this.composeENV.trim() !== "") { + await fsAsync.writeFile(envPath, this.composeENV); + } } async deploy(socket? : DockgeSocket) : Promise { @@ -188,7 +203,7 @@ export class Stack { } // Remove the stack folder - fs.rmSync(this.path, { + await fsAsync.rm(this.path, { recursive: true, force: true }); @@ -218,12 +233,12 @@ export class Stack { stackList = new Map(); // Scan the stacks directory, and get the stack list - let filenameList = fs.readdirSync(stacksDir); + let filenameList = await fsAsync.readdir(stacksDir); for (let filename of filenameList) { try { // Check if it is a directory - let stat = fs.statSync(path.join(stacksDir, filename)); + let stat = await fsAsync.stat(path.join(stacksDir, filename)); if (!stat.isDirectory()) { continue; } @@ -314,7 +329,7 @@ export class Stack { let dir = path.join(server.stacksDir, stackName); if (!skipFSOperations) { - if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { + if (!await fileExists(dir) || !(await fsAsync.stat(dir)).isDirectory()) { // Maybe it is a stack managed by docker compose directly let stackList = await this.getStackList(server, true); let stack = stackList.get(stackName); diff --git a/backend/util-server.ts b/backend/util-server.ts index 04d34db..0277346 100644 --- a/backend/util-server.ts +++ b/backend/util-server.ts @@ -5,6 +5,7 @@ import { log } from "./log"; import { ERROR_TYPE_VALIDATION } from "./util-common"; import { R } from "redbean-node"; import { verifyPassword } from "./password-hash"; +import fs from "fs"; export interface JWTDecoded { username : string; @@ -82,3 +83,9 @@ export async function doubleCheckPassword(socket : DockgeSocket, currentPassword return user; } + +export function fileExists(file : string) { + return fs.promises.access(file, fs.constants.F_OK) + .then(() => true) + .catch(() => false); +} diff --git a/package.json b/package.json index 69c29f2..73b10ed 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "composerize": "~1.4.1", "croner": "~7.0.5", "dayjs": "~1.11.10", + "dotenv": "~16.3.1", "express": "~4.18.2", "express-static-gzip": "~2.1.7", "http-graceful-shutdown": "~3.1.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c25b72e..c11d267 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: dayjs: specifier: ~1.11.10 version: 1.11.10 + dotenv: + specifier: ~16.3.1 + version: 16.3.1 express: specifier: ~4.18.2 version: 4.18.2 @@ -2154,6 +2157,11 @@ packages: esutils: 2.0.3 dev: true + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: false