@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
1,158 lines (1,156 loc) • 46.6 kB
JavaScript
;
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.EncodingType = exports.AllowedExtensions = exports.MaxShareableContentStringLength = void 0;
const DifferenceSet_1 = require("./DifferenceSet");
const IFolderDifference_1 = require("./IFolderDifference");
const IFileDifference_1 = require("./IFileDifference");
const Utilities_1 = require("../core/Utilities");
const ZipStorage_1 = require("./ZipStorage");
const Log_1 = require("../core/Log");
const Storage_1 = require("./Storage");
const BasicValidators_1 = require("./BasicValidators");
exports.MaxShareableContentStringLength = 65536;
// part of security/reliability and defense in depth is to only allow our file functions to work with files from an allow list
// this list is also replicated in /public/preload.js
exports.AllowedExtensions = [
"js",
"ts",
"json",
"md",
"png",
"css",
"woff",
"ttf",
"woff2",
"jpg",
"gitignore",
"jpeg",
"gif",
"lang",
"fsb",
"map",
"yml",
"ico",
"ogg",
"nojekyll",
"mjs",
"env",
"wav",
"tga",
"old",
"mc",
"",
"zip",
"mcstructure",
"mcworld",
"mcproject",
"material",
"vertex",
"geometry",
"fragment",
"map",
"js.map",
"mctemplate",
"mcfunction",
"mcaddon",
"mcpack",
"html",
"dat",
"dat_old",
"txt",
"ldb",
"log",
];
const _minecraftProjectFolderNames = [
"behavior_packs",
"resource_packs",
"worlds",
"world",
"world_template",
"skin_pack",
"scripts",
"content",
"marketing art",
"store art",
"db",
"texts",
"animation_controllers",
"blocks",
"structures",
"entities",
"functions",
"items",
"dialogue",
"animations",
"entity",
"materials",
"models",
"textures",
"fogs",
"materials",
"particles",
"ui",
];
var EncodingType;
(function (EncodingType) {
EncodingType[EncodingType["ByteBuffer"] = 0] = "ByteBuffer";
EncodingType[EncodingType["Utf8String"] = 1] = "Utf8String";
})(EncodingType = exports.EncodingType || (exports.EncodingType = {}));
class StorageUtilities {
static isUsableFile(path) {
const extension = StorageUtilities.getTypeFromName(path);
return exports.AllowedExtensions.includes(extension);
}
static getEncodingByFileName(name) {
const fileType = this.getTypeFromName(name);
const nameLow = name.toLowerCase();
switch (fileType) {
case "mcstructure":
case "zip":
case "dat":
case "dat_old":
case "ldb":
case "ico":
case "tga":
case "ogg":
case "wav":
case "gif":
case "jpeg":
case "jpg":
case "png":
case "mp3":
case "fsb":
case "woff":
case "woff2":
case "ttf":
case "pdb":
case "exe":
case "nbt":
case "mcworld":
case "mcproject":
case "mctemplate":
case "mcpack":
case "mcaddon":
return EncodingType.ByteBuffer;
}
if (fileType === "" && nameLow.startsWith("manifest-")) {
return EncodingType.ByteBuffer;
}
if (nameLow.startsWith("0") && fileType === "log") {
return EncodingType.ByteBuffer;
}
return EncodingType.Utf8String;
}
static absolutize(path) {
if (!path.startsWith(StorageUtilities.standardFolderDelimiter)) {
path = StorageUtilities.standardFolderDelimiter + path;
}
return path;
}
static getUniqueChildFolderName(name, folder) {
let num = 1;
let nameCand = name;
let isUnique = false;
while (!isUnique) {
isUnique = true;
for (const childFolderName in folder.folders) {
if (StorageUtilities.canonicalizeName(childFolderName) === StorageUtilities.canonicalizeName(nameCand)) {
isUnique = false;
}
}
if (!isUnique) {
nameCand = name + " " + num;
num++;
}
}
return nameCand;
}
static ensureEndsDelimited(path) {
if (!path.endsWith(StorageUtilities.standardFolderDelimiter)) {
path = path + StorageUtilities.standardFolderDelimiter;
}
if (path.startsWith("." + StorageUtilities.standardFolderDelimiter)) {
path = path.substring(1);
}
else if (!path.startsWith(StorageUtilities.standardFolderDelimiter)) {
path = StorageUtilities.standardFolderDelimiter + path;
}
return path;
}
static ensureEndsWithDelimiter(path) {
if (!path.endsWith(StorageUtilities.standardFolderDelimiter)) {
path = path + StorageUtilities.standardFolderDelimiter;
}
return path;
}
static ensureStartsWithDelimiter(path) {
if (!path.startsWith(StorageUtilities.standardFolderDelimiter)) {
path = StorageUtilities.standardFolderDelimiter + path;
}
return path;
}
static ensureNotStartsWithDelimiter(path) {
if (path.startsWith(StorageUtilities.standardFolderDelimiter)) {
path = path.substring(1);
}
return path;
}
static joinPath(pathA, pathB) {
let fullPath = pathA;
if (!fullPath.endsWith(StorageUtilities.standardFolderDelimiter)) {
fullPath += StorageUtilities.standardFolderDelimiter;
}
if (pathB.startsWith("/")) {
fullPath += pathB.substring(1, pathB.length);
}
else {
fullPath += pathB;
}
return fullPath;
}
static getMimeType(file) {
switch (StorageUtilities.getTypeFromName(file.name)) {
case "js":
return "application/javascript";
case "ts":
return "application/typescript";
case "json":
return "application/json";
case "mcworld":
case "mctemplate":
case "mcproject":
case "mcaddon":
case "mcpack":
case "zip":
return "application/zip";
case "mcstucture":
return "application/octet-stream";
case "mcfunction":
case "material":
case "env":
case "lang":
return "text/plain";
case "wav":
return "audio/wav";
case "mp3":
return "audio/mp3";
case "ogg":
return "audio/ogg";
case "jpg":
case "jpeg":
return "image/jpg";
case "png":
return "image/png";
case "tiff":
case "tga":
return "image/tiff";
default:
return "application/octet-stream";
}
}
static getContentAsString(file) {
if (typeof file.content === "string") {
return file.content;
}
else if (file.content instanceof Uint8Array) {
return "data:" + StorageUtilities.getMimeType(file) + ";base64," + Utilities_1.default.uint8ArrayToBase64(file.content);
}
return undefined;
}
static async getFileStorageFolder(file) {
let zipStorage = file.fileContainerStorage;
if (!zipStorage) {
await file.loadContent();
if (!file.content || !(file.content instanceof Uint8Array)) {
return undefined;
}
zipStorage = new ZipStorage_1.default();
await zipStorage.loadFromUint8Array(file.content, file.name);
file.fileContainerStorage = zipStorage;
file.fileContainerStorage.storagePath = file.storageRelativePath + "#";
}
return zipStorage.rootFolder;
}
static getContaineredFileLeafPath(path) {
if (!path) {
return;
}
const lastHash = path.lastIndexOf("#");
if (lastHash >= 0) {
path = path.substring(lastHash + 1);
}
return path;
}
static isMinecraftInternalFolder(folder) {
const nameCanon = folder.name.toLowerCase();
return _minecraftProjectFolderNames.includes(nameCanon);
}
static isContainerFile(path) {
const extension = StorageUtilities.getTypeFromName(path);
if (extension === "zip" ||
extension === "mcworld" ||
extension === "mcproject" ||
extension === "mctemplate" ||
extension === "mcpack" ||
extension === "mcaddon") {
return true;
}
return false;
}
static isFileStorageItem(file) {
return this.isContainerFile(file.name);
}
static canonicalizeName(name) {
name = name.trim(); //.toLowerCase();
if (name.startsWith("/") || name.startsWith("\\")) {
name = name.substring(1, name.length);
}
if (name.endsWith("/") || name.endsWith("\\")) {
name = name.substring(0, name.length - 1);
}
// constructor is a keyword that will cause array existence checks to fail in interesting ways
if (name === "constructor") {
name = "__constructor";
}
name = name.replace(/%20/g, " ");
name = name.replace(/%28/g, "(");
name = name.replace(/%29/g, ")");
return name;
}
static isPathEqual(pathA, pathB) {
return StorageUtilities.canonicalizePath(pathA) === StorageUtilities.canonicalizePath(pathB);
}
static canonicalizePath(path) {
path = path.trim(); // .toLowerCase();
path = path.replace(/\\/g, "/");
path = path.replace(/%20/g, " ");
path = path.replace(/%28/g, "(");
path = path.replace(/%29/g, ")");
return path;
}
static canonicalizePathAsFileName(path) {
let result = path.trim().toLowerCase();
path = path.replace(/%20/g, " ");
path = path.replace(/%28/g, "(");
path = path.replace(/%29/g, ")");
result = result.replace(/:/g, "_");
result = result.replace(/\//g, "-");
result = result.replace(/\\/g, "-");
result = result.replace(/%/g, "-");
result = result.replace(/--/g, "-");
result = result.replace(/--/g, "-");
result = result.replace(/\?/g, "-");
result = result.replace(/\|/g, "-");
return result;
}
static ensureFileNameIsSafe(path) {
let result = path.trim().toLowerCase();
path = path.replace(/%20/g, " ");
path = path.replace(/%28/g, "(");
path = path.replace(/%29/g, ")");
result = result.replace(/:/g, "_");
result = result.replace(/\//g, "-");
result = result.replace(/\\/g, "-");
result = result.replace(/%/g, "-");
result = result.replace(/--/g, "-");
result = result.replace(/`/g, "-");
result = result.replace(/'/g, "-");
result = result.replace(/–/g, "-");
return result;
}
static hasPathSeparator(path) {
if (!path) {
return false;
}
let lastSlash = path.lastIndexOf("/", path.length - 1);
if (lastSlash >= 0) {
return true;
}
lastSlash = path.lastIndexOf("\\", path.length - 1);
if (lastSlash >= 0) {
return true;
}
return false;
}
static getLeafName(path) {
let name = path;
if (name.endsWith("/")) {
name = name.substring(0, name.length - 1);
}
if (name.endsWith("\\")) {
name = name.substring(0, name.length - 1);
}
let lastSlash = name.lastIndexOf("/", path.length - 1);
if (lastSlash >= 0) {
name = name.substring(lastSlash + 1, name.length);
}
lastSlash = name.lastIndexOf("\\", name.length - 1);
if (lastSlash >= 0) {
name = name.substring(lastSlash + 1, name.length);
}
return name;
}
static getFolderPath(path) {
let lastSlash = path.lastIndexOf("/", path.length - 1);
if (lastSlash >= 0 && lastSlash < path.length - 1) {
path = path.substring(0, lastSlash + 1);
}
else {
lastSlash = path.lastIndexOf("\\", path.length - 1);
if (lastSlash >= 0 && lastSlash < path.length - 1) {
path = path.substring(0, lastSlash + 1);
}
}
return path;
}
static getParentFolderNameFromPath(path) {
let lastSlash = path.lastIndexOf("/", path.length - 1);
if (lastSlash >= 0 && lastSlash < path.length - 1) {
const nextLastSlash = path.lastIndexOf("/", lastSlash - 1);
return path.substring(nextLastSlash + 1, lastSlash);
}
else {
lastSlash = path.lastIndexOf("\\", path.length - 1);
if (lastSlash >= 0 && lastSlash < path.length - 1) {
const nextLastSlash = path.lastIndexOf("/", lastSlash - 1);
return path.substring(nextLastSlash + 1, lastSlash);
}
}
return undefined;
}
static removeContainerExtension(name) {
let nameW = name.trim();
if (nameW.endsWith(".zip")) {
nameW = nameW.substring(0, nameW.length - 4);
}
return nameW;
}
static getBaseFromName(name) {
const nameW = name.trim();
const lastPeriod = nameW.lastIndexOf(".");
if (lastPeriod < 0) {
return name;
}
return nameW.substring(0, lastPeriod);
}
static getTypeFromName(name) {
const nameW = name.trim().toLowerCase();
const lastPeriod = nameW.lastIndexOf(".");
if (lastPeriod < 0) {
return "";
}
return nameW.substring(lastPeriod + 1, nameW.length);
}
static async folderContentsEqual(folderA, folderB, excludingFiles, whitespaceAgnostic, ignoreLinesContaining) {
if (folderA === undefined && folderB === undefined) {
return { result: true, reason: "Both folders are undefined." };
}
if (folderA === undefined) {
return { result: false, reason: "First folder is undefined." };
}
if (folderB === undefined) {
return { result: false, reason: "Second folder is undefined." };
}
await folderA.load(false);
await folderB.load(false);
if (folderA.fileCount !== folderB.fileCount) {
return {
result: false,
reason: "Folder '" +
folderA.fullPath +
"' has " +
folderA.fileCount +
" files; folder '" +
folderB.fullPath +
"' has " +
folderB.fileCount +
" files.",
};
}
for (const fileName in folderA.files) {
const fileA = folderA.files[fileName];
const fileB = folderB.files[fileName];
if (fileA === undefined) {
return { result: false, reason: "Unexpected file '" + fileName + "' undefined." };
}
if (fileB === undefined) {
return { result: false, reason: "File '" + fileName + "' does not exist in '" + folderB.fullPath + "'" };
}
if (!excludingFiles || !excludingFiles.includes(fileA.name)) {
const result = await StorageUtilities.fileContentsEqual(fileA, fileB, whitespaceAgnostic, ignoreLinesContaining);
if (!result) {
return {
result: false,
reason: "File '" +
fileA.fullPath +
"' (size: " +
fileA.content?.length +
(fileA.isBinary ? "B" : "C") +
") contents does not match '" +
fileB.fullPath +
"' (size: " +
fileB.content?.length +
(fileB.isBinary ? "B" : "C") +
")",
};
}
}
}
for (const folderName in folderA.folders) {
const childFolderA = folderA.folders[folderName];
const childFolderB = folderB.folders[folderName];
if (childFolderA === undefined) {
return { result: false, reason: "Unexpected folder undefined. " };
}
if (childFolderB === undefined) {
return { result: false, reason: "Folder '" + folderName + "' does not exist in '" + folderB.fullPath + "'" };
}
const result = await StorageUtilities.folderContentsEqual(childFolderA, childFolderB, excludingFiles, whitespaceAgnostic, ignoreLinesContaining);
if (!result.result) {
return result;
}
}
return { result: true, reason: "Folders are equal" };
}
static async fileContentsEqual(fileA, fileB, whitespaceAgnostic, ignoreLinesContaining) {
if (fileA === undefined && fileB === undefined) {
return true;
}
if (fileA === undefined) {
return false;
}
if (fileB === undefined) {
return false;
}
const fileAExists = await fileA.exists();
if (!fileAExists) {
return false;
}
const fileBExists = await fileB.exists();
if (fileAExists && !fileBExists) {
return false;
}
await fileA.loadContent(false);
await fileB.loadContent(false);
if (fileA.content === undefined && fileB.content === undefined) {
return true;
}
const extA = StorageUtilities.getTypeFromName(fileA.name);
const extB = StorageUtilities.getTypeFromName(fileB.name);
let contentA = fileA.content;
let contentB = fileB.content;
if (contentA === null && contentB === null) {
return true;
}
if (contentA === null || contentB === null) {
return false;
}
if (ignoreLinesContaining && typeof contentA === "string" && typeof contentB === "string") {
for (const ignoreLine of ignoreLinesContaining) {
contentA = Utilities_1.default.stripLinesContaining(contentA, ignoreLine);
contentB = Utilities_1.default.stripLinesContaining(contentB, ignoreLine);
}
}
if (extA === "json" && extB === "json" && typeof contentA === "string" && typeof contentB === "string") {
return this.jsonContentsAreEqualIgnoreWhitespace(contentA, contentB);
}
else if (whitespaceAgnostic) {
return StorageUtilities.contentsAreEqualIgnoreWhitespace(contentA, contentB);
}
return StorageUtilities.contentsAreEqual(contentA, contentB);
}
static jsonContentsAreEqualIgnoreWhitespace(contentA, contentB) {
contentA = this.stripWhitespace(contentA);
contentB = this.stripWhitespace(contentB);
return contentA === contentB;
}
static stripWhitespace(content) {
content = content.trim();
content = content.replace(/ /gi, "");
content = content.replace(/\r/gi, "");
content = content.replace(/\n/gi, "");
content = content.replace(/\t/gi, "");
return content;
}
static contentsAreEqual(contentA, contentB) {
if (contentA === null && contentB === null) {
return true;
}
if (typeof contentA === "string" && typeof contentB === "string") {
return contentA === contentB;
}
if (contentA instanceof Uint8Array && contentB instanceof Uint8Array) {
return Utilities_1.default.uint8ArraysAreEqual(contentA, contentB);
}
return false;
}
static contentsAreEqualIgnoreWhitespace(contentA, contentB) {
if (contentA === null && contentB === null) {
return true;
}
if (typeof contentA === "string" && typeof contentB === "string") {
return this.stripWhitespace(contentA) === this.stripWhitespace(contentB);
}
if (contentA instanceof Uint8Array && contentB instanceof Uint8Array) {
return Utilities_1.default.uint8ArraysAreEqual(contentA, contentB);
}
return false;
}
static async getDifferences(original, updated, includeDeletions, matchSingleChildFolders) {
// matchSingleChildFolders: for project template starters, where they have a structure like:
// resource_packs/template_name
// that gets renamed to
// resource_packs/mikesfooproject
// then -- if there is one folder in the original and one folder in the updated,
// we want to match them up irrespective of the name
const data = new DifferenceSet_1.default();
await this.addDifferences(data, original, updated, includeDeletions, matchSingleChildFolders);
return data;
}
static getFirstFile(folder) {
for (const fileName in folder.files) {
const file = folder.files[fileName];
if (file) {
return file;
}
}
for (const folderName in folder.folders) {
const subFolder = folder.folders[folderName];
if (subFolder) {
const file = this.getFirstFile(subFolder);
if (file) {
return file;
}
}
}
return undefined;
}
static async addDifferences(differences, original, updated, includeDeletions, matchSingleFolders) {
await original.load(false);
await updated.load(false);
let result = IFolderDifference_1.FolderDifferenceType.none;
// get a list of existing files and folders in the target
const updatedFilesToConsider = {};
const updatedFoldersToConsider = {};
for (const updatedFileName in updated.files) {
if (BasicValidators_1.BasicValidators.isFileNameOKForSharing(updatedFileName)) {
updatedFilesToConsider[updatedFileName] = true;
}
}
for (const updatedFolderName in updated.folders) {
if (BasicValidators_1.BasicValidators.isFolderNameOKForSharing(updatedFolderName)) {
updatedFoldersToConsider[updatedFolderName] = true;
}
}
for (const originalFileName in original.files) {
if (BasicValidators_1.BasicValidators.isFileNameOKForSharing(originalFileName)) {
const originalFile = original.files[originalFileName];
if (originalFile !== undefined) {
updatedFilesToConsider[originalFileName] = false;
if (updated.fileExists(originalFileName)) {
const updatedFile = updated.files[originalFileName];
const areEqual = await StorageUtilities.fileContentsEqual(originalFile, updatedFile);
if (!areEqual) {
if ((result & IFolderDifference_1.FolderDifferenceType.fileContentsDifferent) === 0) {
result += IFolderDifference_1.FolderDifferenceType.fileContentsDifferent;
}
differences.fileDifferences.push({
type: IFileDifference_1.FileDifferenceType.contentsDifferent,
path: originalFile.storageRelativePath,
original: originalFile,
updated: updatedFile,
});
}
}
else if (includeDeletions) {
if ((result & IFolderDifference_1.FolderDifferenceType.fileListDifferent) === 0) {
result += IFolderDifference_1.FolderDifferenceType.fileListDifferent;
}
differences.fileDifferences.push({
type: IFileDifference_1.FileDifferenceType.fileDeleted,
path: originalFile.storageRelativePath,
original: originalFile,
});
}
}
}
}
for (const originalFolderName in original.folders) {
if (BasicValidators_1.BasicValidators.isFolderNameOKForSharing(originalFolderName)) {
const originalChildFolder = original.folders[originalFolderName];
if (originalChildFolder !== undefined) {
updatedFoldersToConsider[originalFolderName] = false;
if (updated.folderExists(originalFolderName) ||
(matchSingleFolders && updated.folderCount === 1 && original.folderCount === 1)) {
let updatedChildFolder = updated.folders[originalFolderName];
if (matchSingleFolders && updated.folderCount && original.folderCount && !updatedChildFolder) {
updatedChildFolder = updated.getFolderByIndex(0);
}
if (updatedChildFolder !== undefined) {
updatedFoldersToConsider[updatedChildFolder.name] = false;
const childResult = await StorageUtilities.addDifferences(differences, originalChildFolder, updatedChildFolder, includeDeletions, matchSingleFolders);
if (childResult !== IFolderDifference_1.FolderDifferenceType.none) {
result = result | childResult;
}
}
}
else if (includeDeletions) {
if ((result & IFolderDifference_1.FolderDifferenceType.folderDeleted) === 0) {
result += IFolderDifference_1.FolderDifferenceType.folderDeleted;
}
this.addDifferencesAsFolderDelete(differences, originalChildFolder);
}
}
}
}
for (const updatedFileName in updatedFilesToConsider) {
if (updatedFilesToConsider[updatedFileName] === true) {
const updatedFile = updated.files[updatedFileName];
if (updatedFile !== undefined) {
if ((result & IFolderDifference_1.FolderDifferenceType.fileListDifferent) === 0) {
result += IFolderDifference_1.FolderDifferenceType.fileListDifferent;
}
if (updatedFile.content !== undefined && updatedFile.content !== null) {
differences.fileDifferences.push({
type: IFileDifference_1.FileDifferenceType.fileAdded,
path: StorageUtilities.relativizePathToOriginal(original, updated, updatedFile.storageRelativePath),
updated: updatedFile,
});
}
}
}
}
for (const updatedFolderName in updatedFoldersToConsider) {
if (updatedFoldersToConsider[updatedFolderName] === true) {
const updatedFolder = updated.folders[updatedFolderName];
if (updatedFolder !== undefined) {
if ((result & IFolderDifference_1.FolderDifferenceType.folderAdded) === 0) {
result += IFolderDifference_1.FolderDifferenceType.folderAdded;
}
this.addDifferencesAsFolderAdd(differences, updatedFolder, original, updated);
}
}
}
if (result !== IFolderDifference_1.FolderDifferenceType.none) {
differences.folderDifferences.push({
type: result,
path: original.storageRelativePath,
original: original,
updated: updated,
});
}
return result;
}
static relativizePathToOriginal(original, updated, path) {
const folders = original.storageRelativePath.split(StorageUtilities.standardFolderDelimiter);
for (let i = folders.length - 1; i >= 0; i--) {
const lastIndexOfInPath = path.lastIndexOf("/" + folders[i] + "/");
const lastIndexOfInSource = original.storageRelativePath.lastIndexOf("/" + folders[i] + "/");
if (lastIndexOfInPath >= 0 && lastIndexOfInSource >= 0) {
return original.storageRelativePath.substring(0, lastIndexOfInSource) + path.substring(lastIndexOfInPath);
}
}
const originalPathLength = original.storageRelativePath.length;
const updatedPathLength = updated.storageRelativePath.length;
if (updatedPathLength > originalPathLength) {
path = path.substring(updatedPathLength - originalPathLength, path.length);
}
return path;
}
static async addDifferencesAsFolderAdd(differences, childUpdated, original, updated) {
await childUpdated.load(false);
differences.folderDifferences.push({
type: IFolderDifference_1.FolderDifferenceType.folderAdded,
path: StorageUtilities.relativizePathToOriginal(original, updated, childUpdated.storageRelativePath),
updated: childUpdated,
});
for (const updatedFileName in childUpdated.files) {
const updatedFile = childUpdated.files[updatedFileName];
if (updatedFile !== undefined) {
differences.fileDifferences.push({
type: IFileDifference_1.FileDifferenceType.fileAdded,
path: StorageUtilities.relativizePathToOriginal(original, updated, updatedFile.storageRelativePath),
updated: updatedFile,
});
}
}
for (const updatedFolderName in childUpdated.folders) {
const updatedFolder = childUpdated.folders[updatedFolderName];
if (updatedFolder !== undefined) {
this.addDifferencesAsFolderAdd(differences, updatedFolder, original, updated);
}
}
}
static async addDifferencesAsFolderDelete(differences, original) {
await original.load(false);
differences.folderDifferences.push({
type: IFolderDifference_1.FolderDifferenceType.folderDeleted,
path: original.storageRelativePath,
updated: original,
});
for (const originalFileName in original.files) {
const originalFile = original.files[originalFileName];
if (originalFile !== undefined) {
differences.fileDifferences.push({
type: IFileDifference_1.FileDifferenceType.fileDeleted,
path: originalFile.storageRelativePath,
original: originalFile,
});
}
}
for (const originalFolderName in original.folders) {
const originalFolder = original.folders[originalFolderName];
if (originalFolder !== undefined) {
this.addDifferencesAsFolderDelete(differences, originalFolder);
}
}
}
static isPathRiskyForDelete(path) {
path = path.toLowerCase().trim();
if (path.indexOf("system32") >= 0 || path.indexOf("program files") >= 0 || path.indexOf("programdata") >= 0) {
return true;
}
// a very crude way to ensure this code never removes c:\ or c:\my documents or whatever or something elemental.
return Utilities_1.default.countChar(path, "/") + Utilities_1.default.countChar(path, "\\") < 4;
}
static getParentOfParentFolderNamed(folderName, folder) {
let ancestorFolder = undefined;
while (folder.name !== folderName && folder.parentFolder) {
folder = folder.parentFolder;
}
if (folder.parentFolder) {
ancestorFolder = folder.parentFolder;
}
return ancestorFolder;
}
static getJsonObject(file) {
if (!file.content) {
return undefined;
}
if (!(typeof file.content === "string")) {
return undefined;
}
let jsonObject = undefined;
let didFailToParse = false;
let contents = file.content;
contents = Utilities_1.default.fixJsonContent(contents);
try {
jsonObject = JSON.parse(contents);
}
catch (e) {
file.isInErrorState = true;
file.errorStateMessage = e.message;
didFailToParse = true;
Log_1.default.fail("Could not parse JSON from '" + file.fullPath + "': " + e.message);
}
if (file.isInErrorState && !didFailToParse && contents.length > 0) {
file.isInErrorState = false;
file.errorStateMessage = undefined;
}
return jsonObject;
}
static isIgnorableFolder(folderName) {
return folderName.startsWith(".") && !folderName.toLowerCase().startsWith(".vscode");
}
static async getUniqueFileName(baseName, extension, folder) {
let candFileName = baseName + "." + extension;
let exists = folder.fileExists(candFileName);
let increment = 0;
while (exists && increment < 99) {
increment++;
candFileName = baseName + " " + String(increment) + "." + extension;
exists = folder.fileExists(candFileName);
}
return candFileName;
}
static async ensureFilesFromJson(storage, json) {
if (typeof json === "string") {
try {
json = JSON.parse(json);
}
catch (e) {
return e.toString();
}
}
if (typeof json === "object") {
for (let path in json) {
let data = json[path];
if (!path.startsWith(StorageUtilities.standardFolderDelimiter)) {
path = StorageUtilities.standardFolderDelimiter + path;
}
const file = await storage.rootFolder.ensureFileFromRelativePath(path);
if (!file) {
return "Could not create file '" + path + "'.";
}
if (file.isBinary) {
return "Could not create file '" + path + "'; it is a binary file.";
}
if (typeof data === "object") {
try {
data = JSON.stringify(data, null, 2);
}
catch (e) {
return e.toString();
}
}
file.setContent(data);
}
}
return;
}
static async createStorageFromString(content) {
let storage = undefined;
try {
content = content.trim();
if (content.length < 1) {
return "No content provided.";
}
if (content.startsWith("{") || content.startsWith("[")) {
storage = new Storage_1.default();
const result = await StorageUtilities.ensureFilesFromJson(storage, content);
if (result) {
return result;
}
}
else {
const contentUnbase64 = Utilities_1.default.base64ToUint8Array(content);
if (contentUnbase64.length < 2) {
return "Invalid base64 content provided.";
}
if (contentUnbase64[0] === "{".charCodeAt(0)) {
const jsonStr = new TextDecoder("utf-8").decode(contentUnbase64);
storage = new Storage_1.default();
const result = await StorageUtilities.ensureFilesFromJson(storage, jsonStr);
if (!result && typeof result === "string") {
return result;
}
}
else {
storage = new ZipStorage_1.default();
await storage.loadFromBase64(content);
}
}
if (storage.errorStatus) {
return "Error processing content." + (storage.errorMessage ? "Details: " + storage.errorMessage : "");
}
}
catch (e) {
return "Unexpected error processing content.";
}
return storage;
}
static async createStorageFromUntrustedString(untrustedContent) {
if (untrustedContent.length > exports.MaxShareableContentStringLength) {
return ("Shared content are too large to include in the URL (" +
untrustedContent.length +
" > " +
exports.MaxShareableContentStringLength +
").");
}
const result = await StorageUtilities.createStorageFromString(untrustedContent);
if (!result || typeof result === "string") {
return result;
}
const valResult = await BasicValidators_1.BasicValidators.isFolderSharingValid(result.rootFolder);
if (valResult !== undefined) {
return valResult;
}
return result;
}
static async syncFolderTo(source, target, forceFolders, forceFileUpdates, removeOnTarget, exclude, include, messageUpdater, dontOverwriteExistingFiles, skipFilesAtRoot) {
let modifiedFileCount = 0;
// Log.debug("Syncing folder '" + source.storageRelativePath + "' to '" + target.storageRelativePath + "'");
if (StorageUtilities.isIgnorableFolder(source.name)) {
return 0;
}
/*
if (messageUpdater) {
await messageUpdater(
"Syncing folder from '" + source.storageRelativePath + "' to '" + target.storageRelativePath + "'."
);
}*/
await source.load(forceFolders);
await target.load(forceFolders);
// get a list of existing files and folders in the target
let targetFiles = {};
let targetFolders = {};
for (const targetFileName in target.files) {
if (target.files[targetFileName] !== undefined) {
targetFiles[targetFileName] = true;
}
}
if (!skipFilesAtRoot) {
for (const targetFolderName in target.folders) {
if (target.folders[targetFolderName] !== undefined && !StorageUtilities.isIgnorableFolder(targetFolderName)) {
targetFolders[targetFolderName] = true;
}
}
for (const sourceFileName in source.files) {
const sourceFile = source.files[sourceFileName];
let process = true;
if (exclude !== undefined && StorageUtilities.matchesList(sourceFileName, exclude)) {
process = false;
}
if (include !== undefined && !StorageUtilities.matchesList(sourceFileName, include)) {
process = false;
}
if (sourceFile !== undefined) {
if (process) {
targetFiles[sourceFileName] = false;
const targetFile = target.ensureFile(sourceFile.name);
let updateFile = true;
if (dontOverwriteExistingFiles) {
if (await targetFile.exists()) {
updateFile = false;
if (messageUpdater) {
messageUpdater("Not updating '" + targetFile.fullPath + "' as it already exists.");
}
}
}
if (updateFile) {
const wasUpdated = await this.syncFileTo(sourceFile, targetFile, forceFileUpdates, messageUpdater);
if (wasUpdated) {
modifiedFileCount++;
}
}
}
}
}
}
for (const sourceFolderName in source.folders) {
if (!StorageUtilities.isIgnorableFolder(sourceFolderName)) {
const sourceChildFolder = source.folders[sourceFolderName];
let process = true;
if (exclude !== undefined && StorageUtilities.matchesList("/" + sourceFolderName, exclude)) {
process = false;
}
if (sourceChildFolder !== undefined) {
if (process) {
targetFolders[sourceFolderName] = false;
const targetChildFolder = target.ensureFolder(sourceChildFolder.name);
await targetChildFolder.ensureExists();
const subfolderFilesUpdated = await this.syncFolderTo(sourceChildFolder, targetChildFolder, forceFolders, forceFileUpdates, removeOnTarget, exclude, include, messageUpdater, dontOverwriteExistingFiles);
modifiedFileCount += subfolderFilesUpdated;
}
}
}
}
if (removeOnTarget) {
for (const targetFileName in targetFiles) {
let process = true;
/*
If a file matches the exclude list, ignore it, don't remove it. But commenting this out so that excluded files get removed on target.
if (exclude !== undefined && StorageUtilities.matchesList(targetFileName, exclude)) {
process = false;
}*/
if (process && targetFiles[targetFileName] === true) {
if (messageUpdater) {
await messageUpdater("Removing file '" + target.fullPath + "' (" + targetFileName + ")");
}
await target.deleteFile(targetFileName);
modifiedFileCount++;
}
}
}
return modifiedFileCount;
}
static matchesList(name, list) {
name = StorageUtilities.canonicalizeName(name);
let nameMinusSlash = name;
if (nameMinusSlash.startsWith("/")) {
nameMinusSlash = nameMinusSlash.substring(1, nameMinusSlash.length);
}
for (let i = 0; i < list.length; i++) {
const listC = StorageUtilities.canonicalizeName(list[i]);
if (name === listC) {
return true;
}
if (!listC.startsWith("/")) {
if (nameMinusSlash === listC) {
return true;
}
}
if (listC.length > 2 && listC.startsWith("*") && listC.endsWith("*")) {
if (name.indexOf(listC.substring(1, listC.length - 1)) >= 0) {
return true;
}
}
else if (listC.length > 2 && listC.startsWith("*") && !listC.endsWith("*")) {
if (name.endsWith(listC.substring(1))) {
return true;
}
}
else if (listC.length > 2 && !listC.startsWith("*") && listC.endsWith("*")) {
if (name.startsWith(listC.substring(0, listC.length - 1))) {
return true;
}
}
}
return false;
}
static sanitizePathBasic(path) {
path = path.replace(/</gi, "_");
path = path.replace(/>/gi, "_");
path = path.replace(/ /gi, "_");
path = path.replace(/"/gi, "_");
path = path.replace(/'/gi, "_");
path = path.replace(/::/gi, "_");
path = path.replace(/,/gi, "_");
path = path.replace(/:/gi, "_");
path = path.replace(/\r/gi, "_");
path = path.replace(/\n/gi, "_");
path = path.replace(/__/gi, "_");
path = path.replace(/__/gi, "_");
while (path.length > 1 && path.startsWith("_")) {
path = path.substring(1, path.length);
}
while (path.length > 1 && path.endsWith("_")) {
path = path.substring(0, path.length - 1);
}
path = path.trim();
return path;
}
static sanitizePath(path) {
if (Utilities_1.default.isAlphaNumeric(path)) {
return path;
}
let utf8Encode = new TextEncoder();
const base64 = Utilities_1.default.arrayBufferToBase64(utf8Encode.encode(path))
.replace(/\//gi, " ")
.replace(/=/gi, "_");
return base64;
}
static async syncFileTo(source, target, force, messageUpdater) {
// Log.debug("Syncing file content '" + source.fullPath + "'");
await source.loadContent(true);
if (source.content == null) {
Log_1.default.debug("No content for file " + source.storageRelativePath);
return;
}
if (!force) {
if (await target.exists()) {
await target.loadContent(false);
if (StorageUtilities.contentsAreEqual(source.content, target.content)) {
return;
}
}
}
if (messageUpdater) {
let targetPath = target.fullPath;
targetPath = targetPath.replace("fs.mctprojects/root/", "");
let mess = "Updating file: " + targetPath;
if (source.content) {
mess += " (size: " + source.content.length + ")";
}
await messageUpdater(mess);
}
// Log.debug("Copying file " + source.storageRelativePath + " to " + target.storageRelativePath);
target.setContent(source.content);
return true;
}
}
exports.default = StorageUtilities;
StorageUtilities.standardFolderDelimiter = "/";
//# sourceMappingURL=../maps/storage/StorageUtilities.js.map