UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

539 lines (538 loc) 19.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 Log_1 = __importDefault(require("../core/Log")); const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities")); const NodeStorage_1 = __importDefault(require("./NodeStorage")); const NodeFolder_1 = __importDefault(require("./NodeFolder")); const ManagedWorld_1 = __importDefault(require("./ManagedWorld")); const WorldBackup_1 = __importDefault(require("./WorldBackup")); const IWorldBackupData_1 = require("./IWorldBackupData"); const fs = __importStar(require("fs")); class WorldBackupManager { _containerStorage; _containerFolder; _manifest; _worlds = new Map(); _fileListings = {}; _isLoaded = false; /** * Get the container folder for all worlds. */ get containerFolder() { return this._containerFolder; } /** * Get the global file listings for deduplication. */ get fileListings() { return this._fileListings; } /** * Check if the manager has been loaded. */ get isLoaded() { return this._isLoaded; } /** * Get all loaded worlds. */ get worlds() { return Array.from(this._worlds.values()); } /** * Alias for worlds (for consistency with ServerManager API). */ get managedWorlds() { return this.worlds; } /** * Get the total number of backups across all worlds. */ get totalBackupCount() { let count = 0; for (const world of this._worlds.values()) { count += world.backups.length; } return count; } /** * Create a new WorldBackupManager. * * @param containerStorage NodeStorage pointing to the worlds container folder * @param initialFileListings Optional initial file listings for deduplication */ constructor(containerStorage, initialFileListings) { this._containerStorage = containerStorage; this._containerFolder = containerStorage.rootFolder; this._manifest = this.createEmptyManifest(); if (initialFileListings) { this._fileListings = initialFileListings; } } /** * Create an empty manifest. */ createEmptyManifest() { return { version: IWorldBackupData_1.MANIFEST_VERSION, lastModified: new Date().toISOString(), worlds: {}, }; } /** * Load the manager: read manifest and discover worlds. */ async load() { if (this._isLoaded) { return; } await this._containerFolder.ensureExists(); await this._containerFolder.load(true); // Load manifest await this.loadManifest(); // Discover worlds await this.discoverWorlds(); // Build file listings for deduplication await this.buildFileListings(); this._isLoaded = true; if (this._worlds.size > 0) { Log_1.default.verbose(`WorldBackupManager loaded: ${this._worlds.size} worlds, ${this.totalBackupCount} backups`); } } /** * Load the global manifest. */ async loadManifest() { const manifestFile = this._containerFolder.files["manifest.json"]; if (!manifestFile) { this._manifest = this.createEmptyManifest(); return; } await manifestFile.loadContent(false); const data = StorageUtilities_1.default.getJsonObject(manifestFile); if (data && data.version) { this._manifest = data; } else { this._manifest = this.createEmptyManifest(); } } /** * Save the global manifest. */ async saveManifest() { // Update world summaries this._manifest.worlds = {}; for (const [id, world] of this._worlds) { this._manifest.worlds[id] = world.getSummary(); } this._manifest.lastModified = new Date().toISOString(); const manifestFile = this._containerFolder.ensureFile("manifest.json"); manifestFile.setContent(JSON.stringify(this._manifest, null, 2)); await manifestFile.saveContent(); } /** * Discover all worlds in the container folder. * Worlds are identified by their 8-character ID folder names. */ async discoverWorlds() { for (const folderName in this._containerFolder.folders) { // World folders are 8-character IDs if (ManagedWorld_1.default.isValidId(folderName)) { const folder = this._containerFolder.folders[folderName]; if (folder) { const world = await ManagedWorld_1.default.load(folder); if (world) { this._worlds.set(world.id, world); } } } } } /** * Build file listings from all existing backups for deduplication. */ async buildFileListings() { this._fileListings = {}; for (const world of this._worlds.values()) { const worldFolder = world.folder; if (worldFolder instanceof NodeFolder_1.default) { const listings = await worldFolder.generateFileListings(this._fileListings); Object.assign(this._fileListings, listings); } } Log_1.default.verbose(`Built file listings: ${Object.keys(this._fileListings).length} unique files`); } /** * Create a new managed world. * * @param friendlyName User-visible name * @param configurationHash Optional initial configuration hash * @returns The newly created ManagedWorld */ async createWorld(friendlyName, configurationHash) { await this.ensureLoaded(); const world = await ManagedWorld_1.default.create(friendlyName, this._containerFolder, configurationHash); this._worlds.set(world.id, world); await this.saveManifest(); return world; } /** * Get a world by ID (synchronous lookup). */ getWorld(worldId) { return this._worlds.get(worldId); } /** * Get a world by ID (async version, ensures loaded). */ async getWorldAsync(worldId) { await this.ensureLoaded(); return this._worlds.get(worldId); } /** * Get a world by friendly name (first match). */ async getWorldByName(friendlyName) { await this.ensureLoaded(); for (const world of this._worlds.values()) { if (world.friendlyName === friendlyName) { return world; } } return undefined; } /** * Get or create a world for a server slot. * This provides a consistent way to manage backups for server slots. * * @param slotNumber The slot number (0, 1, 2, etc.) * @returns The ManagedWorld for this slot */ async getOrCreateWorldForSlot(slotNumber) { await this.ensureLoaded(); const slotName = `Slot ${slotNumber} World`; // Look for existing world for this slot let world = await this.getWorldByName(slotName); if (!world) { // Create a new world for this slot world = await this.createWorld(slotName); Log_1.default.verbose(`Created managed world for slot ${slotNumber}: ${world.id}`); } return world; } /** * List all worlds. */ async listWorlds() { await this.ensureLoaded(); return this.worlds; } /** * Update a world's metadata. */ async updateWorld(worldId, updates) { await this.ensureLoaded(); const world = this._worlds.get(worldId); if (!world) { return false; } if (updates.friendlyName !== undefined) { world.friendlyName = updates.friendlyName; } if (updates.notes !== undefined) { world.notes = updates.notes; } if (updates.tags !== undefined) { world.tags = updates.tags; } await world.save(); await this.saveManifest(); return true; } /** * Delete a world and all its backups. */ async deleteWorld(worldId) { await this.ensureLoaded(); const world = this._worlds.get(worldId); if (!world) { return false; } const success = await world.delete(); if (success) { this._worlds.delete(worldId); await this.saveManifest(); } return success; } /** * Create a backup of a world from a source path. * * @param worldId The world ID to backup to * @param sourceWorldPath Path to the world folder to backup (e.g., slot0/worlds/defaultWorld/) * @param options Backup options * @returns Backup result */ async createBackup(worldId, sourceWorldPath, options) { await this.ensureLoaded(); const world = this._worlds.get(worldId); if (!world) { return { success: false, error: `World ${worldId} not found` }; } // Check if source path exists if (!fs.existsSync(sourceWorldPath)) { return { success: false, error: `Source world path does not exist: ${sourceWorldPath}` }; } try { const backupId = WorldBackup_1.default.generateId(); const backupFolder = world.folder.ensureFolder(backupId); await backupFolder.ensureExists(); const backupType = options?.backupType || options?.type || IWorldBackupData_1.WorldBackupType.full; const inclusionList = options?.incrementalFileList || options?.inclusionList || []; const addFilesToInclusionList = !options?.incrementalFileList && !options?.inclusionList; // Get source storage const sourceStorage = new NodeStorage_1.default(sourceWorldPath, ""); await sourceStorage.rootFolder.load(true); // Read world name if available let worldName; const levelNameFile = sourceStorage.rootFolder.files["levelname.txt"]; if (levelNameFile) { await levelNameFile.loadContent(false); if (typeof levelNameFile.content === "string") { worldName = levelNameFile.content.trim(); } } // Copy files with deduplication const destStorageRelativePath = StorageUtilities_1.default.ensureStartsWithDelimiter(StorageUtilities_1.default.ensureEndsWithDelimiter(`${worldId}/${backupId}`)); await sourceStorage.rootFolder.copyContentsTo(backupFolder.fullPath, inclusionList, addFilesToInclusionList, this._fileListings, destStorageRelativePath); // Optionally copy behavior_packs if (options?.includeBehaviorPacks) { const bpPath = NodeStorage_1.default.ensureEndsWithDelimiter(sourceWorldPath) + "behavior_packs"; if (fs.existsSync(bpPath)) { const bpStorage = new NodeStorage_1.default(bpPath, ""); await bpStorage.rootFolder.load(true); const bpFolder = backupFolder.ensureFolder("behavior_packs"); await bpFolder.ensureExists(); await bpStorage.rootFolder.copyContentsTo(bpFolder.fullPath, inclusionList, addFilesToInclusionList, this._fileListings, destStorageRelativePath + "behavior_packs/"); } } // Optionally copy resource_packs if (options?.includeResourcePacks) { const rpPath = NodeStorage_1.default.ensureEndsWithDelimiter(sourceWorldPath) + "resource_packs"; if (fs.existsSync(rpPath)) { const rpStorage = new NodeStorage_1.default(rpPath, ""); await rpStorage.rootFolder.load(true); const rpFolder = backupFolder.ensureFolder("resource_packs"); await rpFolder.ensureExists(); await rpStorage.rootFolder.copyContentsTo(rpFolder.fullPath, inclusionList, addFilesToInclusionList, this._fileListings, destStorageRelativePath + "resource_packs/"); } } // Calculate stats let totalBytes = 0; let newFiles = 0; let deduplicatedFiles = 0; for (const file of inclusionList) { if (file.sourcePath) { deduplicatedFiles++; } else { newFiles++; if (file.size) { totalBytes += file.size; } } } // Create metadata const metadata = { id: backupId, worldId: worldId, createdAt: new Date().toISOString(), backupType: backupType, configurationHash: options?.configurationHash, serverVersion: options?.serverVersion, sizeBytes: totalBytes, fileCount: inclusionList.length, deduplicatedFileCount: deduplicatedFiles, worldName: worldName, notes: options?.notes, files: inclusionList, }; // Create and save backup const backup = await WorldBackup_1.default.create(metadata, world.folder); world.registerBackup(backup); await world.save(); await this.saveManifest(); Log_1.default.message(`Created backup ${backupId} for world ${worldId}: ${inclusionList.length} files, ${newFiles} new, ${deduplicatedFiles} deduplicated`); return { success: true, backupId: backupId, backupPath: backupFolder.fullPath, stats: { totalFiles: inclusionList.length, newFiles: newFiles, deduplicatedFiles: deduplicatedFiles, totalBytes: totalBytes, savedBytes: 0, // TODO: Calculate actual savings }, }; } catch (e) { Log_1.default.error(`Failed to create backup for world ${worldId}: ${e.message}`); return { success: false, error: e.message }; } } /** * List backups for a world. */ async listBackups(worldId) { await this.ensureLoaded(); const world = this._worlds.get(worldId); if (!world) { return []; } return await world.loadBackups(); } /** * Get a specific backup. */ async getBackup(worldId, backupId) { await this.ensureLoaded(); const world = this._worlds.get(worldId); if (!world) { return undefined; } return await world.getBackup(backupId); } /** * Restore a backup to a target path. */ async restoreBackup(worldId, backupId, targetPath, clearTarget) { await this.ensureLoaded(); const backup = await this.getBackup(worldId, backupId); if (!backup) { return { success: false, error: `Backup ${backupId} not found in world ${worldId}` }; } return await backup.restore({ targetPath, clearTarget }, this._containerFolder); } /** * Delete a backup. */ async deleteBackup(worldId, backupId) { await this.ensureLoaded(); const world = this._worlds.get(worldId); if (!world) { return false; } const success = await world.deleteBackup(backupId); if (success) { await this.saveManifest(); } return success; } /** * Export a backup as .mcworld file. */ async exportMcWorld(worldId, backupId, outputPath, worldName) { await this.ensureLoaded(); const backup = await this.getBackup(worldId, backupId); if (!backup) { return { success: false, error: `Backup ${backupId} not found in world ${worldId}` }; } return await backup.exportMcWorld({ outputPath, worldName }, this._containerFolder); } /** * Prune old backups for a world. */ async pruneBackups(worldId, keepCount) { await this.ensureLoaded(); const world = this._worlds.get(worldId); if (!world) { return 0; } const deleted = await world.pruneBackups(keepCount); if (deleted > 0) { await this.saveManifest(); } return deleted; } /** * Get or create a world for a given friendly name. * If a world with this name exists, returns it. Otherwise creates a new one. */ async getOrCreateWorld(friendlyName, configurationHash) { await this.ensureLoaded(); const existing = await this.getWorldByName(friendlyName); if (existing) { return existing; } return await this.createWorld(friendlyName, configurationHash); } /** * Get the latest backup for a world. */ async getLatestBackup(worldId) { await this.ensureLoaded(); const world = this._worlds.get(worldId); if (!world) { return undefined; } return await world.getLatestBackup(); } /** * Ensure the manager is loaded. */ async ensureLoaded() { if (!this._isLoaded) { await this.load(); } } /** * Reload the manager (refresh from disk). */ async reload() { this._isLoaded = false; this._worlds.clear(); this._fileListings = {}; await this.load(); } } exports.default = WorldBackupManager;