# 模块05:文件上传下载功能 --- ## 🎨 UI设计系统概览 > **完整设计系统文档请参考:** `UI设计系统.md` ### 核心设计原则 - **现代简约**:界面清晰,层次分明 - **专业高效**:减少操作步骤,提升工作效率 - **一致性**:统一的视觉语言和交互模式 - **可访问性**:符合WCAG 2.1 AA标准 ### 关键设计令牌 **颜色系统:** - 主色:`#0d6efd`(操作按钮、选中状态) - 成功:`#198754`(连接成功状态) - 危险:`#dc3545`(删除操作、错误提示) - 深灰:`#212529`(导航栏背景) - 浅灰:`#e9ecef`(工具栏背景) **字体系统:** - 字体族:系统字体栈(-apple-system, Segoe UI, Roboto等) - 正文:14px,行高1.5 - 标题:20-32px,行高1.2-1.4 - 小号文字:12px(文件大小、日期等) **间距系统:** - 基础单位:8px - 标准间距:16px(1rem) - 组件内边距:8px-16px **组件规范:** - 导航栏:高度48px,深色背景 - 工具栏:浅灰背景,按钮间距8px - 文件项:最小高度44px,悬停效果150ms - 按钮:圆角4px,过渡150ms **交互规范:** - 悬停效果:150ms过渡 - 触摸目标:最小44x44px - 键盘导航:Tab、Enter、Delete、F2、F5、Esc - 焦点状态:2px蓝色轮廓 **响应式断点:** - 移动端:< 768px(双面板垂直排列) - 平板:768px - 1024px - 桌面:> 1024px(标准布局) --- ## 5.1 功能概述 实现本地与SFTP服务器之间、以及两个SFTP服务器之间的文件上传和下载功能。 ## 5.2 后端设计 ### 5.2.1 SftpService扩展方法 ```java // 上传文件到SFTP public void uploadFile(String sessionId, InputStream inputStream, String remotePath, long fileSize) throws Exception { ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } try { channel.put(inputStream, remotePath); } catch (SftpException e) { throw new Exception("上传失败: " + e.getMessage(), e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (Exception e) { // 忽略关闭异常 } } } } // 从SFTP下载文件 public void downloadFile(String sessionId, String remotePath, OutputStream outputStream) throws Exception { ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } try { channel.get(remotePath, outputStream); } catch (SftpException e) { throw new Exception("下载失败: " + e.getMessage(), e); } finally { if (outputStream != null) { try { outputStream.close(); } catch (Exception e) { // 忽略关闭异常 } } } } // SFTP间传输 public void transferBetweenSftp(String sourceSessionId, String sourcePath, String targetSessionId, String targetPath) throws Exception { // 创建临时文件 String tempDir = System.getProperty("java.io.tmpdir"); String tempFile = tempDir + File.separator + UUID.randomUUID().toString(); try { // 从源SFTP下载到临时文件 ChannelSftp sourceChannel = sessionManager.getSession(sourceSessionId); if (sourceChannel == null) { throw new Exception("源会话不存在或已断开"); } sourceChannel.get(sourcePath, tempFile); // 上传临时文件到目标SFTP ChannelSftp targetChannel = sessionManager.getSession(targetSessionId); if (targetChannel == null) { throw new Exception("目标会话不存在或已断开"); } targetChannel.put(tempFile, targetPath); } finally { // 删除临时文件 File file = new File(tempFile); if (file.exists()) { file.delete(); } } } ``` ### 5.2.2 LocalFileService扩展方法 ```java // 上传本地文件到SFTP public void uploadToSftp(String localPath, String sessionId, String remotePath, SftpService sftpService) throws Exception { File file = new File(localPath); if (!file.exists()) { throw new Exception("本地文件不存在: " + localPath); } try (InputStream inputStream = new FileInputStream(file)) { sftpService.uploadFile(sessionId, inputStream, remotePath, file.length()); } } // 从SFTP下载到本地 public void downloadFromSftp(String sessionId, String remotePath, String localPath, SftpService sftpService) throws Exception { File file = new File(localPath); File parentDir = file.getParentFile(); // 确保父目录存在 if (parentDir != null && !parentDir.exists()) { parentDir.mkdirs(); } try (OutputStream outputStream = new FileOutputStream(file)) { sftpService.downloadFile(sessionId, remotePath, outputStream); } } ``` ### 5.2.3 FileController扩展接口 ```java @Autowired private LocalFileService localFileService; // 上传文件 @PostMapping("/upload") public ApiResponse uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("targetSessionId") String targetSessionId, @RequestParam("targetPath") String targetPath) { try { if ("local".equals(targetSessionId)) { // 上传到本地 File destFile = new File(targetPath, file.getOriginalFilename()); file.transferTo(destFile); } else { // 上传到SFTP try (InputStream inputStream = file.getInputStream()) { String remotePath = targetPath.endsWith("/") ? targetPath + file.getOriginalFilename() : targetPath + "/" + file.getOriginalFilename(); sftpService.uploadFile(targetSessionId, inputStream, remotePath, file.getSize()); } } return ApiResponse.success("上传成功", null); } catch (Exception e) { return ApiResponse.error("上传失败: " + e.getMessage()); } } // 下载文件 @GetMapping("/download") public void downloadFile(@RequestParam String sessionId, @RequestParam String path, HttpServletResponse response) { try { if ("local".equals(sessionId)) { // 下载本地文件 File file = new File(path); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(file.getName(), "UTF-8")); response.setContentLengthLong(file.length()); try (InputStream inputStream = new FileInputStream(file); OutputStream outputStream = response.getOutputStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } outputStream.flush(); } } else { // 下载SFTP文件 ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } FileInfo fileInfo = sftpService.getFileInfo(sessionId, path); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileInfo.getName(), "UTF-8")); response.setContentLengthLong(fileInfo.getSize()); try (OutputStream outputStream = response.getOutputStream()) { sftpService.downloadFile(sessionId, path, outputStream); outputStream.flush(); } } } catch (Exception e) { response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); try { response.getWriter().write("下载失败: " + e.getMessage()); } catch (Exception ex) { // 忽略 } } } // 服务器间传输 @PostMapping("/transfer") public ApiResponse transferFiles(@RequestBody TransferRequest request) { try { String sourceSessionId = request.getSourceSessionId(); String sourcePath = request.getSourcePath(); String targetSessionId = request.getTargetSessionId(); String targetPath = request.getTargetPath(); // 获取源文件名 String fileName; if ("local".equals(sourceSessionId)) { File file = new File(sourcePath); fileName = file.getName(); } else { FileInfo fileInfo = sftpService.getFileInfo(sourceSessionId, sourcePath); fileName = fileInfo.getName(); } // 构建目标路径 String finalTargetPath = targetPath.endsWith("/") ? targetPath + fileName : targetPath + "/" + fileName; if ("local".equals(sourceSessionId) && "local".equals(targetSessionId)) { // 本地到本地 Files.copy(new File(sourcePath).toPath(), new File(finalTargetPath).toPath()); } else if ("local".equals(sourceSessionId)) { // 本地到SFTP localFileService.uploadToSftp(sourcePath, targetSessionId, finalTargetPath, sftpService); } else if ("local".equals(targetSessionId)) { // SFTP到本地 localFileService.downloadFromSftp(sourceSessionId, sourcePath, finalTargetPath, sftpService); } else { // SFTP到SFTP sftpService.transferBetweenSftp(sourceSessionId, sourcePath, targetSessionId, finalTargetPath); } return ApiResponse.success("传输成功", null); } catch (Exception e) { return ApiResponse.error("传输失败: " + e.getMessage()); } } ``` ## 5.3 前端设计 ### 5.3.1 上传界面 ```html

拖拽文件到此处或点击选择文件

``` ### 5.3.2 上传实现 ```javascript // 文件选择 document.getElementById('file-input').addEventListener('change', function(e) { const files = e.target.files; uploadFiles(files); }); // 拖拽上传 const dropZone = document.getElementById('drop-zone'); dropZone.addEventListener('dragover', function(e) { e.preventDefault(); dropZone.style.backgroundColor = '#f0f0f0'; }); dropZone.addEventListener('dragleave', function(e) { e.preventDefault(); dropZone.style.backgroundColor = ''; }); dropZone.addEventListener('drop', function(e) { e.preventDefault(); dropZone.style.backgroundColor = ''; const files = e.dataTransfer.files; uploadFiles(files); }); // 上传文件 function uploadFiles(files) { const targetPanelId = getTargetPanelId(); // 获取目标面板ID const targetSessionId = panelState[targetPanelId].sessionId; const targetPath = panelState[targetPanelId].currentPath; Array.from(files).forEach(file => { uploadSingleFile(file, targetSessionId, targetPath); }); } // 上传单个文件 function uploadSingleFile(file, targetSessionId, targetPath) { let formData = new FormData(); formData.append('file', file); formData.append('targetSessionId', targetSessionId); formData.append('targetPath', targetPath); showUploadProgress(true); $.ajax({ url: '/api/files/upload', method: 'POST', data: formData, processData: false, contentType: false, xhr: function() { let xhr = new window.XMLHttpRequest(); xhr.upload.addEventListener('progress', function(e) { if (e.lengthComputable) { let percent = Math.round((e.loaded / e.total) * 100); updateUploadProgress(percent); } }); return xhr; }, success: function(response) { if (response.success) { alert(file.name + ' 上传成功'); loadFiles(targetPanelId); } else { alert('上传失败: ' + response.message); } }, error: function(xhr, status, error) { alert('上传失败: ' + error); }, complete: function() { showUploadProgress(false); } }); } // 显示上传进度 function showUploadProgress(show) { const progressDiv = document.getElementById('upload-progress'); progressDiv.style.display = show ? 'block' : 'none'; } // 更新上传进度 function updateUploadProgress(percent) { const progressBar = document.querySelector('#upload-progress .progress-bar'); progressBar.style.width = percent + '%'; progressBar.textContent = percent + '%'; } ``` ### 5.3.3 下载实现 ```javascript // 下载文件 function downloadFile(sessionId, path) { window.location.href = '/api/files/download?sessionId=' + encodeURIComponent(sessionId) + '&path=' + encodeURIComponent(path); } // 批量下载 function downloadSelectedFiles(panelId) { const selectedFiles = panelState[panelId].selectedFiles; selectedFiles.forEach(path => { downloadFile(panelState[panelId].sessionId, path); }); } ``` ### 5.3.4 跨服务器传输 ```javascript // 传输到对面面板 function transferToOppositePanel() { const sourcePanelId = getSourcePanelId(); const targetPanelId = getTargetPanelId(); const sourceSessionId = panelState[sourcePanelId].sessionId; const targetSessionId = panelState[targetPanelId].sessionId; const targetPath = panelState[targetPanelId].currentPath; const selectedFiles = panelState[sourcePanelId].selectedFiles; if (selectedFiles.length === 0) { alert('请先选择要传输的文件'); return; } selectedFiles.forEach(sourcePath => { $.ajax({ url: '/api/files/transfer', method: 'POST', contentType: 'application/json', data: JSON.stringify({ sourceSessionId: sourceSessionId, sourcePath: sourcePath, targetSessionId: targetSessionId, targetPath: targetPath }), success: function(response) { if (response.success) { alert('传输成功'); loadFiles(targetPanelId); } else { alert('传输失败: ' + response.message); } }, error: function(xhr, status, error) { alert('传输失败: ' + error); } }); }); } ``` ## 5.4 性能优化 ### 5.4.1 大文件处理 - 使用流式传输避免内存溢出 - 设置合理的超时时间 - 显示实时进度 - 支持断点续传(可选) ### 5.4.2 断点续传(高级功能) ```java // 支持断点的上传 public void uploadFileWithResume(String sessionId, InputStream inputStream, String remotePath, long fileSize, long resumeFrom) throws Exception { ChannelSftp channel = sessionManager.getSession(sessionId); if (channel == null) { throw new Exception("会话不存在或已断开"); } try { if (resumeFrom > 0) { // 断点续传,跳过已传输的字节 inputStream.skip(resumeFrom); channel.put(inputStream, remotePath, ChannelSftp.RESUME); } else { channel.put(inputStream, remotePath); } } catch (SftpException e) { throw new Exception("上传失败: " + e.getMessage(), e); } } ``` ### 5.4.3 并发控制 - 限制同时上传/下载数量 - 队列管理机制 - 任务取消功能 ### 5.4.4 临时文件清理 - SFTP间传输后删除临时文件 - 定期清理超时临时文件 - 使用系统临时目录 ## 实施步骤 1. **更新SftpService**:添加上传、下载、传输方法 2. **更新LocalFileService**:添加与SFTP交互的方法 3. **更新FileController**:添加上传、下载、传输接口 4. **编译测试** ``` mvn clean compile ``` 5. **启动服务** ``` mvn spring-boot:run ``` ## 测试验证 ### 1. 上传文件 ```bash curl -X POST http://localhost:8080/sftp-manager/api/files/upload \ -F "file=@test.txt" \ -F "targetSessionId=local" \ -F "targetPath=C:/test" ``` ### 2. 下载文件 ```bash curl "http://localhost:8080/sftp-manager/api/files/download?sessionId=local&path=C:/test/test.txt" \ --output downloaded.txt ``` ### 3. 服务器间传输 ```bash curl -X POST http://localhost:8080/sftp-manager/api/files/transfer \ -H "Content-Type: application/json" \ -d '{ "sourceSessionId": "sftp-uuid1", "sourcePath": "/home/source.txt", "targetSessionId": "sftp-uuid2", "targetPath": "/home/target/" }' ``` ## 注意事项 1. **文件大小限制**:application.yml中配置最大文件大小 2. **超时设置**:大文件传输需要增加超时时间 3. **磁盘空间**:确保目标位置有足够空间 4. **权限检查**:上传/下载前检查文件权限 5. **临时文件**:及时清理临时文件 ## 下一步 完成模块05后,继续模块06:文件删除功能