diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d26b987..93ad99d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,23 +14,24 @@ "source=bdscore_dind,target=/var/lib/docker,type=volume" ], "extensions": [ - "GitHub.copilot-nightly", - "GitHub.copilot-labs", - "benshabatnoam.google-translate-ext", - "eamodio.gitlens", - "github.vscode-pull-request-github", - "visualstudioexptteam.vscodeintellicode", - "redhat.vscode-yaml", - "ms-vscode-remote.remote-containers", - "wix.vscode-import-cost", - "eg2.vscode-npm-script", - "christian-kohler.npm-intellisense", - "christian-kohler.path-intellisense", - "aaron-bond.better-comments", - "vscode-icons-team.vscode-icons", - "me-dutour-mathieu.vscode-github-actions", - "cschleiden.vscode-github-actions", - "oderwat.indent-rainbow", - "ms-azuretools.vscode-docker" - ] + "GitHub.copilot-nightly", + "GitHub.copilot-labs", + "benshabatnoam.google-translate-ext", + "eamodio.gitlens", + "github.vscode-pull-request-github", + "visualstudioexptteam.vscodeintellicode", + "redhat.vscode-yaml", + "ms-vscode-remote.remote-containers", + "wix.vscode-import-cost", + "eg2.vscode-npm-script", + "christian-kohler.npm-intellisense", + "christian-kohler.path-intellisense", + "aaron-bond.better-comments", + "vscode-icons-team.vscode-icons", + "me-dutour-mathieu.vscode-github-actions", + "cschleiden.vscode-github-actions", + "oderwat.indent-rainbow", + "ms-azuretools.vscode-docker", + "formulahendry.code-runner" +] } diff --git a/.gitignore b/.gitignore index 5eb12a1..0866fe3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ dist/ src/**/*.d.ts src/**/*.d.js +backup_*.zip \ No newline at end of file diff --git a/.npmignore b/.npmignore index 05a9d0c..f21dac1 100644 --- a/.npmignore +++ b/.npmignore @@ -1 +1,2 @@ -!dist/ \ No newline at end of file +!dist/ +backup_*.zip \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index db2dde1..4966300 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ - "github.copilot" + "github.copilot", + "formulahendry.code-runner" ] } \ No newline at end of file diff --git a/src/backup.ts b/src/backup.ts index d1c2616..005e34a 100644 --- a/src/backup.ts +++ b/src/backup.ts @@ -9,33 +9,120 @@ import simpleGit from "simple-git"; const ServerPathRoot = path.resolve(process.env.SERVER_PATH||path.join(os.homedir(), "bds_core/servers")); const backupFolderPath = path.resolve(process.env.BACKUP_PATH||path.join(os.homedir(), "bds_core/backups")); -export default CreateBackup; -export async function CreateBackup(WriteFile: {path: string}|true|false = false) { - if (!(fs.existsSync(backupFolderPath))) await fsPromise.mkdir(backupFolderPath, {recursive: true}); +async function createTempFolder() { + let cleaned = false; + const tempFolderPath = path.join(os.tmpdir(), Buffer.from(Math.random().toString()).toString("hex")+"tmpFolder"); + if (fs.existsSync(tempFolderPath)) await fse.rm(tempFolderPath, {recursive: true}); + await fsPromise.mkdir(tempFolderPath, { recursive: true }); + + /** + * Add file to temp Folder + * + * @param filePath - Original file path + * @param onStorage - on Storage temp file path, example: serverName/fileName + * @returns + */ + const addFile = async (filePath: string, onStorage?: string) => { + if (cleaned) throw new Error("Cannot add file after cleaning"); + if (onStorage === undefined) onStorage = path.parse(filePath).name; + const onTempStorage = path.join(tempFolderPath, onStorage); + const basenameFolder = path.parse(onTempStorage).dir; + await fsPromise.mkdir(basenameFolder, { recursive: true }).catch(() => undefined); + await fsPromise.copyFile(filePath, onTempStorage); + return; + } + + /** + * Add folder to temp Folder (include subfolders) + * + * @param folderPath - Original folder path + * @param onStorage - on Storage temp folder path, example: serverName/folderName + * @returns + */ + const addFolder = async (folderPath: string, onStorage: string = path.basename(folderPath)) => { + if (cleaned) throw new Error("Cannot add folder after cleaning"); + if (!(fs.existsSync(folderPath) && fs.lstatSync(folderPath).isDirectory())) throw new Error(`${folderPath} is not a folder`); + await fse.copy(folderPath, path.join(tempFolderPath, onStorage), {recursive: true}); + return; + } + + /** + * Get only files from temp folder recursively + * + * @returns list files + */ + const listFiles = async () => { + if (cleaned) throw new Error("Cannot list files after cleaning"); + const listFolder = async (folderPath: string) => { + const folderFiles = (await fsPromise.readdir(folderPath)).filter(file => !(file === ".git")).map(file => path.join(folderPath, file)); + for (const file of folderFiles) { + const FileStats = await fsPromise.lstat(file).catch(() => null); + if (FileStats === null) {} + else if (FileStats.isDirectory()) folderFiles.push(...(await listFolder(file))); + else if (FileStats.isSymbolicLink()){}; + } + return folderFiles; + } + const FilesMaped = (await listFolder(tempFolderPath)).filter(a => !(fs.lstatSync(a).isDirectory())).map(PathF => PathF.replace(tempFolderPath+path.sep, "")); + return FilesMaped; + } + + /** + * Remove temp folder and lock to add new files and folders + * + * @returns + */ + const cleanFolder = async () => { + if (cleaned) throw new Error("Cannot clean folder after cleaning"); + await fse.rm(tempFolderPath, {recursive: true, force: true}); + cleaned = true; + return; + } + return { + tempFolderPath, + addFile, + addFolder, + listFiles, + cleanFolder + }; +} + +async function genericAddFiles() { // Create empty zip Buffer - const BackupZip = new AdmZip(); + const TempFolder = await createTempFolder() // List all Servers for (const __Server_Path of fs.readdirSync(ServerPathRoot).filter(Server => !!bdsCoretypes.PlatformArray.find(Platform => Platform === Server))) { const Platform = __Server_Path as bdsCoretypes.Platform; const ServerPath = path.join(ServerPathRoot, __Server_Path); if (fs.existsSync(ServerPath)) { - if (fs.existsSync(path.join(ServerPath, "worlds"))) BackupZip.addLocalFolder(await fsPromise.realpath(path.join(ServerPath, "worlds")), Platform+"/worlds"); - if (fs.existsSync(path.join(ServerPath, "server.properties"))) BackupZip.addLocalFile(await fsPromise.realpath(path.join(ServerPath, "server.properties")), Platform+"/server.properties"); - if (fs.existsSync(path.join(ServerPath, "permissions.json"))) BackupZip.addLocalFile(path.join(ServerPath, "permissions.json"), Platform+"/permissions.json"); + if (fs.existsSync(path.join(ServerPath, "worlds"))) await TempFolder.addFolder(await fsPromise.realpath(path.join(ServerPath, "worlds")), Platform+"/worlds"); + if (fs.existsSync(path.join(ServerPath, "server.properties"))) await TempFolder.addFile(await fsPromise.realpath(path.join(ServerPath, "server.properties")), Platform+"/server.properties"); + if (fs.existsSync(path.join(ServerPath, "permissions.json"))) await TempFolder.addFile(path.join(ServerPath, "permissions.json"), Platform+"/permissions.json"); if (Platform === "java") { - if (fs.existsSync(path.join(ServerPath, "banned-ips.json"))) BackupZip.addLocalFile(path.join(ServerPath, "banned-ips.json"), Platform+"/banned-ips.json"); - if (fs.existsSync(path.join(ServerPath, "banned-players.json"))) BackupZip.addLocalFile(path.join(ServerPath, "banned-players.json"), Platform+"/banned-players.json"); - if (fs.existsSync(path.join(ServerPath, "whitelist.json"))) BackupZip.addLocalFile(path.join(ServerPath, "whitelist.json"), Platform+"/whitelist.json"); + if (fs.existsSync(path.join(ServerPath, "banned-ips.json"))) await TempFolder.addFile(path.join(ServerPath, "banned-ips.json"), Platform+"/banned-ips.json"); + if (fs.existsSync(path.join(ServerPath, "banned-players.json"))) await TempFolder.addFile(path.join(ServerPath, "banned-players.json"), Platform+"/banned-players.json"); + if (fs.existsSync(path.join(ServerPath, "whitelist.json"))) await TempFolder.addFile(path.join(ServerPath, "whitelist.json"), Platform+"/whitelist.json"); // Filter folders const Folders = fs.readdirSync(ServerPath).filter(Folder => fs.lstatSync(path.join(ServerPath, Folder)).isDirectory()).filter(a => !(a === "libraries"||a === "logs"||a === "versions")); - for (const world of Folders) BackupZip.addLocalFolder(path.join(ServerPath, world), Platform+"/"+world); + for (const world of Folders) await TempFolder.addFolder(path.join(ServerPath, world), Platform+"/"+world); } } } + return TempFolder; +} +export default CreateBackup; +export async function CreateBackup(WriteFile: {path: string}|true|false = false) { + if (!(fs.existsSync(backupFolderPath))) await fsPromise.mkdir(backupFolderPath, {recursive: true}); + // Add Folders and files + const TempFolder = await genericAddFiles() + // Create empty zip Buffer + const zip = new AdmZip(); + for (const file of await TempFolder.listFiles()) zip.addLocalFile(path.join(TempFolder.tempFolderPath, file), (path.sep+path.parse(file).dir)); + await TempFolder.cleanFolder(); // Get Zip Buffer - const zipBuffer = BackupZip.toBuffer(); + const zipBuffer = zip.toBuffer(); if (typeof WriteFile === "object") { let BackupFile = path.resolve(backupFolderPath, `${new Date().toString().replace(/[-\(\)\:\s+]/gi, "_")}.zip`); if (!!WriteFile.path) BackupFile = path.resolve(WriteFile.path); @@ -117,4 +204,4 @@ export async function gitBackup(Platform: bdsCoretypes.Platform, options?: gitBa if (commit) await gitLocal.commit(`${Platform} backup - ${(new Date()).toISOString()}`); if (!!options&&!!options.pushCommits) await gitLocal.push(); return; -} \ No newline at end of file +} diff --git a/src/bin/index.ts b/src/bin/index.ts index 4a4c62d..55ffa45 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -5,20 +5,22 @@ import { isValidCron } from "cron-validator"; import * as BdsCore from "../index"; import * as bdsTypes from "../globalType"; import cli_color from "cli-color"; +import path from "path"; +import { promises as fsPromise } from "fs"; -const Yargs = yargs(process.argv.slice(2)).option("platform", { - alias: "p", - describe: "Bds Core Platform", - demandOption: true, - type: "string", - choices: ["bedrock", "java", "pocketmine", "spigot", "dragonfly"], - default: "bedrock" -}).command("download", "Download and Install server", yargs => { +const Yargs = yargs(process.argv.slice(2)).command("download", "Download and Install server", yargs => { const options = yargs.option("version", { alias: "v", describe: "Server Version", demandOption: true, type: "string" + }).option("platform", { + alias: "p", + describe: "Bds Core Platform", + demandOption: true, + type: "string", + choices: ["bedrock", "java", "pocketmine", "spigot", "dragonfly"], + default: "bedrock" }).parseSync(); const Platform = options.platform as bdsTypes.Platform; console.log("Starting Download..."); @@ -26,8 +28,25 @@ const Yargs = yargs(process.argv.slice(2)).option("platform", { console.log("Sucess to download server"); console.info("Release date: %s", `${res.Date.getDate()}/${res.Date.getMonth()+1}/${res.Date.getFullYear()}`); }); +}).command("backup", "Create Backups", async yargs => { + const {storage} = yargs.option("storage", { + alias: "s", + describe: "Storage Path", + demandOption: false, + type: "string", + default: path.join(process.cwd(), "backup_"+new Date().toString().replace(/[-\(\)\:\s+]/gi, "_"))+".zip" + }).parseSync(); + const zipBuffer = await BdsCore.Backup.CreateBackup(false); + await fsPromise.writeFile(storage, zipBuffer); }).command("start", "Start Server", async yargs => { - const options = await yargs.option("cronBackup", { + const options = await yargs.option("platform", { + alias: "p", + describe: "Bds Core Platform", + demandOption: true, + type: "string", + choices: ["bedrock", "java", "pocketmine", "spigot", "dragonfly"], + default: "bedrock" + }).option("cronBackup", { alias: "b", describe: "cron job to backup server maps", type: "string"