UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

454 lines (453 loc) 20.5 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. 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 IFolder_1 = require("../storage/IFolder"); const NodeFile_1 = __importDefault(require("./NodeFile")); const NodeStorage_1 = __importDefault(require("./NodeStorage")); const StorageUtilities_1 = __importStar(require("../storage/StorageUtilities")); const FolderBase_1 = __importDefault(require("../storage/FolderBase")); const Log_1 = __importDefault(require("./../core/Log")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); class NodeFolder extends FolderBase_1.default { _name; _path; folders; files; _storage; _parentFolder; get storage() { return this._storage; } get parentFolder() { return this._parentFolder; } get name() { return this._name; } get fullPath() { let path = this._path; if (!path.endsWith(NodeStorage_1.default.platformFolderDelimiter)) { path += NodeStorage_1.default.platformFolderDelimiter; } return path + this.name; } constructor(storage, parentFolder, parentPath, folderName) { super(); this._storage = storage; this._parentFolder = parentFolder; this._path = parentPath; this._name = folderName; this.folders = {}; this.files = {}; } async scanForChanges() { // No-op for node storage } ensureFile(name) { Log_1.default.assert(name.indexOf("/") < 0, "Unexpected to find / in file name: " + name); const nameCanon = StorageUtilities_1.default.canonicalizeName(name); let candFile = this.files[nameCanon]; if (candFile == null) { candFile = new NodeFile_1.default(this, name); this.files[nameCanon] = candFile; } return candFile; } _removeFile(file) { const nameCanon = StorageUtilities_1.default.canonicalizeName(file.name); const candFile = this.files[nameCanon]; Log_1.default.assert(candFile === file, "Files don't match."); this.files[nameCanon] = undefined; this.storage.notifyFileRemoved(this.storageRelativePath + file.name); } _addExistingFile(file) { const nameCanon = StorageUtilities_1.default.canonicalizeName(file.name); this.files[nameCanon] = file; } async deleteThisFolder() { if (this.storage.readOnly) { throw new Error("Deletion of this folder " + this.fullPath + " is not supported in read only mode."); } let absPath = path.resolve(this.fullPath); if (StorageUtilities_1.default.isPathRiskyForDelete(absPath)) { throw new Error("Deletion of this folder " + absPath + " is not supported because it seems too basic."); } let isSuccess = true; if (fs.existsSync(this.fullPath)) { try { fs.rmdirSync(this.fullPath, { recursive: true, }); } catch (e) { isSuccess = false; } } this.removeMeFromParent(); return isSuccess; } async deleteAllFolderContents() { if (this.storage.readOnly) { throw new Error("Deletion of folder contents at " + this.fullPath + " is not supported in read only mode."); } return await this.recursiveDeleteContentsOfThisFolder(); } ensureFolder(name) { Log_1.default.assert(name.indexOf("/") < 0, "Unexpected to find / in folder name: " + name); const nameCanon = StorageUtilities_1.default.canonicalizeName(name); let candFolder = this.folders[nameCanon]; if (!candFolder) { candFolder = new NodeFolder(this._storage, this, this.fullPath, name); this.folders[nameCanon] = candFolder; } return candFolder; } _addExistingFolderToParent(folder) { const nameCanon = StorageUtilities_1.default.canonicalizeName(folder.name); this.folders[nameCanon] = folder; } async moveTo(newStorageRelativePath) { const oldFullPath = this.fullPath; const newFolderPath = StorageUtilities_1.default.getFolderPath(newStorageRelativePath); const newFolderName = StorageUtilities_1.default.getLeafName(newStorageRelativePath); if (newFolderName.length < 2) { throw new Error("New path is not correct."); } if (this.isSameFolder(newStorageRelativePath)) { return false; } if (this._parentFolder !== null) { const newParentFolder = await this._parentFolder.storage.ensureFolderFromStorageRelativePath(newFolderPath); if (newParentFolder.folders[newFolderName] !== undefined) { throw new Error("Could not move folder; folder exists at specified path: " + newStorageRelativePath); } // Compute new full path from current _path before any in-memory changes. // _path holds the parent directory, so _path + newFolderName = target location. let parentPath = this._path; if (!parentPath.endsWith(NodeStorage_1.default.platformFolderDelimiter)) { parentPath += NodeStorage_1.default.platformFolderDelimiter; } const newFullPath = parentPath + newFolderName; // Perform disk rename FIRST. If this throws (e.g., EPERM from OneDrive/antivirus), // in-memory state is still clean and callers can fall back to copy+delete. Log_1.default.verbose("Renaming folder from: " + oldFullPath + " to " + newFullPath); fs.renameSync(oldFullPath, newFullPath); // Disk rename succeeded — now update in-memory state to match. this._parentFolder._removeExistingFolderFromParent(this); this._parentFolder = newParentFolder; this._name = newFolderName; newParentFolder._addExistingFolderToParent(this); } this._name = newFolderName; return true; } async exists() { return fs.existsSync(this.fullPath); } async ensureExists() { const exists = fs.existsSync(this.fullPath); if (!exists) { // Log.message("Creating folder '" + this.fullPath + "'"); fs.mkdirSync(this.fullPath, { recursive: true }); } return true; } async generateFileListings(listings) { await this.load(true); if (!listings) { listings = {}; } if (this.files["files.json"] !== undefined) { const file = this.files["files.json"]; if (!file.isContentLoaded) { await file.loadContent(false); } const obj = StorageUtilities_1.default.getJsonObject(file); if (obj && obj.files) { for (const fileInfo of obj.files) { if (fileInfo.hash && fileInfo.size && fileInfo.path && fileInfo.sourcePath === undefined) { const pathHash = this.generatePathHash(fileInfo); if (!listings[pathHash]) { const relativePath = this.storageRelativePath; if (relativePath) { listings[pathHash] = { path: fileInfo.path, size: fileInfo.size, hash: fileInfo.hash, sourcePath: StorageUtilities_1.default.canonicalizePath(relativePath + fileInfo.path), }; } } } } } } else { for (const dirName in this.folders) { const folder = this.folders[dirName]; if (folder && !folder.errorStatus) { await folder.generateFileListings(listings); } } } return listings; } generatePathHash(fileInfo) { let pathHash = StorageUtilities_1.default.getBaseFromName(fileInfo.path).toLowerCase(); if (fileInfo.size) { pathHash += "|" + fileInfo.size; } if (fileInfo.hash) { pathHash += "|" + fileInfo.hash; } return pathHash; } async copyContentsTo(destRootPath, fileInclusionList, addFilesToInclusionList, listings, destStorageRelativePath, copyPath) { await this.load(true); destRootPath = NodeStorage_1.default.ensureEndsWithDelimiter(destRootPath); let dirPath = NodeStorage_1.default.ensureEndsWithDelimiter(destRootPath); if (copyPath) { dirPath += copyPath; } const targetFolderExists = fs.existsSync(dirPath); if (!targetFolderExists) { fs.mkdirSync(dirPath, { recursive: true }); } for (const fileName in this.files) { const file = this.files[fileName]; let filePath = fileName; if (copyPath) { filePath = copyPath + filePath; } filePath = StorageUtilities_1.default.canonicalizePath(filePath); let targetFileSize; if (fileInclusionList) { for (const filePathAndSize of fileInclusionList) { if (StorageUtilities_1.default.canonicalizePath(filePathAndSize.path) === filePath) { targetFileSize = filePathAndSize; } } } if (file) { await file.loadContent(true); if (file.content) { const encoding = StorageUtilities_1.default.getEncodingByFileName(file.name); if (!targetFileSize) { targetFileSize = { size: file.content.length, path: filePath, }; if (addFilesToInclusionList === true) { fileInclusionList.push(targetFileSize); } } if (encoding === StorageUtilities_1.EncodingType.ByteBuffer) { let arrData = file.content; if (targetFileSize && arrData instanceof Buffer && targetFileSize.size && arrData.length > targetFileSize.size) { Log_1.default.verbose("Making truncated buffer copy of " + file.fullPath + " to size " + targetFileSize.size); arrData = arrData.subarray(0, targetFileSize.size); } else if (targetFileSize && arrData instanceof Uint8Array && targetFileSize.size && arrData.length > targetFileSize.size) { Log_1.default.verbose("Making truncated array copy of " + file.fullPath + " to size " + targetFileSize.size); arrData = arrData.subarray(0, targetFileSize.size); } const hash = crypto.createHash("MD5"); hash.update(arrData); targetFileSize.hash = hash.digest("base64"); const pathHash = this.generatePathHash(targetFileSize); let usingSourcePath = false; if (listings) { const fileListing = listings[pathHash]; if (fileListing && fileListing.sourcePath) { targetFileSize.sourcePath = fileListing.sourcePath; usingSourcePath = true; } } if (!usingSourcePath) { fs.writeFileSync(dirPath + file.name, arrData); if (listings) { listings[pathHash] = { size: targetFileSize.size, hash: targetFileSize.hash, path: targetFileSize.path, sourcePath: StorageUtilities_1.default.canonicalizePath(destStorageRelativePath + targetFileSize.path), }; } } } else { const hash = crypto.createHash("MD5"); hash.update(file.content); targetFileSize.hash = hash.digest("base64"); const pathHash = this.generatePathHash(targetFileSize); let usingSourcePath = false; if (listings) { const fileListing = listings[pathHash]; if (fileListing && fileListing.sourcePath) { targetFileSize.sourcePath = fileListing.sourcePath; usingSourcePath = true; } } if (!usingSourcePath) { fs.writeFileSync(dirPath + file.name, file.content, { encoding: "utf8" }); if (listings) { listings[pathHash] = { size: targetFileSize.size, hash: targetFileSize.hash, path: targetFileSize.path, sourcePath: StorageUtilities_1.default.canonicalizePath(destStorageRelativePath + targetFileSize.path), }; } } } } } } if (copyPath === undefined) { copyPath = ""; } for (const folderName in this.folders) { const nf = this.folders[folderName]; if (nf && !nf.errorStatus) { await nf.copyContentsTo(destRootPath, fileInclusionList, addFilesToInclusionList, listings, destStorageRelativePath, copyPath + folderName + NodeStorage_1.default.platformFolderDelimiter); } } } async saveFilesList(pathDescriptor, inclusionList) { const obj = { path: pathDescriptor, files: inclusionList }; fs.writeFileSync(NodeStorage_1.default.ensureEndsWithDelimiter(this.fullPath) + "files.json", JSON.stringify(obj, null, 2), { encoding: "utf8", }); } async copyContentsOut(destFolder) { await this.load(true); if (this.files["files.json"] !== undefined) { const file = this.files["files.json"]; if (!file.isContentLoaded) { await file.loadContent(false); } const obj = StorageUtilities_1.default.getJsonObject(file); if (obj && obj.files) { for (const fileInfo of obj.files) { if (fileInfo.hash && fileInfo.size && fileInfo.path) { let file; if (fileInfo.sourcePath) { file = await this.storage.rootFolder.getFileFromRelativePath(StorageUtilities_1.default.ensureStartsWithDelimiter(fileInfo.sourcePath)); } else { file = await this.getFileFromRelativePath(StorageUtilities_1.default.ensureStartsWithDelimiter(fileInfo.path)); } if (file) { if (!file.isContentLoaded) { await file.loadContent(); } if (file.content !== null) { const targetFile = await destFolder.ensureFileFromRelativePath(StorageUtilities_1.default.ensureStartsWithDelimiter(fileInfo.path)); if (targetFile) { // Log.verbose("Copying file '" + file.fullPath + "' out to '" + targetFile.fullPath + "'"); targetFile.setContent(file.content); } } } else { Log_1.default.debug("Could not find expected backup file '" + fileInfo.path + "' in " + this.fullPath); } } } } } else { Log_1.default.debug("Could not find files.json in " + this.fullPath); } await destFolder.saveAll(); } async createFile(name) { return this.ensureFile(name); } loadSync(force) { if (this.lastLoadedOrSaved != null && !force) { return this.lastLoadedOrSaved; } // Log.debug("Reading details on folder '" + this.fullPath + "'"); if (fs.existsSync(this.fullPath)) { const results = fs.readdirSync(this.fullPath); results.forEach((fileOrFolderName) => { let filePath = this.fullPath; if (!filePath.endsWith(NodeStorage_1.default.platformFolderDelimiter)) { filePath += NodeStorage_1.default.platformFolderDelimiter; } filePath += fileOrFolderName; try { const stat = fs.statSync(filePath); if (stat.isDirectory() && !StorageUtilities_1.default.isIgnorableFolder(fileOrFolderName)) { this.ensureFolder(fileOrFolderName); } else if (stat.isFile() && StorageUtilities_1.default.isUsableFile(filePath)) { const file = this.ensureFile(fileOrFolderName); if (stat.mtime) { file.modifiedAtLoad = new Date(stat.mtime); } } } catch (e) { this.errorStatus = IFolder_1.FolderErrorStatus.unreadable; Log_1.default.error("Error opening folder '" + filePath + "': " + e.toString()); } }); } this.updateLastLoadedOrSaved(); return this.lastLoadedOrSaved; } async load(force) { if (this.lastLoadedOrSaved != null && !force) { return this.lastLoadedOrSaved; } return this.loadSync(force); } } exports.default = NodeFolder;