"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 = ` File Download - ${filename}
📱

APK文件下载

${filename}
文件大小: ${fileSize}
立即下载
⚠️ 此下载链接有效期为10分钟,请及时下载
`; 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