UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

423 lines (422 loc) 18.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(require("fs")); const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities")); /** Debounce time in ms for batching file change notifications */ const FILE_CHANGE_DEBOUNCE_MS = 300; class FileSystemCommandHandler { _window; _ipcMain; _env; _utils; _watchingPaths = {}; /** Pending file changes for debouncing, keyed by storage path */ _pendingFileChanges = {}; constructor(browserWindow, incomingIpcMain, creatorTools, env, utils) { this._window = browserWindow; this._ipcMain = incomingIpcMain; this._env = env; this._utils = utils; this.exists = this.exists.bind(this); this.folderExists = this.folderExists.bind(this); this.mkdir = this.mkdir.bind(this); this.readDir = this.readDir.bind(this); this.renameFolder = this.renameFolder.bind(this); this.deleteFolder = this.deleteFolder.bind(this); this.readFile = this.readFile.bind(this); this.writeFile = this.writeFile.bind(this); this.writeUtf8File = this.writeUtf8File.bind(this); this.readUtf8File = this.readUtf8File.bind(this); this.stat = this.stat.bind(this); this.getDirname = this.getDirname.bind(this); this.rootStorageExists = this.rootStorageExists.bind(this); this._ipcMain.handle("asyncgetDirname", this.getDirname); this._ipcMain.handle("asyncfsExists", this.exists); this._ipcMain.handle("asyncfsFolderExists", this.folderExists); this._ipcMain.handle("asyncfsRenameFolder", this.renameFolder); this._ipcMain.handle("asyncfsDeleteFolder", this.deleteFolder); this._ipcMain.handle("asyncfsRootStorageExists", this.rootStorageExists); this._ipcMain.handle("asyncfsMkdir", this.mkdir); this._ipcMain.handle("asyncfsReaddir", this.readDir); this._ipcMain.handle("asyncfsWriteFile", this.writeFile); this._ipcMain.handle("asyncfsWriteUtf8File", this.writeUtf8File); this._ipcMain.handle("bsyncfsReadFile", this.readFile); this._ipcMain.handle("asyncfsReadUtf8File", this.readUtf8File); this._ipcMain.handle("asyncfsStat", this.stat); } exists(evt, data) { const slargs = data.split("|"); const path = this._utils.deTokenizePath(slargs[1]); if (path === undefined) { throw new Error("EXT: Could not process path: " + slargs[1]); } this._utils.validateFilePath(path); this._window.webContents.send("appsvc", "asyncfsExists|" + slargs[0] + "|" + fs.existsSync(path)); } rootStorageExists(evt, data) { const slargs = data.split("|"); let storagePath = slargs[1]; if (!storagePath) { this._window.webContents.send("appsvc", "asyncfsRootStorageExists|" + slargs[0] + "|false"); return; } let result = false; if (storagePath.startsWith("<BDRK>")) { storagePath = this._utils.getMinecraftReleasePath(); if (storagePath) { result = true; } } else if (storagePath.startsWith("<BDPV>")) { storagePath = this._utils.getMinecraftPreviewPath(); if (storagePath) { result = true; } } else if (storagePath.startsWith("<pt_") || storagePath.startsWith("<DOCP>")) { storagePath = this._utils.deTokenizePath(storagePath); if (storagePath) { result = true; } } if (!storagePath) { this._window.webContents.send("appsvc", "asyncfsRootStorageExists|" + slargs[0] + "|false"); return; } if (result === true) { if (fs.existsSync(storagePath) === false) { result = false; } else { if (!this._watchingPaths[storagePath]) { // Capture storagePath in a const for use in callbacks (TypeScript flow analysis) const watchedPath = storagePath; // Initialize pending changes structure for this storage path this._pendingFileChanges[watchedPath] = { added: new Set(), removed: new Set(), updated: new Set(), timer: null, }; let listener = fs.watch(watchedPath, { recursive: true }, (eventType, fileName) => { if (!fileName) { return; } const mappedPath = this._utils.ensureMappingForPath(watchedPath); const updatePath = StorageUtilities_1.default.ensureEndsWithDelimiter("<pt_" + mappedPath + ">") + fileName.toString(); const fullFilePath = watchedPath + "/" + fileName.toString(); // Get the pending changes for this storage const pending = this._pendingFileChanges[watchedPath]; if (!pending) { return; } if (eventType === "change") { // File contents changed - only if it wasn't just added if (!pending.added.has(updatePath)) { pending.updated.add(updatePath); } } else if (eventType === "rename") { // On Windows, "rename" is fired for both adds and deletes // Check if file exists to disambiguate try { if (fs.existsSync(fullFilePath)) { // File exists - it was added or renamed to this name // If it was in removed, move it to updated (quick delete+recreate) if (pending.removed.has(updatePath)) { pending.removed.delete(updatePath); pending.updated.add(updatePath); } else { pending.added.add(updatePath); } } else { // File doesn't exist - it was removed or renamed away // If it was in added, just remove from added (never notify) if (pending.added.has(updatePath)) { pending.added.delete(updatePath); } else { // Mark for removal pending.updated.delete(updatePath); pending.removed.add(updatePath); } } } catch (e) { // If we can't check existence, assume it was removed pending.removed.add(updatePath); } } // Reset the debounce timer if (pending.timer) { clearTimeout(pending.timer); } pending.timer = setTimeout(() => { this._flushPendingChanges(watchedPath); }, FILE_CHANGE_DEBOUNCE_MS); }); this._watchingPaths[watchedPath] = listener; } } } this._window.webContents.send("appsvc", "asyncfsRootStorageExists|" + slargs[0] + "|" + result.toString()); } /** * Flushes pending file changes for a storage path, sending batched IPC messages. * This is called after the debounce timer expires. */ _flushPendingChanges(storagePath) { const pending = this._pendingFileChanges[storagePath]; if (!pending) { return; } // Clear the timer reference pending.timer = null; try { // Send removed files first for (const path of pending.removed) { this._window.webContents.send("appsvc", "localFileRemoved|" + path); } // Send added files for (const path of pending.added) { this._window.webContents.send("appsvc", "localFileAdded|" + path); } // Send updated files for (const path of pending.updated) { this._window.webContents.send("appsvc", "localFileUpdate|" + path); } } catch (e) { // Window may have been closed } // Clear the sets pending.added.clear(); pending.removed.clear(); pending.updated.clear(); } folderExists(evt, data) { const slargs = data.split("|"); if (slargs[1].startsWith("<BDRK>")) { let mcpath = this._utils.getMinecraftReleasePath(); if (!mcpath) { this._window.webContents.send("appsvc", "asyncfsFolderExists|" + slargs[0] + "|false"); } } if (slargs[1].startsWith("<BDPV>")) { let BDPVath = this._utils.getMinecraftPreviewPath(); if (!BDPVath) { this._window.webContents.send("appsvc", "asyncfsFolderExists|" + slargs[0] + "|false"); } } const path = this._utils.deTokenizePath(slargs[1]); if (path === undefined) { throw new Error("FEX: Could not process path: " + slargs[1]); } const folderExistsResult = fs.existsSync(path); let result = false; if (folderExistsResult) { const statResult = fs.statSync(path); if (statResult.isDirectory()) { result = true; } } try { this._window.webContents.send("appsvc", "asyncfsFolderExists|" + slargs[0] + "|" + result.toString()); } catch (e) { } } getDirname(evt, data) { const slargs = data.split("|"); this._window.webContents.send("appsvc", "asyncgetDirname|" + slargs[0] + "|" + __dirname); } renameFolder(evt, data) { const slargs = data.split("|"); const fromPath = this._utils.deTokenizePath(slargs[1]); if (fromPath === undefined) { throw new Error("RNDA: Could not process path: " + slargs[1]); } const toPath = this._utils.deTokenizePath(slargs[2]); if (toPath === undefined) { throw new Error("RNDB: Could not process path: " + slargs[2]); } this._utils.validateFolderPath(fromPath); this._utils.validateFolderPath(toPath); let result = true; try { fs.renameSync(fromPath, toPath); } catch (e) { result = false; } this._window.webContents.send("appsvc", "asyncfsRenameFolder|" + slargs[0] + "|" + result.toString()); } deleteFolder(evt, data) { const slargs = data.split("|"); const path = this._utils.deTokenizePath(slargs[1]); if (path === undefined) { throw new Error("DLF: Could not process path: " + slargs[1]); } this._utils.validateFolderPath(path); let result = true; try { fs.rmSync(path, { recursive: true, force: true }); } catch (e) { result = false; } this._window.webContents.send("appsvc", "asyncfsDeleteFolder|" + slargs[0] + "|" + result.toString()); } mkdir(evt, data) { const slargs = data.split("|"); const path = this._utils.deTokenizePath(slargs[1]); if (path === undefined) { throw new Error("MKD: Could not process path: " + slargs[1]); } this._utils.validateFolderPath(path); // Use recursive: true to create parent directories and avoid EEXIST errors try { fs.mkdirSync(path, { recursive: true }); } catch (e) { // Only throw if it's not an EEXIST error (directory already exists is OK) if (e.code !== "EEXIST") { throw e; } } this._window.webContents.send("appsvc", "asyncfsMkdir|" + slargs[0] + "|true"); } readDir(evt, data) { const slargs = data.split("|"); const path = this._utils.deTokenizePath(slargs[1]); if (path === undefined) { throw new Error("RDD: Could not process path: " + slargs[1]); } this._utils.validateFolderPath(path); let fsReadDirResult = undefined; try { fsReadDirResult = fs.readdirSync(path); } catch (e) { } const res = fsReadDirResult ? JSON.stringify(fsReadDirResult) : "<undefined>"; this._window.webContents.send("appsvc", "asyncfsReadDir|" + slargs[0] + "|" + res); } writeFile(evt, data) { const slargs = data.split("|"); const writeFilePath = this._utils.deTokenizePath(slargs[1]); if (writeFilePath === undefined) { throw new Error("WFI: Could not process path: " + slargs[1]); } let writeFileContent = slargs[2]; this._utils.validateFilePath(writeFilePath); // Ensure parent directory exists before writing const dirPath = StorageUtilities_1.default.getFolderPath(writeFilePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } const dvWriteFileContent = new DataView(this._utils.base64ToArrayBuffer(writeFileContent)); fs.writeFileSync(writeFilePath, dvWriteFileContent); this._window.webContents.send("appsvc", "asyncfsWriteFile|" + slargs[0] + "|"); } readFile(evt, data) { const slargs = data.split("|"); const path = this._utils.deTokenizePath(slargs[1]); if (path === undefined) { throw new Error("RFI: Could not process path: " + slargs[1]); } this._utils.validateFilePath(path); if (!fs.existsSync(path)) { this._window.webContents.send("appsvc", "bsyncfsReadFile|" + slargs[0] + "|<undefined>"); return; } const readFileResult = fs.readFileSync(path); this._window.webContents.send("appsvc", "bsyncfsReadFile|" + slargs[0] + "|" + this._utils.arrayBufferToBase64(readFileResult)); } readUtf8File(evt, data) { const slargs = data.split("|"); const path = this._utils.deTokenizePath(slargs[1]); if (path === undefined) { throw new Error("RUF: Could not process path: " + slargs[1]); } this._utils.validateFilePath(path); if (!fs.existsSync(path)) { this._window.webContents.send("appsvc", "asyncfsReadUtf8File|" + slargs[0] + "|<undefined>"); return; } const fileContents = fs.readFileSync(path, { encoding: "utf8" }); this._window.webContents.send("appsvc", "asyncfsReadUtf8File|" + slargs[0] + "|" + fileContents); } stat(evt, data) { const slargs = data.split("|"); const path = this._utils.deTokenizePath(slargs[1]); if (path === undefined) { throw new Error("STT: Could not process path: " + slargs[1]); } this._utils.validateFolderPath(path); const statResult = fs.statSync(path); const statResultData = { isDirectory: statResult.isDirectory(), isFile: statResult.isFile(), mtime: statResult.mtime, ctime: statResult.ctime, size: statResult.size, }; try { this._window.webContents.send("appsvc", "asyncfsStat|" + slargs[0] + "|" + JSON.stringify(statResultData)); } catch (e) { } } writeUtf8File(evt, data) { const slargs = data.split("|"); const path = this._utils.deTokenizePath(slargs[1]); if (path === undefined) { throw new Error("WUF: Could not process path: " + slargs[1]); } const content = slargs[2]; this._utils.validateFilePath(path); const dirPath = StorageUtilities_1.default.getFolderPath(path); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } fs.writeFileSync(path, content, { encoding: "utf8" }); this._window.webContents.send("appsvc", "asyncfsWriteUtf8File|" + slargs[0] + "|"); } } exports.default = FileSystemCommandHandler;