Files
server/dist/services/CloudflareShareService.js
wdvipa 450367dea2 111
2026-02-09 16:34:01 +08:00

428 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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