const AdmZip = require('adm-zip') const fs = require('fs-extra') const got = require('got') const os = require('os') const path = require('path') const { pipeline } = require('stream/promises') const ConfigManager = require('./configmanager') function getProfileBaseDirectory(profileId){ return path.join(ConfigManager.getDataDirectory(), 'profiles', profileId) } function getProfileDownloadDirectory(profileId){ return path.join(getProfileBaseDirectory(profileId), 'downloads') } function getProfileCacheFile(profileId, fileName){ return path.join(getProfileDownloadDirectory(profileId), fileName) } function getServerBundleDirectory(profile){ return path.join(getProfileBaseDirectory(profile.id), profile.serverDirectoryName || 'server') } function isRemoteSource(source){ return /^https?:\/\//i.test(source) } function resolveLocalSource(source){ if(source == null){ return null } if(source.startsWith('file://')){ return decodeURIComponent(source.substring('file://'.length)) } return path.resolve(source) } async function downloadSourceToFile(source, destination){ await fs.ensureDir(path.dirname(destination)) if(isRemoteSource(source)){ await pipeline( got.stream(source), fs.createWriteStream(destination) ) return destination } const localSource = resolveLocalSource(source) const localStat = await fs.stat(localSource) if(localStat.isDirectory()){ throw new Error(`Directory source is not supported for file cache: ${localSource}`) } await fs.copy(localSource, destination, { overwrite: true }) return destination } async function extractZipToDirectory(zipPath, destination){ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'launcher-assets-')) try { await fs.ensureDir(tempDir) new AdmZip(zipPath).extractAllTo(tempDir, true) const rootEntries = (await fs.readdir(tempDir)).filter((entry) => entry !== '__MACOSX') await fs.remove(destination) if(rootEntries.length === 1){ const onlyEntry = path.join(tempDir, rootEntries[0]) const entryStat = await fs.stat(onlyEntry) if(entryStat.isDirectory()){ await fs.copy(onlyEntry, destination, { overwrite: true }) return } } await fs.copy(tempDir, destination, { overwrite: true }) } finally { await fs.remove(tempDir) } } async function ensureCachedFile(profileId, source, fileName){ const cachePath = getProfileCacheFile(profileId, fileName) await downloadSourceToFile(source, cachePath) return cachePath } async function ensureServerWorldInstalled(profile, serverDirectory){ if(!profile.worldArchiveUrl){ return null } const targetWorldDirectory = path.join(serverDirectory, 'world') if(isRemoteSource(profile.worldArchiveUrl) || profile.worldArchiveUrl.endsWith('.zip') || profile.worldArchiveUrl.startsWith('file://')){ const cachePath = await ensureCachedFile(profile.id, profile.worldArchiveUrl, 'world.zip') await extractZipToDirectory(cachePath, targetWorldDirectory) } else { await fs.remove(targetWorldDirectory) await fs.copy(resolveLocalSource(profile.worldArchiveUrl), targetWorldDirectory, { overwrite: true }) } return targetWorldDirectory } exports.getProfileBaseDirectory = getProfileBaseDirectory exports.getServerBundleDirectory = getServerBundleDirectory exports.prefetchProfileAssets = async function(profile){ if(profile.worldArchiveUrl){ await ensureCachedFile(profile.id, profile.worldArchiveUrl, 'world.zip') } if(profile.serverJarUrl){ await ensureCachedFile(profile.id, profile.serverJarUrl, 'server.jar') } const currentState = ConfigManager.getLibraryProfileAssetState(profile.id) ConfigManager.setLibraryProfileAssetState(profile.id, { ...currentState, prefetchedAt: new Date().toISOString() }) ConfigManager.save() } exports.ensureWorldInstalled = async function(profile, serverId){ if(!profile.worldArchiveUrl || !profile.worldDirectoryName){ return null } const savesDirectory = path.join(ConfigManager.getInstanceDirectory(), serverId, 'saves') const targetWorldDirectory = path.join(savesDirectory, profile.worldDirectoryName) await fs.ensureDir(savesDirectory) if(isRemoteSource(profile.worldArchiveUrl) || profile.worldArchiveUrl.endsWith('.zip') || profile.worldArchiveUrl.startsWith('file://')){ const cachePath = await ensureCachedFile(profile.id, profile.worldArchiveUrl, 'world.zip') await extractZipToDirectory(cachePath, targetWorldDirectory) } else { await fs.remove(targetWorldDirectory) await fs.copy(resolveLocalSource(profile.worldArchiveUrl), targetWorldDirectory, { overwrite: true }) } ConfigManager.setLibraryQuickPlayWorld(profile.id, profile.worldDirectoryName) ConfigManager.setLibraryProfileAssetState(profile.id, { worldInstalledAt: new Date().toISOString(), worldInstalledServerId: serverId, worldDirectoryName: profile.worldDirectoryName }) ConfigManager.save() return targetWorldDirectory } exports.ensureServerJarInstalled = async function(profile){ if(!profile.serverJarUrl){ return null } const targetDirectory = getServerBundleDirectory(profile) const targetJarPath = path.join(targetDirectory, 'server.jar') await fs.ensureDir(targetDirectory) const cachePath = await ensureCachedFile(profile.id, profile.serverJarUrl, 'server.jar') await fs.copy(cachePath, targetJarPath, { overwrite: true }) await ensureServerWorldInstalled(profile, targetDirectory) ConfigManager.setLibraryProfileAssetState(profile.id, { serverInstalledAt: new Date().toISOString(), serverDirectory: targetDirectory, serverJarPath: targetJarPath }) ConfigManager.save() return { serverDirectory: targetDirectory, serverJarPath: targetJarPath } } exports.ensureServerBundleInstalled = exports.ensureServerJarInstalled exports.prepareProfileForLaunch = async function(profile, serverId){ return exports.ensureWorldInstalled(profile, serverId) } exports.ensureServerWorldInstalled = ensureServerWorldInstalled