@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
539 lines (538 loc) • 19.5 kB
JavaScript
"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;