UNPKG

@origami-minecraft/stable

Version:

Origami is a terminal-first Minecraft launcher that supports authentication, installation, and launching of Minecraft versions — with built-in support for Microsoft accounts, mod loaders, profile management, and more. Designed for power users, modders, an

347 lines (281 loc) 10.2 kB
import fs, { copyFileSync, rename, unlinkSync, writeFile } from "fs-extra"; import envPaths from "../tools/envs"; import path, { join } from "path"; import chokidar from "chokidar"; import { Metadata } from "../../types/launcher"; import { platform } from "os"; import pLimit, { LimitFunction } from "p-limit"; import { logger, progress } from "../game/launch/handler"; import * as tar from 'tar'; import LauncherProfileManager from "../tools/launcher"; import LauncherOptionsManager from "../game/launch/options"; import { readdir, rm, unlink } from "fs/promises"; import AdmZip from "adm-zip"; import { logPopupError } from "../tools/logger"; import zlib from "zlib"; import { Readable, Transform } from "stream"; import { _temp_safe } from "./download"; import { pipeline } from "stream/promises"; import { promisify } from "util"; export function ensureDir(dir: string) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } export function cleanDir(dir: string) { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } } export function moveFileSync(oldPath: string, newPath: string) { copyFileSync(oldPath, newPath); unlinkSync(oldPath); } export function localpath(isCache: boolean = false) { let folder = isCache ? envPaths('Origami-Cache').temp : envPaths('Origami-Data').data; ensureDir(folder); return folder; }; export function minecraft_dir(origami_data?: boolean) { let mc = envPaths('.minecraft').config; ensureDir(mc); ensureDir(path.join(mc, "versions")); if(origami_data) { let data = path.join(localpath(), '.data'); ensureDir(data); return data; } return mc; }; let _isSyncing = false; export function sync_minecraft_data_dir(version: string, options?: boolean) { if (_isSyncing) return minecraft_dir(); _isSyncing = true; try { const rootDir = minecraft_dir(); const mc = path.join(rootDir, 'versions', version); const data = path.join(mc, 'data'); const profile_manager = new LauncherProfileManager(); const settings = new LauncherOptionsManager(); const current_profile = profile_manager.getSelectedProfile(); settings.setProfile(current_profile); if (settings.getFixedOptions().universal_game_dir && !options) { return rootDir; } ensureDir(mc); ensureDir(data); return data; } finally { _isSyncing = false; } } export async function async_minecraft_data_dir(version: string): Promise<string> { const newDir = sync_minecraft_data_dir(version); const legacyDir = path.join(minecraft_dir(), 'origami_files', 'instances', version); if (!(await fs.pathExists(legacyDir))) { return newDir; } const files = await collectFiles(legacyDir); const progress = logger.progress(); const task = progress.create(`Migrating ${version}`, files.length); progress.start(); await fs.ensureDir(newDir); for (let i = 0; i < files.length; i++) { const relPath = path.relative(legacyDir, files[i]); const destPath = path.join(newDir, relPath); await fs.ensureDir(path.dirname(destPath)); await fs.copy(files[i], destPath); task?.increment(); } await fs.remove(legacyDir); task?.stop(false); logger.success(`Migration complete for version ${version}.`); return newDir; } export async function collectFiles(dir: string): Promise<string[]> { const entries = await fs.readdir(dir, { withFileTypes: true }); const files: string[] = []; for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { const nested = await collectFiles(fullPath); files.push(...nested); } else { files.push(fullPath); } } return files; } export function printVersion () { let package_json = path.join(__dirname, '..', '..', '..', 'package.json'); if (fs.existsSync(package_json)) { const { version } = JSON.parse(fs.readFileSync(package_json, { encoding: "utf-8" })); return version; } else { return "LATEST" } } export function waitForFolder(metadata: Metadata, id: string) { const versionsDir = path.join(minecraft_dir(), 'versions'); function watchForVersion(version: string, onFound: (versionFolder: string) => void) { const watcher = chokidar.watch(versionsDir, { depth: 1, ignoreInitial: true, }); watcher.on('addDir', (newPath) => { const name = path.basename(newPath).toLowerCase(); if (name.includes(version.toLowerCase()) && name.includes(metadata.name.toLowerCase())) { watcher.close(); onFound(newPath); } }); } return new Promise<string>((resolve) => { watchForVersion(id, (versionFolder) => { console.log(`📁 Detected ${metadata.name} version folder: ${versionFolder}`); resolve(versionFolder); }); }); } export function valid_string(input: any) { return typeof input === 'string'; } export function valid_boolean(input: any) { return typeof input === 'boolean'; } export function parse_input(input: string | boolean | string[]): string | boolean { if(valid_boolean(input)) return input; else if(valid_string(input)) return input; return input.join(' '); } export function getSafeConcurrencyLimit(): number { const platform_ = platform(); switch (platform_) { case 'win32': return 32; case 'darwin': return 16; case 'linux': return 64; default: return 16; } } export async function limitedAll<T>( tasks: (() => Promise<T>)[] | Promise<T>[], limit: LimitFunction = pLimit(getSafeConcurrencyLimit()) ): Promise<T[]> { const wrappedTasks = tasks.map(task => typeof task === 'function' ? limit(task) : limit(() => task) ); return Promise.all(wrappedTasks); } export async function moveFolderContents(srcFolder: string, destFolder: string) { const entries = await fs.readdir(srcFolder); for (const entry of entries) { const srcPath = path.join(srcFolder, entry); const destPath = path.join(destFolder, entry); await fs.move(srcPath, destPath, { overwrite: true }); } } export function sanitizePathSegment(input: string): string { return input .replaceAll(/[<>:"/\\|?*\x00-\x1F]/g, '_') .replaceAll(/\s+/g, '_') .replaceAll(' ', '_') .trim(); } export function jsonParser(str: string) { try { return JSON.parse(str); } catch(_) { return {}; } } export async function cleanAfterInstall(dir: string) { await new Promise((res) => setTimeout(res, 100)); let mc = await readdir(minecraft_dir(), { withFileTypes: true }); let logs = mc.filter(v => !v.isDirectory() && v.name.endsWith('.log')); await rm(dir, { recursive: true, force: true }); await Promise.all(logs.map(async(log) => { let log_path = path.join(minecraft_dir(), log.name); let logs_path = path.join(minecraft_dir(), 'logs'); ensureDir(logs_path); return await rename(log_path, path.join(logs_path, log.name)); })); } export async function extractZip(zip_file: string, target: string) { try { const zip = new AdmZip(zip_file); const entries = zip.getEntries(); let start = false; const prog = progress.create(`${path.basename(zip_file)}`, entries.length, true); for (const entry of entries) { if(!start) { progress.start(); start = true; } const dest = path.join(target, entry.entryName); if(entry.isDirectory) { ensureDir(dest); prog?.increment(); continue; } let data = await new Promise<Buffer>((res, rej) => entry.getDataAsync((data, err) => { if(err) return rej(new Error(err)); res(data); })); ensureDir(path.dirname(dest)); await writeFile(dest, data); prog?.increment(); } progress.stopAll(); return; } catch (e) { await logPopupError('Extraction Error', `🌸 Uh-oh! Something went wrong while unpacking your files:\n${(e as Error).message}`, true); return; } } function isGzipped(file: string) { return file.endsWith(".tar.gz") || file.endsWith(".tgz"); } export async function extractTar(tarFile: string, target: string) { try { ensureDir(target); const gzip = isGzipped(tarFile); let tarPath = tarFile; let entries: string[] = []; await tar.t({ file: tarPath, gzip, onentry: (entry) => { entries.push(entry.path) }, onwarn(code, message, data) { logger.warn(`[TAR WARN] (${code}) - ${message} - Tar Code: ${data.tarCode || '<unknown>'} - File: ${data.file || '<unknown>'}`) }, }); let start = false; const prog = progress.create(`${path.basename(tarFile)}`, entries.length, true); await tar.x({ file: tarPath, cwd: target, gzip, onentry: () => { if(!start) { progress.start(); start = true; } prog?.increment(); }, onwarn(code, message, data) { logger.warn(`[TAR WARN] (${code}) - ${message} - Tar Code: ${data.tarCode || '<unknown>'} - File: ${data.file || '<unknown>'}`) }, }) progress.stopAll(); return; } catch (e) { await logPopupError( 'Extraction Error', `🌸 Uh-oh! Something went wrong while unpacking your files:\n${(e as Error).message}`, true ); return; } }