Files
DeEarthX-V3/backend/src/utils/DeEarth.ts
2026-01-01 01:21:20 +08:00

209 lines
6.6 KiB
TypeScript

import fs from "node:fs";
import crypto from "node:crypto";
import { Azip } from "./ziplib.js";
import got from "got";
import { Utils } from "./utils.js";
import config from "./config.js";
import { logger } from "./logger.js";
interface IMixinFile {
name: string;
data: string;
}
interface IFileInfo {
filename: string;
hash: string;
mixins: IMixinFile[];
}
interface IHashResponse {
[hash: string]: { project_id: string };
}
interface IProjectInfo {
id: string;
client_side: string;
server_side: string;
}
export class DeEarth {
private movePath: string;
private modsPath: string;
private files: IFileInfo[];
private utils: Utils;
constructor(modsPath: string, movePath: string) {
this.utils = new Utils();
this.movePath = movePath;
this.modsPath = modsPath;
this.files = [];
logger.debug("DeEarth instance created", { modsPath, movePath });
}
async Main(): Promise<void> {
logger.info("Starting DeEarth process");
if (!fs.existsSync(this.movePath)) {
logger.debug("Creating target directory", { path: this.movePath });
fs.mkdirSync(this.movePath, { recursive: true });
}
await this.getFilesInfo();
const clientSideMods = await this.identifyClientSideMods();
await this.moveClientSideMods(clientSideMods);
logger.info("DeEarth process completed");
}
private async identifyClientSideMods(): Promise<string[]> {
const clientMods: string[] = [];
if (config.filter.hashes) {
logger.info("Starting hash check for client-side mods");
clientMods.push(...await this.checkHashesForClientMods());
}
if (config.filter.mixins) {
logger.info("Starting mixins check for client-side mods");
clientMods.push(...await this.checkMixinsForClientMods());
}
const uniqueMods = [...new Set(clientMods)];
logger.info("Client-side mods identified", { count: uniqueMods.length, mods: uniqueMods });
return uniqueMods;
}
private async checkHashesForClientMods(): Promise<string[]> {
const hashToFilename = new Map<string, string>();
const hashes = this.files.map(file => {
hashToFilename.set(file.hash, file.filename);
return file.hash;
});
logger.debug("Checking mod hashes with Modrinth API", { fileCount: this.files.length });
try {
const fileInfoResponse = await got.post(`${this.utils.modrinth_url}/v2/version_files`, {
headers: { "User-Agent": "DeEarth", "Content-Type": "application/json" },
json: { hashes, algorithm: "sha1" }
}).json<IHashResponse>();
const projectIdToFilename = new Map<string, string>();
const projectIds = Object.entries(fileInfoResponse)
.map(([hash, info]) => {
const filename = hashToFilename.get(hash);
if (filename) projectIdToFilename.set(info.project_id, filename);
return info.project_id;
});
const projectsResponse = await got.get(`${this.utils.modrinth_url}/v2/projects?ids=${JSON.stringify(projectIds)}`, {
headers: { "User-Agent": "DeEarth" }
}).json<IProjectInfo[]>();
const clientMods = projectsResponse
.filter(p => p.client_side === "required" && p.server_side === "unsupported")
.map(p => projectIdToFilename.get(p.id))
.filter(Boolean) as string[];
logger.debug("Hash check completed", { count: clientMods.length });
return clientMods;
} catch (error: any) {
logger.error("Hash check failed", error);
return [];
}
}
private async checkMixinsForClientMods(): Promise<string[]> {
const clientMods: string[] = [];
for (const file of this.files) {
for (const mixin of file.mixins) {
try {
const config = JSON.parse(mixin.data);
if (!config.mixins?.length && config.client?.length > 0 && !file.filename.includes("lib")) {
clientMods.push(file.filename);
break;
}
} catch (error: any) {
logger.warn("Failed to parse mixin config", { filename: file.filename, mixin: mixin.name, error: error.message });
}
}
}
logger.debug("Mixins check completed", { count: clientMods.length });
return [...new Set(clientMods)];
}
private async moveClientSideMods(clientMods: string[]): Promise<void> {
if (!clientMods.length) {
logger.info("No client-side mods to move");
return;
}
let successCount = 0, errorCount = 0;
for (const sourcePath of clientMods) {
try {
const targetPath = `${this.movePath}/${sourcePath.replace(this.modsPath, "").replace(/^\/+/, "")}`;
logger.info("Moving file", { source: sourcePath, target: targetPath });
await fs.promises.rename(sourcePath, targetPath);
successCount++;
} catch (error: any) {
logger.error("Failed to move file", { source: sourcePath, error: error.message });
errorCount++;
}
}
logger.info("File movement completed", { total: clientMods.length, success: successCount, error: errorCount });
}
private async getFilesInfo(): Promise<void> {
const jarFiles = this.getJarFiles();
logger.info("Getting file information", { fileCount: jarFiles.length });
for (const jarFilename of jarFiles) {
const fullPath = `${this.modsPath}/${jarFilename}`;
try {
const fileData = fs.readFileSync(fullPath);
const mixins = await this.extractMixins(fileData);
this.files.push({
filename: fullPath,
hash: crypto.createHash('sha1').update(fileData).digest('hex'),
mixins
});
logger.debug("File processed", { filename: fullPath, mixinCount: mixins.length });
} catch (error: any) {
logger.error("Error processing file", { filename: fullPath, error: error.message });
}
}
logger.debug("File information collection completed", { processedFiles: this.files.length });
}
private getJarFiles(): string[] {
if (!fs.existsSync(this.modsPath)) fs.mkdirSync(this.modsPath, { recursive: true });
return fs.readdirSync(this.modsPath).filter(f => f.endsWith(".jar"));
}
private async extractMixins(jarData: Buffer): Promise<IMixinFile[]> {
const mixins: IMixinFile[] = [];
const zipEntries = Azip(jarData);
await Promise.all(zipEntries.map(async (entry) => {
if (entry.entryName.endsWith(".mixins.json") && !entry.entryName.includes("/")) {
try {
const data = await entry.getData();
mixins.push({ name: entry.entryName, data: data.toString() });
} catch (error: any) {
logger.error(`Error extracting ${entry.entryName}`, error);
}
}
}));
return mixins;
}
}