feat:文件列表与依赖列表

This commit is contained in:
Tianpao
2025-10-05 19:36:21 +08:00
parent f7cd96af43
commit ce2d44689d
11 changed files with 395 additions and 11 deletions

3
.gitignore vendored
View File

@@ -133,6 +133,5 @@ ndist
.pnp.* .pnp.*
# test files # test files
config.json
*.zip *.zip
*.mrpack
instance/

14
config.example.json Normal file
View File

@@ -0,0 +1,14 @@
{
"database":{
"host":"string",
"port":0,
"user":"string",
"database":"string",
"password":"string"
},
"express":{
"port":0,
"host":"",
"logger":true
}
}

View File

@@ -17,11 +17,13 @@
"test": "tsc&&node dist/main.js" "test": "tsc&&node dist/main.js"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.3",
"@types/node": "^24.6.2", "@types/node": "^24.6.2",
"@types/pg": "^8.15.5",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"express": "^5.1.0", "express": "^5.1.0",
"postgres": "^3.4.7" "pg": "^8.16.3"
} }
} }

208
pnpm-lock.yaml generated
View File

@@ -11,22 +11,64 @@ importers:
express: express:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.1.0 version: 5.1.0
postgres: pg:
specifier: ^3.4.7 specifier: ^8.16.3
version: 3.4.7 version: 8.16.3
devDependencies: devDependencies:
'@types/express':
specifier: ^5.0.3
version: 5.0.3
'@types/node': '@types/node':
specifier: ^24.6.2 specifier: ^24.6.2
version: 24.6.2 version: 24.6.2
'@types/pg':
specifier: ^8.15.5
version: 8.15.5
typescript: typescript:
specifier: ^5.9.3 specifier: ^5.9.3
version: 5.9.3 version: 5.9.3
packages: packages:
'@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/express-serve-static-core@5.0.7':
resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==}
'@types/express@5.0.3':
resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==}
'@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/node@24.6.2': '@types/node@24.6.2':
resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==} resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==}
'@types/pg@8.15.5':
resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==}
'@types/qs@6.14.0':
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
'@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
'@types/send@0.17.5':
resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==}
'@types/send@1.2.0':
resolution: {integrity: sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==}
'@types/serve-static@1.15.9':
resolution: {integrity: sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==}
accepts@2.0.0: accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -212,9 +254,55 @@ packages:
path-to-regexp@8.3.0: path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
postgres@3.4.7: pg-cloudflare@1.2.7:
resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
engines: {node: '>=12'}
pg-connection-string@2.9.1:
resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
pg-pool@3.10.1:
resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==}
peerDependencies:
pg: '>=8.0'
pg-protocol@1.10.3:
resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
pg@8.16.3:
resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==}
engines: {node: '>= 16.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
postgres-bytea@1.0.0:
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
engines: {node: '>=0.10.0'}
postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
proxy-addr@2.0.7: proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
@@ -269,6 +357,10 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
statuses@2.0.1: statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -304,12 +396,67 @@ packages:
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
snapshots: snapshots:
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
'@types/node': 24.6.2
'@types/connect@3.4.38':
dependencies:
'@types/node': 24.6.2
'@types/express-serve-static-core@5.0.7':
dependencies:
'@types/node': 24.6.2
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 1.2.0
'@types/express@5.0.3':
dependencies:
'@types/body-parser': 1.19.6
'@types/express-serve-static-core': 5.0.7
'@types/serve-static': 1.15.9
'@types/http-errors@2.0.5': {}
'@types/mime@1.3.5': {}
'@types/node@24.6.2': '@types/node@24.6.2':
dependencies: dependencies:
undici-types: 7.13.0 undici-types: 7.13.0
'@types/pg@8.15.5':
dependencies:
'@types/node': 24.6.2
pg-protocol: 1.10.3
pg-types: 2.2.0
'@types/qs@6.14.0': {}
'@types/range-parser@1.2.7': {}
'@types/send@0.17.5':
dependencies:
'@types/mime': 1.3.5
'@types/node': 24.6.2
'@types/send@1.2.0':
dependencies:
'@types/node': 24.6.2
'@types/serve-static@1.15.9':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 24.6.2
'@types/send': 0.17.5
accepts@2.0.0: accepts@2.0.0:
dependencies: dependencies:
mime-types: 3.0.1 mime-types: 3.0.1
@@ -506,7 +653,50 @@ snapshots:
path-to-regexp@8.3.0: {} path-to-regexp@8.3.0: {}
postgres@3.4.7: {} pg-cloudflare@1.2.7:
optional: true
pg-connection-string@2.9.1: {}
pg-int8@1.0.1: {}
pg-pool@3.10.1(pg@8.16.3):
dependencies:
pg: 8.16.3
pg-protocol@1.10.3: {}
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.0
postgres-date: 1.0.7
postgres-interval: 1.2.0
pg@8.16.3:
dependencies:
pg-connection-string: 2.9.1
pg-pool: 3.10.1(pg@8.16.3)
pg-protocol: 1.10.3
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.2.7
pgpass@1.0.5:
dependencies:
split2: 4.2.0
postgres-array@2.0.0: {}
postgres-bytea@1.0.0: {}
postgres-date@1.0.7: {}
postgres-interval@1.2.0:
dependencies:
xtend: 4.0.2
proxy-addr@2.0.7: proxy-addr@2.0.7:
dependencies: dependencies:
@@ -595,6 +785,8 @@ snapshots:
side-channel-map: 1.0.1 side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2 side-channel-weakmap: 1.0.2
split2@4.2.0: {}
statuses@2.0.1: {} statuses@2.0.1: {}
statuses@2.0.2: {} statuses@2.0.2: {}
@@ -616,3 +808,5 @@ snapshots:
vary@1.1.2: {} vary@1.1.2: {}
wrappy@1.0.2: {} wrappy@1.0.2: {}
xtend@4.0.2: {}

View File

View File

@@ -0,0 +1,110 @@
import { createHash } from "node:crypto";
import { Pool } from "pg";
export class VersionListController {
versionCache: Version[];
private pool: Pool;
hashList: string[];
constructor(pool: Pool) {
this.pool = pool;
this.versionCache = [];
this.hashList = ["md5", "sha1", "sha256"];
this._refreshCache(); // 初始化缓存
setInterval(this._refreshCache, 1000 * 60 * 60 * 2); // 2小时刷新缓存
}
async getVersionList() {
if (this.versionCache.length === 0) {
await this._refreshCache();
}
console.log("use cache");
return this.versionCache;
}
private async _refreshCache() {
const arr: any[] = [];
const result = (
await this.pool.query(`
SELECT mcversion."version",
mcversion."Type",
mcversion."Date",
array_agg(DISTINCT variation.arch) as arch,
variation."OSbuild"
FROM variation
LEFT JOIN mcversion ON mcversion.id = variation."MCVId"
GROUP BY mcversion."version", mcversion."Type", mcversion."Date", variation."OSbuild"
ORDER BY "Date" DESC
`)
).rows;
result.forEach((row: VersionPool) => {
const url: string[] = [];
for (const arch of row.arch) {
url.push(`./mc/version/${row.version}/${arch}`);
arr.push({
id: row.version,
type: row.Type,
BuildType: "UWP",
Arch: ["x64", "x86"],
url,
time: row.Date,
});
}
});
this.versionCache = arr;
}
async getLibraries(id: string, arch: string) {
const archs = ["x64", "x86", "arm"];
if (
!archs.includes(arch) ||
this.versionCache.filter((v) => v.id === id).length === 0
) {
return;
}
const r = (
await this.pool.query(
`
SELECT variation.id
FROM variation
LEFT JOIN mcversion ON mcversion.id = variation."MCVId"
WHERE mcversion."versionHash" = $1
AND variation.arch = $2
LIMIT 1
`,
[createHash("sha256").update(id).digest(), arch]
)
).rows; // 获取版本id
const data = (
await this.pool.query(
`
SELECT "filePathName"."pathName",
"filesHash"."size",
${this.hashList.map(hash => `encode("filesHash"."${hash}", 'hex') as "${hash}"`).join(', ')}
FROM variation_files_data
LEFT JOIN files ON files.id = variation_files_data."filesId"
LEFT JOIN "filePathName" ON "filePathName".id = files."pathNameId"
LEFT JOIN "filesHash" ON "filesHash".id = files."hashId"
WHERE variation_files_data."variationId" = $1
`,
[r[0].id]
)
).rows;
console.log(data);
return data;
}
}
interface VersionPool {
id: number;
version: string;
Type: string;
Date: string;
arch: string;
}
interface Version {
id: string;
type: string;
url: string;
time: string;
}

View File

@@ -0,0 +1,10 @@
import express from "express";
import config from "./utils/config.js";
import versionlistRouter from "./router/versionlist.router.js";
const app = express();
app.use('/mc',versionlistRouter)
app.listen(config.express.port,()=>{
console.log(`server is running on port ${config.express.port}`);
})

View File

View File

@@ -0,0 +1,23 @@
import { Router, Request, Response } from "express";
import { VersionListController } from "../controller/versionlist.controller.js";
import pool from "../utils/pgsql.js";
const versionListController = new VersionListController(pool);
const router = Router();
router.get("/version_manifest", async (req: Request, res: Response) => {
const version = await versionListController.getVersionList();
res.status(200).json({ version });
});
router.get("/version/:id/:arch", async (req: Request, res: Response) => {
const version = req.params.id;
const arch = req.params.arch;
const libraries = await versionListController.getLibraries(version,arch);
if(!libraries){
res.sendStatus(404);
return;
}
res.status(200).json({ libraries });
})
export default router;

22
src/utils/config.ts Normal file
View File

@@ -0,0 +1,22 @@
import fs from "fs";
interface Config {
database: {
host: string;
port: number;
user: string;
database: string;
password: string;
},
express:{
port:number;
logger:boolean;
}
}
const config:Config = (() => {
if (!fs.existsSync(`./config.json`)) {
throw new Error(`config is not defined`);
}
return JSON.parse(fs.readFileSync(`./config.json`, "utf8"));
})();
export default config;

10
src/utils/pgsql.ts Normal file
View File

@@ -0,0 +1,10 @@
import { Pool } from "pg";
import config from "./config.js";
const pool = new Pool({
host: config.database.host,
port: config.database.port,
user: config.database.user,
database: config.database.database,
password: config.database.password,
});
export default pool;