Files
sftp-manager/docs/05-文件上传下载功能.md
liu 14289beb66 Initial commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 10:10:11 +08:00

592 lines
18 KiB
Markdown
Raw 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.
# 模块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
- 标准间距16px1rem
- 组件内边距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<Void> 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<Void> 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
<div class="upload-area">
<input type="file" id="file-input" multiple style="display:none">
<button onclick="document.getElementById('file-input').click()">选择文件</button>
<div class="drop-zone" id="drop-zone">
<p>拖拽文件到此处或点击选择文件</p>
</div>
<div class="upload-progress" id="upload-progress" style="display:none;">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%">0%</div>
</div>
</div>
</div>
```
### 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文件删除功能