428 lines
15 KiB
JavaScript
428 lines
15 KiB
JavaScript
"use strict";
|
||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||
};
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.CloudflareShareService = void 0;
|
||
const child_process_1 = require("child_process");
|
||
const fs_1 = __importDefault(require("fs"));
|
||
const path_1 = __importDefault(require("path"));
|
||
const http_1 = __importDefault(require("http"));
|
||
const express_1 = __importDefault(require("express"));
|
||
const Logger_1 = __importDefault(require("../utils/Logger"));
|
||
/**
|
||
* Cloudflare文件分享服务
|
||
* 用于生成临时文件分享链接,有效期10分钟
|
||
*/
|
||
class CloudflareShareService {
|
||
constructor() {
|
||
this.activeShares = new Map();
|
||
this.logger = new Logger_1.default('CloudflareShare');
|
||
// 每分钟清理过期的分享会话
|
||
this.cleanupInterval = setInterval(() => {
|
||
this.cleanupExpiredShares();
|
||
}, 60 * 1000);
|
||
}
|
||
/**
|
||
* 为文件创建临时分享链接
|
||
* @param filePath 文件路径
|
||
* @param filename 文件名
|
||
* @param durationMinutes 有效期(分钟),默认10分钟
|
||
* @returns 分享链接信息
|
||
*/
|
||
async createShareLink(filePath, filename, durationMinutes = 10) {
|
||
try {
|
||
// 检查文件是否存在
|
||
if (!fs_1.default.existsSync(filePath)) {
|
||
throw new Error(`文件不存在: ${filePath}`);
|
||
}
|
||
// 检查cloudflared是否存在
|
||
const cloudflaredPath = await this.findCloudflared();
|
||
if (!cloudflaredPath) {
|
||
throw new Error('cloudflared 未找到,请先安装 cloudflared');
|
||
}
|
||
// 生成会话ID
|
||
const sessionId = this.generateSessionId();
|
||
// 创建临时服务器
|
||
const port = await this.findAvailablePort(8080);
|
||
const server = await this.createFileServer(filePath, filename, port);
|
||
// 启动cloudflared隧道
|
||
const tunnelProcess = await this.startCloudflaredTunnel(cloudflaredPath, port);
|
||
const tunnelUrl = await this.extractTunnelUrl(tunnelProcess);
|
||
// 创建分享会话
|
||
const expiresAt = new Date(Date.now() + durationMinutes * 60 * 1000);
|
||
const shareSession = {
|
||
sessionId,
|
||
filePath,
|
||
filename,
|
||
port,
|
||
server,
|
||
tunnelProcess,
|
||
tunnelUrl,
|
||
createdAt: new Date(),
|
||
expiresAt
|
||
};
|
||
this.activeShares.set(sessionId, shareSession);
|
||
this.logger.info(`创建分享链接成功: ${tunnelUrl} (有效期: ${durationMinutes}分钟)`);
|
||
return {
|
||
success: true,
|
||
sessionId,
|
||
shareUrl: tunnelUrl,
|
||
filename,
|
||
expiresAt: expiresAt.toISOString(),
|
||
durationMinutes
|
||
};
|
||
}
|
||
catch (error) {
|
||
const errorMessage = error.message || error.toString() || '未知错误';
|
||
this.logger.error('创建分享链接失败:', errorMessage);
|
||
this.logger.error('错误详情:', error);
|
||
return {
|
||
success: false,
|
||
error: errorMessage
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* 停止分享会话
|
||
*/
|
||
async stopShare(sessionId) {
|
||
const session = this.activeShares.get(sessionId);
|
||
if (!session) {
|
||
return false;
|
||
}
|
||
try {
|
||
// 关闭服务器
|
||
if (session.server) {
|
||
session.server.close();
|
||
}
|
||
// 终止cloudflared进程
|
||
if (session.tunnelProcess && !session.tunnelProcess.killed) {
|
||
session.tunnelProcess.kill('SIGTERM');
|
||
// 如果进程没有正常退出,强制杀死
|
||
setTimeout(() => {
|
||
if (session.tunnelProcess && !session.tunnelProcess.killed) {
|
||
session.tunnelProcess.kill('SIGKILL');
|
||
}
|
||
}, 5000);
|
||
}
|
||
this.activeShares.delete(sessionId);
|
||
this.logger.info(`停止分享会话: ${sessionId}`);
|
||
return true;
|
||
}
|
||
catch (error) {
|
||
this.logger.error(`停止分享会话失败: ${sessionId}`, error);
|
||
return false;
|
||
}
|
||
}
|
||
/**
|
||
* 获取活动分享会话列表
|
||
*/
|
||
getActiveShares() {
|
||
const shares = [];
|
||
for (const [sessionId, session] of this.activeShares) {
|
||
shares.push({
|
||
sessionId,
|
||
filename: session.filename,
|
||
shareUrl: session.tunnelUrl,
|
||
createdAt: session.createdAt.toISOString(),
|
||
expiresAt: session.expiresAt.toISOString(),
|
||
isExpired: Date.now() > session.expiresAt.getTime()
|
||
});
|
||
}
|
||
return shares;
|
||
}
|
||
/**
|
||
* 清理过期的分享会话
|
||
*/
|
||
cleanupExpiredShares() {
|
||
const now = Date.now();
|
||
const expiredSessions = [];
|
||
for (const [sessionId, session] of this.activeShares) {
|
||
if (now > session.expiresAt.getTime()) {
|
||
expiredSessions.push(sessionId);
|
||
}
|
||
}
|
||
for (const sessionId of expiredSessions) {
|
||
this.stopShare(sessionId);
|
||
this.logger.info(`自动清理过期分享会话: ${sessionId}`);
|
||
}
|
||
}
|
||
/**
|
||
* 查找cloudflared可执行文件
|
||
*/
|
||
async findCloudflared() {
|
||
// 相对于项目根目录的路径
|
||
const projectRoot = path_1.default.resolve(process.cwd(), '..');
|
||
const possiblePaths = [
|
||
path_1.default.join(projectRoot, 'cloudflared'), // 项目根目录
|
||
'./cloudflared', // 当前目录
|
||
path_1.default.join(process.cwd(), 'cloudflared'), // 完整路径
|
||
'/usr/local/bin/cloudflared', // 系统安装路径
|
||
'/usr/bin/cloudflared',
|
||
'./bin/cloudflared'
|
||
];
|
||
this.logger.info(`查找cloudflared,项目根目录: ${projectRoot}`);
|
||
for (const cloudflaredPath of possiblePaths) {
|
||
this.logger.debug(`检查路径: ${cloudflaredPath}`);
|
||
if (fs_1.default.existsSync(cloudflaredPath)) {
|
||
this.logger.info(`找到cloudflared: ${cloudflaredPath}`);
|
||
return cloudflaredPath;
|
||
}
|
||
}
|
||
// 尝试从PATH中查找
|
||
return new Promise((resolve) => {
|
||
const which = (0, child_process_1.spawn)('which', ['cloudflared']);
|
||
let output = '';
|
||
let errorOutput = '';
|
||
which.stdout.on('data', (data) => {
|
||
output += data.toString();
|
||
});
|
||
which.stderr.on('data', (data) => {
|
||
errorOutput += data.toString();
|
||
});
|
||
which.on('close', (code) => {
|
||
if (code === 0 && output.trim()) {
|
||
this.logger.info(`在PATH中找到cloudflared: ${output.trim()}`);
|
||
resolve(output.trim());
|
||
}
|
||
else {
|
||
this.logger.warn(`在PATH中未找到cloudflared,退出代码: ${code},错误: ${errorOutput}`);
|
||
resolve(null);
|
||
}
|
||
});
|
||
which.on('error', (error) => {
|
||
this.logger.error('执行which命令失败:', error);
|
||
resolve(null);
|
||
});
|
||
});
|
||
}
|
||
/**
|
||
* 查找可用端口
|
||
*/
|
||
async findAvailablePort(startPort) {
|
||
return new Promise((resolve, reject) => {
|
||
const server = http_1.default.createServer();
|
||
server.listen(startPort, () => {
|
||
const port = server.address()?.port;
|
||
server.close(() => {
|
||
resolve(port);
|
||
});
|
||
});
|
||
server.on('error', () => {
|
||
// 端口被占用,尝试下一个
|
||
this.findAvailablePort(startPort + 1).then(resolve).catch(reject);
|
||
});
|
||
});
|
||
}
|
||
/**
|
||
* 创建文件服务器
|
||
*/
|
||
async createFileServer(filePath, filename, port) {
|
||
const app = (0, express_1.default)();
|
||
// 文件下载页面
|
||
app.get('/', (req, res) => {
|
||
const fileStats = fs_1.default.statSync(filePath);
|
||
const fileSize = this.formatFileSize(fileStats.size);
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>File Download - ${filename}</title>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
}
|
||
.container {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||
max-width: 500px;
|
||
width: 100%;
|
||
}
|
||
.icon {
|
||
font-size: 48px;
|
||
margin-bottom: 20px;
|
||
color: #667eea;
|
||
}
|
||
h1 { color: #333; margin-bottom: 10px; font-size: 24px; }
|
||
.filename {
|
||
color: #666;
|
||
margin-bottom: 8px;
|
||
font-size: 18px;
|
||
word-break: break-all;
|
||
background: #f5f5f5;
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
}
|
||
.filesize {
|
||
color: #888;
|
||
margin-bottom: 30px;
|
||
font-size: 14px;
|
||
}
|
||
.download-btn {
|
||
display: inline-block;
|
||
padding: 16px 32px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
text-decoration: none;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
border: none;
|
||
cursor: pointer;
|
||
}
|
||
.download-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
|
||
}
|
||
.warning {
|
||
margin-top: 20px;
|
||
padding: 16px;
|
||
background: #fff3cd;
|
||
border: 1px solid #ffeaa7;
|
||
border-radius: 8px;
|
||
color: #856404;
|
||
font-size: 14px;
|
||
}
|
||
@media (max-width: 480px) {
|
||
.container { padding: 20px; }
|
||
h1 { font-size: 20px; }
|
||
.filename { font-size: 16px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="icon">📱</div>
|
||
<h1>APK文件下载</h1>
|
||
<div class="filename">${filename}</div>
|
||
<div class="filesize">文件大小: ${fileSize}</div>
|
||
<a href="/download" class="download-btn">立即下载</a>
|
||
<div class="warning">
|
||
⚠️ 此下载链接有效期为10分钟,请及时下载
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
res.send(html);
|
||
});
|
||
// 文件下载接口
|
||
app.get('/download', (req, res) => {
|
||
try {
|
||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||
res.setHeader('Content-Type', 'application/vnd.android.package-archive');
|
||
const fileStream = fs_1.default.createReadStream(filePath);
|
||
fileStream.pipe(res);
|
||
this.logger.info(`文件下载: ${filename} from ${req.ip}`);
|
||
}
|
||
catch (error) {
|
||
this.logger.error('文件下载失败:', error);
|
||
res.status(500).send('下载失败');
|
||
}
|
||
});
|
||
return new Promise((resolve, reject) => {
|
||
const server = app.listen(port, '0.0.0.0', () => {
|
||
this.logger.info(`文件服务器启动: http://0.0.0.0:${port}`);
|
||
resolve(server);
|
||
});
|
||
server.on('error', reject);
|
||
});
|
||
}
|
||
/**
|
||
* 启动cloudflared隧道
|
||
*/
|
||
async startCloudflaredTunnel(cloudflaredPath, port) {
|
||
return new Promise((resolve, reject) => {
|
||
const args = [
|
||
'tunnel',
|
||
'--url', `http://localhost:${port}`,
|
||
'--no-autoupdate',
|
||
'--no-tls-verify'
|
||
];
|
||
const tunnelProcess = (0, child_process_1.spawn)(cloudflaredPath, args);
|
||
tunnelProcess.on('error', (error) => {
|
||
this.logger.error('启动cloudflared失败:', error);
|
||
reject(error);
|
||
});
|
||
// 等待进程启动
|
||
setTimeout(() => {
|
||
if (!tunnelProcess.killed) {
|
||
resolve(tunnelProcess);
|
||
}
|
||
else {
|
||
reject(new Error('cloudflared进程启动失败'));
|
||
}
|
||
}, 3000);
|
||
});
|
||
}
|
||
/**
|
||
* 从cloudflared输出中提取隧道URL
|
||
*/
|
||
async extractTunnelUrl(tunnelProcess) {
|
||
return new Promise((resolve, reject) => {
|
||
let output = '';
|
||
const timeout = setTimeout(() => {
|
||
reject(new Error('获取隧道URL超时'));
|
||
}, 30000);
|
||
const onData = (data) => {
|
||
output += data.toString();
|
||
// 查找隧道URL
|
||
const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
|
||
if (urlMatch) {
|
||
clearTimeout(timeout);
|
||
tunnelProcess.stdout?.off('data', onData);
|
||
tunnelProcess.stderr?.off('data', onData);
|
||
resolve(urlMatch[0]);
|
||
}
|
||
};
|
||
tunnelProcess.stdout?.on('data', onData);
|
||
tunnelProcess.stderr?.on('data', onData);
|
||
});
|
||
}
|
||
/**
|
||
* 生成会话ID
|
||
*/
|
||
generateSessionId() {
|
||
return 'share_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||
}
|
||
/**
|
||
* 格式化文件大小
|
||
*/
|
||
formatFileSize(bytes) {
|
||
if (bytes === 0)
|
||
return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
/**
|
||
* 销毁服务
|
||
*/
|
||
destroy() {
|
||
if (this.cleanupInterval) {
|
||
clearInterval(this.cleanupInterval);
|
||
}
|
||
// 停止所有活动分享会话
|
||
for (const sessionId of this.activeShares.keys()) {
|
||
this.stopShare(sessionId);
|
||
}
|
||
}
|
||
}
|
||
exports.CloudflareShareService = CloudflareShareService;
|
||
exports.default = CloudflareShareService;
|
||
//# sourceMappingURL=CloudflareShareService.js.map
|