Files
server/dist/services/CloudflareShareService.js

428 lines
15 KiB
JavaScript
Raw Normal View History

2026-02-09 16:34:01 +08:00
"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