Skip to content

Commit d88ef78

Browse files
author
Taois
committed
add: 尝试增加ds本地包下载中心
1 parent 30e68c0 commit d88ef78

File tree

6 files changed

+342
-16
lines changed

6 files changed

+342
-16
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ nodejs作为服务端的drpy实现。全面升级异步写法
1111
### 常用超链接
1212

1313
* [本项目主页-免翻](https://github.com/hjdhnx/drpy-node)
14-
* [最新DS本地包-适配皮卡丘](/gh/release)
14+
* ~~[最新DS本地包-适配皮卡丘](/gh/release)~~
15+
* [DS本地包下载中心](/admin/download)
1516
* [接口文档](docs/apidoc.md) | [接口列表如定时任务](docs/apiList.md) |
1617
~~[小猫影视-待对接T4](https://github.com/waifu-project/movie/pull/135)~~
1718
* [代码质量评估工具说明](docs/codeCheck.md) | [DS项目代码评估报告](docs/codeCheckReport.md)

controllers/web.js

Lines changed: 313 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,92 @@
1-
import {readFileSync, existsSync} from 'fs';
1+
import {readFileSync, existsSync, readdirSync, statSync} from 'fs';
2+
import {createReadStream} from 'fs';
3+
import {execSync} from 'child_process';
24
import path from 'path';
5+
import {fileURLToPath} from 'url';
6+
import {createHash} from 'crypto';
37
import {ENV} from '../utils/env.js';
48
import COOKIE from '../utils/cookieManager.js';
9+
import {validateBasicAuth} from '../utils/api_validate.js';
510

611
const COOKIE_AUTH_CODE = process.env.COOKIE_AUTH_CODE || 'drpys';
12+
const IS_VERCEL = process.env.VERCEL;
13+
const DOWNLOAD_AUTH_SECRET = process.env.DOWNLOAD_AUTH_SECRET || 'drpys_download_secret';
14+
15+
const __filename = fileURLToPath(import.meta.url);
16+
const __dirname = path.dirname(__filename);
17+
const projectRootDir = path.dirname(__dirname);
18+
19+
const generateDownloadToken = (filename) => {
20+
const timestamp = Date.now();
21+
const data = `${filename}-${timestamp}-${DOWNLOAD_AUTH_SECRET}`;
22+
const token = createHash('md5').update(data).digest('hex');
23+
return `${token}-${timestamp}`;
24+
};
25+
26+
const validateDownloadToken = (filename, token) => {
27+
if (!token) return false;
28+
const parts = token.split('-');
29+
if (parts.length < 2) return false;
30+
const timestamp = parseInt(parts.pop());
31+
const hash = parts.join('-');
32+
const data = `${filename}-${timestamp}-${DOWNLOAD_AUTH_SECRET}`;
33+
const expectedHash = createHash('md5').update(data).digest('hex');
34+
const now = Date.now();
35+
return hash === expectedHash && (now - timestamp) < 3600000;
36+
};
37+
38+
const findLatestPackage = (projectDir, packageName) => {
39+
try {
40+
const parentDir = path.dirname(projectDir);
41+
const files = readdirSync(parentDir);
42+
43+
const isGreen = packageName.includes('-green');
44+
const ext = packageName.split('.').pop();
45+
const baseName = packageName.replace(/-green\.[^.]+$/, '').replace(/\.[^.]+$/, '');
46+
const pattern = new RegExp(`^${baseName.replace(/\./g, '\\.')}-\\d{8}${isGreen ? '-green' : ''}\\.${ext}`);
47+
48+
console.log(`查找包: ${packageName}, 正则: ${pattern.source}, 父目录: ${parentDir}`);
49+
console.log('目录中的文件:', files.filter(f => f.includes('drpy-node')));
50+
51+
const packageFiles = files
52+
.filter(file => pattern.test(file))
53+
.map(file => {
54+
const filePath = path.join(parentDir, file);
55+
const stats = statSync(filePath);
56+
return {file, filePath, mtime: stats.mtime};
57+
})
58+
.sort((a, b) => b.mtime - a.mtime);
59+
60+
console.log('匹配到的文件:', packageFiles.map(f => f.file));
61+
return packageFiles.length > 0 ? packageFiles[0] : null;
62+
} catch (error) {
63+
console.error('查找包失败:', error.message);
64+
return null;
65+
}
66+
};
67+
68+
const buildPackage = (packageName) => {
69+
try {
70+
let command = 'node package.js';
71+
if (packageName.includes('-green')) {
72+
command += ' -g';
73+
}
74+
if (packageName.includes('.zip')) {
75+
command += ' -z';
76+
}
77+
78+
console.log(`执行打包命令: ${command}, 目录: ${projectRootDir}`);
79+
const output = execSync(command, {cwd: projectRootDir, stdio: 'pipe'});
80+
console.log('打包输出:', output.toString());
81+
const result = findLatestPackage(projectRootDir, packageName);
82+
console.log('打包后查找结果:', result ? result.file : '未找到');
83+
return result;
84+
} catch (error) {
85+
console.error('打包失败:', error.message);
86+
console.error('错误详情:', error.stdout?.toString(), error.stderr?.toString());
87+
throw error;
88+
}
89+
};
790

891
export default (fastify, options, done) => {
992
fastify.get('/admin/encoder', async (request, reply) => {
@@ -75,5 +158,234 @@ export default (fastify, options, done) => {
75158
}
76159
});
77160

161+
fastify.get('/admin/download', {
162+
preHandler: validateBasicAuth
163+
}, async (request, reply) => {
164+
try {
165+
if (IS_VERCEL) {
166+
return reply.code(403).send({
167+
success: false,
168+
message: 'Vercel 环境不支持文件下载功能',
169+
});
170+
}
171+
172+
const projectName = path.basename(projectRootDir);
173+
174+
const files = [
175+
{name: `${projectName}.7z`, desc: '7z 压缩包(标准版)'},
176+
{name: `${projectName}.zip`, desc: 'ZIP 压缩包(标准版)'},
177+
{name: `${projectName}-green.7z`, desc: '7z 压缩包(绿色版,不含[密]文件)'},
178+
{name: `${projectName}-green.zip`, desc: 'ZIP 压缩包(绿色版,不含[密]文件)'}
179+
];
180+
181+
const html = `
182+
<!DOCTYPE html>
183+
<html lang="zh-CN">
184+
<head>
185+
<meta charset="UTF-8">
186+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
187+
<title>下载 ${projectName}</title>
188+
<style>
189+
body {
190+
font-family: Arial, sans-serif;
191+
max-width: 800px;
192+
margin: 50px auto;
193+
padding: 20px;
194+
background-color: #f5f5f5;
195+
}
196+
h1 {
197+
text-align: center;
198+
color: #333;
199+
}
200+
.download-list {
201+
background-color: white;
202+
border-radius: 8px;
203+
padding: 20px;
204+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
205+
}
206+
.download-item {
207+
margin: 10px 0;
208+
padding: 15px;
209+
background-color: #f8f9fa;
210+
border-radius: 4px;
211+
display: flex;
212+
justify-content: space-between;
213+
align-items: center;
214+
}
215+
.download-info {
216+
flex: 1;
217+
}
218+
.download-actions {
219+
display: flex;
220+
gap: 10px;
221+
align-items: center;
222+
}
223+
.download-item a {
224+
text-decoration: none;
225+
font-weight: bold;
226+
padding: 8px 16px;
227+
background-color: #007bff;
228+
color: white;
229+
border-radius: 4px;
230+
transition: background-color 0.3s;
231+
border: none;
232+
cursor: pointer;
233+
}
234+
.download-item a:hover {
235+
background-color: #0056b3;
236+
}
237+
.copy-btn {
238+
text-decoration: none;
239+
font-weight: bold;
240+
padding: 8px 16px;
241+
background-color: #6c757d;
242+
color: white;
243+
border-radius: 4px;
244+
transition: background-color 0.3s;
245+
border: none;
246+
cursor: pointer;
247+
}
248+
.copy-btn:hover {
249+
background-color: #5a6268;
250+
}
251+
.file-type {
252+
color: #666;
253+
font-size: 14px;
254+
}
255+
.toast {
256+
position: fixed;
257+
top: 20px;
258+
right: 20px;
259+
background-color: #28a745;
260+
color: white;
261+
padding: 10px 20px;
262+
border-radius: 4px;
263+
display: none;
264+
z-index: 1000;
265+
}
266+
</style>
267+
</head>
268+
<body>
269+
<h1>${projectName} 下载中心</h1>
270+
<div class="toast" id="toast">链接已复制到剪贴板</div>
271+
<div class="download-list">
272+
${files.map(file => {
273+
const token = generateDownloadToken(file.name);
274+
const downloadUrl = `/admin/download/${file.name}?auth=${token}`;
275+
return `
276+
<div class="download-item">
277+
<div class="download-info">
278+
<strong>${file.name}</strong>
279+
<div class="file-type">${file.desc}</div>
280+
</div>
281+
<div class="download-actions">
282+
<a href="${downloadUrl}">下载</a>
283+
<button class="copy-btn" onclick="copyLink('${downloadUrl}')">复制链接</button>
284+
</div>
285+
</div>`;
286+
}).join('')}
287+
</div>
288+
<script>
289+
function copyLink(url) {
290+
const fullUrl = window.location.origin + url;
291+
navigator.clipboard.writeText(fullUrl).then(() => {
292+
const toast = document.getElementById('toast');
293+
toast.style.display = 'block';
294+
setTimeout(() => {
295+
toast.style.display = 'none';
296+
}, 2000);
297+
});
298+
}
299+
</script>
300+
</body>
301+
</html>`;
302+
303+
reply.type('text/html').send(html);
304+
} catch (error) {
305+
console.error('下载页面加载失败:', error.message);
306+
return reply.code(500).send({
307+
success: false,
308+
message: '加载下载页面失败',
309+
error: error.message,
310+
});
311+
}
312+
});
313+
314+
fastify.get('/admin/download/:filename', {
315+
preHandler: async (request, reply) => {
316+
const {auth} = request.query;
317+
if (validateDownloadToken(request.params.filename, auth)) {
318+
return;
319+
}
320+
const authHeader = request.headers.authorization;
321+
if (!authHeader) {
322+
reply.header('WWW-Authenticate', 'Basic');
323+
return reply.code(401).send('Authentication required');
324+
}
325+
const base64Credentials = authHeader.split(' ')[1];
326+
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
327+
const [username, password] = credentials.split(':');
328+
const validUsername = process.env.API_AUTH_NAME || '';
329+
const validPassword = process.env.API_AUTH_CODE || '';
330+
if (username === validUsername && password === validPassword) {
331+
return;
332+
}
333+
reply.header('WWW-Authenticate', 'Basic');
334+
return reply.code(401).send('Invalid credentials');
335+
}
336+
}, async (request, reply) => {
337+
try {
338+
if (IS_VERCEL) {
339+
return reply.code(403).send({
340+
success: false,
341+
message: 'Vercel 环境不支持文件下载功能',
342+
});
343+
}
344+
345+
const {filename} = request.params;
346+
const projectName = path.basename(projectRootDir);
347+
348+
const validFilenames = [
349+
`${projectName}.7z`,
350+
`${projectName}.zip`,
351+
`${projectName}-green.7z`,
352+
`${projectName}-green.zip`
353+
];
354+
355+
if (!validFilenames.includes(filename)) {
356+
return reply.code(400).send({
357+
success: false,
358+
message: '无效的文件名',
359+
});
360+
}
361+
362+
let latestPackage = findLatestPackage(projectRootDir, filename);
363+
364+
if (!latestPackage) {
365+
console.log(`未找到 ${filename},开始打包...`);
366+
latestPackage = buildPackage(filename);
367+
if (!latestPackage) {
368+
return reply.code(500).send({
369+
success: false,
370+
message: '打包失败,无法创建压缩文件',
371+
});
372+
}
373+
}
374+
375+
const fileStream = createReadStream(latestPackage.filePath);
376+
const contentType = filename.endsWith('.zip') ? 'application/zip' : 'application/x-7z-compressed';
377+
reply.header('Content-Type', contentType);
378+
reply.header('Content-Disposition', `attachment; filename="${encodeURIComponent(latestPackage.file)}"`);
379+
return reply.send(fileStream);
380+
} catch (error) {
381+
console.error('下载文件失败:', error.message);
382+
return reply.code(500).send({
383+
success: false,
384+
message: '下载失败',
385+
error: error.message,
386+
});
387+
}
388+
});
389+
78390
done();
79391
};

package.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,21 @@ const filterGreenFiles = (scriptDir) => {
3737
};
3838

3939
// 压缩目录
40-
const compressDirectory = (scriptDir, green) => {
40+
const compressDirectory = (scriptDir, green, useZip) => {
4141
const currentDir = basename(scriptDir);
4242
const currentTime = new Date().toLocaleDateString('zh-CN', {
4343
year: 'numeric',
4444
month: '2-digit',
4545
day: '2-digit'
4646
}).replace(/\//g, '');
4747
const archiveSuffix = green ? '-green' : '';
48-
const archiveName = `${currentDir}-${currentTime}${archiveSuffix}.7z`;
48+
const archiveExt = useZip ? '.zip' : '.7z';
49+
const archiveName = `${currentDir}-${currentTime}${archiveSuffix}${archiveExt}`;
4950

5051
const parentDir = resolve(scriptDir, '..');
5152
const archivePath = join(parentDir, archiveName);
5253

53-
// 构建 7z 命令
54+
// 构建压缩命令参数
5455
const excludeParams = [];
5556

5657
// 排除目录
@@ -77,7 +78,7 @@ const compressDirectory = (scriptDir, green) => {
7778
}
7879

7980
// 构建命令,打包目录内容而不包含目录本身
80-
const command = `7z a "${archivePath}" "${join(scriptDir, '*')}" -r ${excludeParams.join(' ')}`;
81+
const command = `7z a -t${useZip ? 'zip' : '7z'} "${archivePath}" "${join(scriptDir, '*')}" -r ${excludeParams.join(' ')}`;
8182
console.log(`构建的 7z 命令: ${command}`);
8283

8384
try {
@@ -95,8 +96,9 @@ const main = () => {
9596
// 简单解析命令行参数
9697
const args = process.argv.slice(2);
9798
const green = args.includes('-g') || args.includes('--green');
99+
const useZip = args.includes('-z') || args.includes('--zip');
98100

99-
compressDirectory(scriptDir, green);
101+
compressDirectory(scriptDir, green, useZip);
100102
};
101103

102104
main();

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
"node22-win": "chcp 65001 && node --trace-deprecation --experimental-sqlite index.js",
1515
"package": "python package.py",
1616
"package-green": "python package.py -g",
17+
"package-zip": "python package.py -z",
18+
"package-green-zip": "python package.py -g -z",
1719
"packageJS": "node package.js",
1820
"packageJS-green": "node package.js -g",
21+
"packageJS-zip": "node package.js -z",
22+
"packageJS-green-zip": "node package.js -g -z",
1923
"gzip-1": "node controllers/encoder.js json/十六万歌曲.json",
2024
"ungzip-1": "node controllers/decoder.js json/十六万歌曲.json.gz",
2125
"moontv": "node scripts/mjs/moontv.mjs 采集2025.json -p"

0 commit comments

Comments
 (0)