UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

502 lines (501 loc) 18.7 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 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;