@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
502 lines (501 loc) • 18.7 kB
JavaScript
;
// 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 StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities"));
const Log_1 = __importDefault(require("../core/Log"));
const Utilities_1 = __importDefault(require("../core/Utilities"));
const IWorldBackupData_1 = require("./IWorldBackupData");
const NodeFolder_1 = __importDefault(require("./NodeFolder"));
const NodeStorage_1 = __importDefault(require("./NodeStorage"));
const ZipStorage_1 = __importDefault(require("../storage/ZipStorage"));
const fs = __importStar(require("fs"));
class WorldBackup {
_metadata;
_folder;
/**
* Get the backup ID (folder name, e.g., "world20260101120000").
*/
get id() {
return this._metadata.id;
}
/**
* Get the parent world ID.
*/
get worldId() {
return this._metadata.worldId;
}
/**
* Get the creation timestamp.
*/
get createdAt() {
return new Date(this._metadata.createdAt);
}
/**
* Get the creation timestamp as a number (milliseconds since epoch).
*/
get timestamp() {
return new Date(this._metadata.createdAt).getTime();
}
/**
* Get the backup type.
*/
get backupType() {
return this._metadata.backupType;
}
/**
* Alias for backupType.
*/
get type() {
return this._metadata.backupType;
}
/**
* Get description (alias for notes).
*/
get description() {
return this._metadata.notes;
}
/**
* Get total bytes (alias for sizeBytes).
*/
get totalBytes() {
return this._metadata.sizeBytes;
}
/**
* Get the configuration hash at backup time.
*/
get configurationHash() {
return this._metadata.configurationHash;
}
/**
* Get the server version at backup time.
*/
get serverVersion() {
return this._metadata.serverVersion;
}
/**
* Get the total size in bytes.
*/
get sizeBytes() {
return this._metadata.sizeBytes;
}
/**
* Get the file count.
*/
get fileCount() {
return this._metadata.fileCount;
}
/**
* Get the deduplicated file count.
*/
get deduplicatedFileCount() {
return this._metadata.deduplicatedFileCount;
}
/**
* Get the world name at backup time.
*/
get worldName() {
return this._metadata.worldName;
}
/**
* Get optional notes.
*/
get notes() {
return this._metadata.notes;
}
/**
* Set optional notes.
*/
set notes(value) {
this._metadata.notes = value;
}
/**
* Get the file listing.
*/
get files() {
return this._metadata.files;
}
/**
* Get the folder containing this backup.
*/
get folder() {
return this._folder;
}
/**
* Get the raw metadata object.
*/
get metadata() {
return this._metadata;
}
/**
* Private constructor - use static factory methods.
*/
constructor(metadata, folder) {
this._metadata = metadata;
this._folder = folder;
}
/**
* Generate a backup ID from a timestamp.
*/
static generateId(date) {
return "world" + Utilities_1.default.getDateStr(date || new Date());
}
/**
* Create a new backup with the given metadata.
*
* @param metadata The backup metadata
* @param parentFolder The world folder containing backups
* @returns The newly created WorldBackup
*/
static async create(metadata, parentFolder) {
const backupFolder = parentFolder.ensureFolder(metadata.id);
await backupFolder.ensureExists();
const backup = new WorldBackup(metadata, backupFolder);
await backup.save();
return backup;
}
/**
* Load an existing backup from a folder.
*
* @param folder The backup folder (e.g., worlds/a3k9m2p1/world20260101120000/)
* @param worldId The parent world ID
* @returns The loaded WorldBackup, or undefined if invalid
*/
static async load(folder, worldId) {
// Force reload from disk to ensure we have the actual files
await folder.load(true);
const backupFile = folder.files["backup.json"];
if (!backupFile) {
// Try legacy files.json format
const filesFile = folder.files["files.json"];
if (filesFile) {
return WorldBackup.loadFromLegacyFormat(folder, worldId, filesFile);
}
Log_1.default.debug(`No backup.json found in ${folder.fullPath}`);
return undefined;
}
await backupFile.loadContent(false);
const metadata = StorageUtilities_1.default.getJsonObject(backupFile);
if (!metadata || !metadata.id) {
Log_1.default.debug(`Invalid backup.json in ${folder.fullPath}`);
return undefined;
}
return new WorldBackup(metadata, folder);
}
/**
* Load from legacy files.json format (backwards compatibility).
*/
static async loadFromLegacyFormat(folder, worldId, filesFile) {
await filesFile.loadContent(false);
const legacy = StorageUtilities_1.default.getJsonObject(filesFile);
if (!legacy || !legacy.files) {
return undefined;
}
// Extract date from folder name
const folderName = folder.name;
let createdAt = new Date().toISOString();
if (folderName.startsWith("world") && folderName.length === 19) {
const dateStr = folderName.substring(5);
if (Utilities_1.default.isNumeric(dateStr)) {
createdAt = Utilities_1.default.getDateFromStr(dateStr).toISOString();
}
}
// Calculate stats
let sizeBytes = 0;
let deduplicatedCount = 0;
for (const file of legacy.files) {
if (file.sourcePath) {
deduplicatedCount++;
}
else if (file.size) {
sizeBytes += file.size;
}
}
const metadata = {
id: folderName,
worldId: worldId,
createdAt: createdAt,
backupType: IWorldBackupData_1.WorldBackupType.full,
sizeBytes: sizeBytes,
fileCount: legacy.files.length,
deduplicatedFileCount: deduplicatedCount,
files: legacy.files,
};
return new WorldBackup(metadata, folder);
}
/**
* Save the backup metadata to backup.json.
*/
async save() {
const backupFile = this._folder.ensureFile("backup.json");
backupFile.setContent(JSON.stringify(this._metadata, null, 2));
await backupFile.saveContent();
}
/**
* Recursively force load a folder and all its subfolders.
* This ensures in-memory folder structure matches disk after backup creation.
*/
static async recursiveForceLoad(folder) {
await folder.load(true);
for (const folderName in folder.folders) {
const subFolder = folder.folders[folderName];
if (subFolder) {
await WorldBackup.recursiveForceLoad(subFolder);
}
}
}
/**
* Restore this backup to a target folder.
*
* @param options Restore options
* @param worldContainerFolder The root worlds folder for resolving source paths
* @returns Restore result
*/
async restore(options, worldContainerFolder) {
try {
const targetPath = options.targetPath;
// Optionally clear the target folder
if (options.clearTarget && fs.existsSync(targetPath)) {
const targetStorage = new NodeStorage_1.default(targetPath, "");
await targetStorage.rootFolder.deleteAllFolderContents();
}
// Ensure target exists
if (!fs.existsSync(targetPath)) {
fs.mkdirSync(targetPath, { recursive: true });
}
// Recursively load backup folder contents to ensure all subfolders are loaded from disk
await WorldBackup.recursiveForceLoad(this._folder);
const targetStorage = new NodeStorage_1.default(targetPath, "");
// Copy files based on metadata
let totalBytes = 0;
let restoredFiles = 0;
for (const fileInfo of this._metadata.files) {
if (fileInfo.path) {
let sourceFile;
if (fileInfo.sourcePath) {
// File is deduplicated - get it from the source path (relative to worldContainerFolder)
sourceFile = await worldContainerFolder.getFileFromRelativePath(StorageUtilities_1.default.ensureStartsWithDelimiter(fileInfo.sourcePath));
}
else {
// File is stored in this backup folder
sourceFile = await this._folder.getFileFromRelativePath(StorageUtilities_1.default.ensureStartsWithDelimiter(fileInfo.path));
}
if (sourceFile) {
if (!sourceFile.isContentLoaded) {
await sourceFile.loadContent();
}
if (sourceFile.content !== null) {
const targetFile = await targetStorage.rootFolder.ensureFileFromRelativePath(StorageUtilities_1.default.ensureStartsWithDelimiter(fileInfo.path));
if (targetFile) {
targetFile.setContent(sourceFile.content);
restoredFiles++;
if (fileInfo.size) {
totalBytes += fileInfo.size;
}
}
}
}
else {
Log_1.default.debug(`Could not find backup file '${fileInfo.path}' for restore`);
}
}
}
await targetStorage.rootFolder.saveAll();
Log_1.default.message(`Restored backup ${this._metadata.id} to ${targetPath}: ${restoredFiles} files`);
return {
success: true,
restoredPath: targetPath,
stats: {
totalFiles: restoredFiles,
restoredBytes: totalBytes,
},
};
}
catch (e) {
Log_1.default.error(`Failed to restore backup ${this._metadata.id}: ${e.message}`);
return {
success: false,
error: e.message,
};
}
}
/**
* Export this backup as a .mcworld file.
*
* @param options Export options
* @param worldContainerFolder The root worlds folder for resolving source paths
* @returns Export result
*/
async exportMcWorld(options, worldContainerFolder) {
try {
// First restore to a temp folder
const tempPath = NodeStorage_1.default.ensureEndsWithDelimiter(StorageUtilities_1.default.getFolderPath(options.outputPath)) +
"mcworld_temp_" +
Date.now();
const restoreResult = await this.restore({ targetPath: tempPath }, worldContainerFolder);
if (!restoreResult.success) {
return {
success: false,
error: restoreResult.error,
};
}
// Optionally update the world name
if (options.worldName) {
const levelNamePath = NodeStorage_1.default.ensureEndsWithDelimiter(tempPath) + "levelname.txt";
fs.writeFileSync(levelNamePath, options.worldName, { encoding: "utf8" });
}
// Create the zip file
const tempStorage = new NodeStorage_1.default(tempPath, "");
await tempStorage.rootFolder.load(true);
const zipStorage = new ZipStorage_1.default();
await StorageUtilities_1.default.syncFolderTo(tempStorage.rootFolder, zipStorage.rootFolder, false, false, false);
const zipBytes = await zipStorage.generateUint8ArrayAsync();
// Write the .mcworld file
fs.writeFileSync(options.outputPath, Buffer.from(zipBytes));
// Clean up temp folder
await tempStorage.rootFolder.deleteThisFolder();
const sizeBytes = zipBytes.byteLength;
Log_1.default.message(`Exported backup ${this._metadata.id} to ${options.outputPath} (${sizeBytes} bytes)`);
return {
success: true,
outputPath: options.outputPath,
sizeBytes: sizeBytes,
};
}
catch (e) {
Log_1.default.error(`Failed to export backup ${this._metadata.id}: ${e.message}`);
return {
success: false,
error: e.message,
};
}
}
/**
* Check if this backup has files that are deduplicated (reference another backup).
*/
hasDeduplicatedFiles() {
return this._metadata.files.some((f) => f.sourcePath !== undefined);
}
/**
* Get all sourcePath values that this backup depends on.
* Returns backup folder paths (e.g., "/worldId/backupId/") that contain files this backup references.
*/
getDependencies() {
const deps = new Set();
for (const file of this._metadata.files) {
if (file.sourcePath) {
// Extract the backup folder path from the sourcePath
// sourcePath format: /worldId/backupId/path/to/file.ext
const parts = file.sourcePath.split("/").filter((p) => p.length > 0);
if (parts.length >= 2) {
deps.add(`/${parts[0]}/${parts[1]}/`);
}
}
}
return deps;
}
/**
* Consolidate this backup by copying deduplicated files locally.
* This is needed before deleting a backup that other backups depend on.
*
* @param worldContainerFolder The root worlds folder for resolving source paths
* @returns true if consolidation was successful
*/
async consolidate(worldContainerFolder) {
if (!this.hasDeduplicatedFiles()) {
return true; // Nothing to consolidate
}
try {
await WorldBackup.recursiveForceLoad(this._folder);
let consolidated = 0;
for (const fileInfo of this._metadata.files) {
if (fileInfo.sourcePath && fileInfo.path) {
// Get the source file from the referenced backup
const sourceFile = await worldContainerFolder.getFileFromRelativePath(StorageUtilities_1.default.ensureStartsWithDelimiter(fileInfo.sourcePath));
if (sourceFile) {
if (!sourceFile.isContentLoaded) {
await sourceFile.loadContent();
}
if (sourceFile.content !== null) {
// Create local copy
const targetFile = await this._folder.ensureFileFromRelativePath(StorageUtilities_1.default.ensureStartsWithDelimiter(fileInfo.path));
if (targetFile) {
targetFile.setContent(sourceFile.content);
await targetFile.saveContent();
// Remove the sourcePath reference
delete fileInfo.sourcePath;
consolidated++;
}
}
}
else {
Log_1.default.debug(`Could not find source file '${fileInfo.sourcePath}' for consolidation`);
}
}
}
// Update metadata counts
this._metadata.deduplicatedFileCount = 0;
// Save updated metadata
await this.save();
Log_1.default.message(`Consolidated ${consolidated} deduplicated files in backup ${this._metadata.id}`);
return true;
}
catch (e) {
Log_1.default.error(`Failed to consolidate backup ${this._metadata.id}: ${e.message}`);
return false;
}
}
/**
* Delete this backup.
*/
async delete() {
try {
if (this._folder instanceof NodeFolder_1.default) {
await this._folder.deleteThisFolder();
}
Log_1.default.message(`Deleted backup ${this._metadata.id}`);
return true;
}
catch (e) {
Log_1.default.error(`Failed to delete backup ${this._metadata.id}: ${e}`);
return false;
}
}
}
exports.default = WorldBackup;