This commit is contained in:
Baobhan Sith
2025-04-14 22:51:05 +08:00
parent 286492fc63
commit a974b8b1d9
49 changed files with 13954 additions and 0 deletions
@@ -0,0 +1,104 @@
import { Request, Response } from 'express';
import path from 'path'; // 需要 path 用于处理文件名
import { activeSshConnections } from '../websocket'; // 导入共享的连接 Map
/**
* 处理文件下载请求 (GET /api/v1/sftp/download)
*/
export const downloadFile = async (req: Request, res: Response): Promise<void> => {
const userId = req.session.userId;
const connectionId = req.query.connectionId as string; // 从查询参数获取
const remotePath = req.query.remotePath as string; // 从查询参数获取
// 参数验证
if (!userId) {
res.status(401).json({ message: '未授权:需要登录。' });
return;
}
if (!connectionId || !remotePath) {
res.status(400).json({ message: '缺少必要的查询参数 (connectionId, remotePath)。' });
return;
}
console.log(`SFTP 下载请求:用户 ${userId}, 连接 ${connectionId}, 路径 ${remotePath}`);
// 查找与当前用户会话关联的活动 WebSocket 连接和 SFTP 会话
let userSftpSession = null;
// 注意:这种查找方式效率不高,实际应用中可能需要更优化的结构来按 userId 查找连接
for (const [ws, connData] of activeSshConnections.entries()) {
// 假设 AuthenticatedWebSocket 上存储了 userId
if ((ws as any).userId === userId && connData.sftp) {
// 这里简单地取第一个找到的匹配连接,没有处理 connectionId 的匹配
// TODO: 需要一种方式将 HTTP 请求与特定的 WebSocket/SSH/SFTP 会话关联起来
// 临时方案:假设一个用户只有一个活动的 SSH/SFTP 会话
userSftpSession = connData.sftp;
console.log(`找到用户 ${userId} 的活动 SFTP 会话。`);
break;
}
}
if (!userSftpSession) {
console.warn(`SFTP 下载失败:未找到用户 ${userId} 的活动 SFTP 会话。`);
res.status(404).json({ message: '未找到活动的 SFTP 会话。请确保您已通过 WebSocket 连接到目标服务器。' });
return;
}
try {
// 获取文件状态以确定文件大小(可选,但有助于设置 Content-Length
const stats = await new Promise<import('ssh2').Stats>((resolve, reject) => {
userSftpSession!.lstat(remotePath, (err, stats) => {
if (err) return reject(err);
resolve(stats);
});
});
if (!stats.isFile()) {
res.status(400).json({ message: '指定的路径不是一个文件。' });
return;
}
// 设置响应头
res.setHeader('Content-Disposition', `attachment; filename="${path.basename(remotePath)}"`); // 建议浏览器下载的文件名
res.setHeader('Content-Type', 'application/octet-stream'); // 通用二进制类型
if (stats.size) {
res.setHeader('Content-Length', stats.size.toString());
}
// 创建可读流并 pipe 到响应对象
const readStream = userSftpSession.createReadStream(remotePath);
readStream.on('error', (err: Error) => { // 添加 Error 类型注解
console.error(`SFTP 读取流错误 (用户 ${userId}, 路径 ${remotePath}):`, err);
// 如果响应头还没发送,可以发送错误状态码
if (!res.headersSent) {
res.status(500).json({ message: `读取远程文件失败: ${err.message}` });
} else {
// 如果头已发送,只能尝试结束响应
res.end();
}
});
readStream.pipe(res); // 将文件流直接传输给客户端
// 监听响应对象的 close 事件,确保流被正确关闭 (虽然 pipe 通常会处理)
res.on('close', () => {
console.log(`SFTP 下载流关闭 (用户 ${userId}, 路径 ${remotePath})`);
// readStream.destroy(); // 可选:显式销毁流
});
console.log(`SFTP 开始下载 (用户 ${userId}, 路径 ${remotePath})`);
} catch (error: any) {
console.error(`SFTP 下载处理失败 (用户 ${userId}, 路径 ${remotePath}):`, error);
if (!res.headersSent) {
if (error.message?.includes('No such file')) {
res.status(404).json({ message: '远程文件未找到。' });
} else {
res.status(500).json({ message: `处理下载请求时出错: ${error.message}` });
}
}
}
};
// 其他 SFTP 控制器函数 (例如上传)
// export const uploadFile = ...
+15
View File
@@ -0,0 +1,15 @@
import { Router } from 'express';
import { isAuthenticated } from '../auth/auth.middleware';
import { downloadFile } from './sftp.controller'; // 稍后创建
const router = Router();
// 应用认证中间件
router.use(isAuthenticated);
// GET /api/v1/sftp/download?connectionId=...&remotePath=...
router.get('/download', downloadFile);
// 未来可以添加其他 SFTP 相关 REST API (如果需要,例如上传的大文件断点续传初始化)
export default router;