feat:星系广场的彻底实现
This commit is contained in:
@@ -80,7 +80,7 @@ export class Dex {
|
||||
}
|
||||
|
||||
private async _processModpack(buffer: Buffer, filename?: string): Promise<Buffer> {
|
||||
if (!filename || (!filename.endsWith('.zip') && !filename.endsWith('.mrpack'))) {
|
||||
if (!filename || !filename.endsWith('.zip')) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export class Dex {
|
||||
resolve(zipfile);
|
||||
});
|
||||
}) as Promise<yauzl.ZipFile>);
|
||||
|
||||
logger.info("Modpack zip file detected,It is a PCL packege,try to extract modpack.mrpack");
|
||||
return new Promise((resolve, reject) => {
|
||||
let mrpackBuffer: Buffer | null = null;
|
||||
let hasProcessed = false;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Config, IConfig } from "./utils/config.js";
|
||||
import { Dex } from "./Dex.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
import { checkJava, JavaCheckResult } from "./utils/utils.js";
|
||||
import { Galaxy } from "./galaxy.js";
|
||||
export class Core {
|
||||
private config: IConfig;
|
||||
private readonly app: Application;
|
||||
@@ -16,16 +17,18 @@ export class Core {
|
||||
private readonly upload: multer.Multer;
|
||||
private task: {} = {};
|
||||
dex: Dex;
|
||||
galaxy: Galaxy;
|
||||
constructor(config: IConfig) {
|
||||
this.config = config
|
||||
this.app = express();
|
||||
this.server = createServer(this.app);
|
||||
this.upload = multer()
|
||||
this.ws = new WebSocketServer({ server: this.server })
|
||||
this.ws.on("connection",(e)=>{
|
||||
this.wsx = e
|
||||
})
|
||||
this.dex = new Dex(this.ws)
|
||||
this.galaxy = new Galaxy()
|
||||
this.upload = multer();
|
||||
}
|
||||
|
||||
private async javachecker() {
|
||||
@@ -129,6 +132,8 @@ export class Core {
|
||||
res.status(500).json({ status: 500, message: "Failed to update config" });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.use("/galaxy", this.galaxy.getRouter());
|
||||
}
|
||||
|
||||
public async start() {
|
||||
|
||||
84
backend/src/galaxy.ts
Normal file
84
backend/src/galaxy.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import express from "express";
|
||||
import toml from "smol-toml";
|
||||
import multer, { Multer } from "multer";
|
||||
import AdmZip from "adm-zip";
|
||||
import { logger } from "./utils/logger.js";
|
||||
import got, { Got } from "got";
|
||||
|
||||
export class Galaxy {
|
||||
private readonly upload: multer.Multer;
|
||||
got: Got;
|
||||
constructor() {
|
||||
this.upload = multer()
|
||||
this.got = got.extend({
|
||||
prefixUrl: "https://galaxy.tianpao.top/",
|
||||
//prefixUrl: "http://localhost:3000/",
|
||||
headers: {
|
||||
"User-Agent": "DeEarthX",
|
||||
},
|
||||
responseType: "json",
|
||||
});
|
||||
}
|
||||
getRouter() {
|
||||
const router = express.Router();
|
||||
router.use(express.json()); // 解析 JSON 请求体
|
||||
router.post("/upload",this.upload.array("files"), (req, res) => {
|
||||
const files = req.files as Express.Multer.File[];
|
||||
if(!files || files.length === 0){
|
||||
res.status(400).json({ status: 400, message: "No file uploaded" });
|
||||
return;
|
||||
}
|
||||
const modids = this.getModids(files);
|
||||
logger.info("Uploaded modids", modids);
|
||||
res.json({modids}).end();
|
||||
});
|
||||
router.post("/submit/:type",(req,res)=>{
|
||||
const type = req.params.type;
|
||||
if(type !== "server" && type !== "client"){
|
||||
res.status(400).json({ status: 400, message: "Invalid type" });
|
||||
return;
|
||||
}
|
||||
const modid = req.body.modids as string;
|
||||
if(!modid){
|
||||
res.status(400).json({ status: 400, message: "No modid provided" });
|
||||
return;
|
||||
}
|
||||
this.got.post(`api/mod/submit/${type}`,{
|
||||
json: {
|
||||
modid,
|
||||
}
|
||||
}).then((response)=>{
|
||||
logger.info(`Submitted modids for ${type}:`, response.body);
|
||||
res.json(response.body).end();
|
||||
}).catch((error)=>{
|
||||
logger.error(`Failed to submit modids for ${type}:`, error);
|
||||
res.status(500).json({ status: 500, message: "Failed to submit modids" });
|
||||
})
|
||||
})
|
||||
return router;
|
||||
}
|
||||
|
||||
getModids(files:Express.Multer.File[]):string[] {
|
||||
let modid:string[] = [];
|
||||
for(const file of files){
|
||||
const zip = new AdmZip(file.buffer);
|
||||
const entries = zip.getEntries();
|
||||
for(const entry of entries){
|
||||
if(entry.entryName.endsWith("mods.toml")){
|
||||
const content = entry.getData().toString("utf8");
|
||||
const config = toml.parse(content) as any;
|
||||
modid.push(config.mods[0].modId as string)
|
||||
}else if(entry.entryName.endsWith("neoforge.mods.toml")){
|
||||
const content = entry.getData().toString("utf8");
|
||||
const config = toml.parse(content) as any;
|
||||
modid.push(config.mods[0].modId as string)
|
||||
}else if(entry.entryName.endsWith("fabric.mod.json")){
|
||||
const content = entry.getData().toString("utf8");
|
||||
const config = JSON.parse(content);
|
||||
modid.push(config.id as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
return modid
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
import fs from "node:fs";
|
||||
import crypto from "node:crypto";
|
||||
import { Azip } from "./ziplib.js";
|
||||
import got from "got";
|
||||
import got, { Got } from "got";
|
||||
import { Utils } from "./utils.js";
|
||||
import config from "./config.js";
|
||||
import { logger } from "./logger.js";
|
||||
import toml from "smol-toml";
|
||||
|
||||
interface IMixinFile {
|
||||
name: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface IInfoFile {
|
||||
name: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface IFileInfo {
|
||||
filename: string;
|
||||
hash: string;
|
||||
mixins: IMixinFile[];
|
||||
infos: IInfoFile[];
|
||||
}
|
||||
|
||||
interface IHashResponse {
|
||||
@@ -27,23 +34,36 @@ interface IProjectInfo {
|
||||
server_side: string;
|
||||
}
|
||||
|
||||
interface checkDexpubForClientMods {
|
||||
serverMods: string[];
|
||||
clientMods: string[];
|
||||
}
|
||||
|
||||
export class DeEarth {
|
||||
private movePath: string;
|
||||
private modsPath: string;
|
||||
private files: IFileInfo[];
|
||||
private utils: Utils;
|
||||
private got: Got;
|
||||
|
||||
constructor(modsPath: string, movePath: string) {
|
||||
this.utils = new Utils();
|
||||
this.movePath = movePath;
|
||||
this.modsPath = modsPath;
|
||||
this.files = [];
|
||||
this.got = got.extend({
|
||||
prefixUrl: "https://galaxy.tianpao.top/",
|
||||
headers: {
|
||||
"User-Agent": "DeEarthX",
|
||||
},
|
||||
responseType: "json",
|
||||
});
|
||||
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 });
|
||||
@@ -52,11 +72,11 @@ export class DeEarth {
|
||||
await this.getFilesInfo();
|
||||
const clientSideMods = await this.identifyClientSideMods();
|
||||
await this.moveClientSideMods(clientSideMods);
|
||||
|
||||
|
||||
logger.info("DeEarth process completed");
|
||||
}
|
||||
|
||||
private async identifyClientSideMods(): Promise<string[]> {
|
||||
private async identifyClientSideMods(): Promise<string[]> { // 识别客户端Mod主函数
|
||||
const clientMods: string[] = [];
|
||||
|
||||
if (config.filter.hashes) {
|
||||
@@ -69,11 +89,67 @@ export class DeEarth {
|
||||
clientMods.push(...await this.checkMixinsForClientMods());
|
||||
}
|
||||
|
||||
if (config.filter.dexpub) {
|
||||
logger.info("Starting dexpub check for client-side mods");
|
||||
const dexpubMods = await this.checkDexpubForClientMods();
|
||||
clientMods.push(...dexpubMods.clientMods);
|
||||
const serverModsListSet = new Set(dexpubMods.serverMods);
|
||||
for(let i=0;i>=clientMods.length - 1;i--){
|
||||
if (serverModsListSet.has(clientMods[i])){
|
||||
clientMods.splice(i,1);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Dexpub check completed", { serverMods: dexpubMods.serverMods, clientMods: dexpubMods.clientMods });
|
||||
}
|
||||
|
||||
const uniqueMods = [...new Set(clientMods)];
|
||||
logger.info("Client-side mods identified", { count: uniqueMods.length, mods: uniqueMods });
|
||||
return uniqueMods;
|
||||
}
|
||||
|
||||
private async checkDexpubForClientMods(): Promise<checkDexpubForClientMods> {
|
||||
const clientMods: string[] = [];
|
||||
const serverMods: string[] = [];
|
||||
const modIds: string[] = [];
|
||||
const map: Map<string, string> = new Map();
|
||||
for (const file of this.files) {
|
||||
for (const info of file.infos) {
|
||||
const config = JSON.parse(info.data);
|
||||
const keys = Object.keys(config);
|
||||
if (keys.includes("id")) {
|
||||
modIds.push(config.id);
|
||||
map.set(config.id, file.filename);
|
||||
}else if(keys.includes("mods")){
|
||||
modIds.push(config.mods[0].modId);
|
||||
map.set(config.mods[0].modId, file.filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
const modids = modIds;
|
||||
const modIdToIsTypeMod = await this.got.post(`api/mod/check`,{
|
||||
json: {
|
||||
modids,
|
||||
}
|
||||
}).json<{[modId: string]: boolean}>()
|
||||
const modIdToIsTypeModKeys = Object.keys(modIdToIsTypeMod);
|
||||
for(const modId of modIdToIsTypeModKeys){
|
||||
if(modIdToIsTypeMod[modId]){
|
||||
const MapData = map.get(modId);
|
||||
if(MapData){
|
||||
clientMods.push(MapData);
|
||||
}
|
||||
}else{
|
||||
const MapData = map.get(modId);
|
||||
if(MapData){
|
||||
serverMods.push(MapData);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info("Galaxy check client-side mods", { count: clientMods.length, mods: clientMods });
|
||||
return { serverMods, clientMods };
|
||||
}
|
||||
|
||||
private async checkHashesForClientMods(): Promise<string[]> {
|
||||
const hashToFilename = new Map<string, string>();
|
||||
const hashes = this.files.map(file => {
|
||||
@@ -164,17 +240,19 @@ export class DeEarth {
|
||||
|
||||
for (const jarFilename of jarFiles) {
|
||||
const fullPath = `${this.modsPath}/${jarFilename}`;
|
||||
|
||||
|
||||
try {
|
||||
const fileData = fs.readFileSync(fullPath);
|
||||
const mixins = await this.extractMixins(fileData);
|
||||
|
||||
const infos = await this.extractModInfo(fileData);
|
||||
|
||||
this.files.push({
|
||||
filename: fullPath,
|
||||
hash: crypto.createHash('sha1').update(fileData).digest('hex'),
|
||||
mixins
|
||||
mixins,
|
||||
infos,
|
||||
});
|
||||
|
||||
|
||||
logger.debug("File processed", { filename: fullPath, mixinCount: mixins.length });
|
||||
} catch (error: any) {
|
||||
logger.error("Error processing file", { filename: fullPath, error: error.message });
|
||||
@@ -189,6 +267,27 @@ export class DeEarth {
|
||||
return fs.readdirSync(this.modsPath).filter(f => f.endsWith(".jar"));
|
||||
}
|
||||
|
||||
private async extractModInfo(jarData: Buffer): Promise<IInfoFile[]> {
|
||||
const infos: IInfoFile[] = [];
|
||||
const zipEntries = Azip(jarData);
|
||||
await Promise.all(zipEntries.map(async (entry) => {
|
||||
try {
|
||||
if (entry.entryName.endsWith("neoforge.mods.toml") || entry.entryName.endsWith("mods.toml")) {
|
||||
const data = await entry.getData();
|
||||
infos.push({ name: entry.entryName, data: JSON.stringify(toml.parse(data.toString())) });
|
||||
} else if (entry.entryName.endsWith("fabric.mod.json")) {
|
||||
const data = await entry.getData();
|
||||
infos.push({ name: entry.entryName, data: data.toString() });
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(`Error extracting ${entry.entryName}`, error);
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
return infos;
|
||||
}
|
||||
|
||||
private async extractMixins(jarData: Buffer): Promise<IMixinFile[]> {
|
||||
const mixins: IMixinFile[] = [];
|
||||
const zipEntries = Azip(jarData);
|
||||
|
||||
@@ -26,7 +26,7 @@ const DEFAULT_CONFIG: IConfig = {
|
||||
},
|
||||
filter: {
|
||||
hashes: true,
|
||||
dexpub: false,
|
||||
dexpub: true,
|
||||
mixins: true,
|
||||
},
|
||||
oaf: true
|
||||
|
||||
Reference in New Issue
Block a user