Initial commit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
liu
2026-02-03 10:10:11 +08:00
commit 14289beb66
45 changed files with 15479 additions and 0 deletions

View File

@@ -0,0 +1,591 @@
# 模块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文件删除功能