532
docs/06-文件删除功能.md
Normal file
532
docs/06-文件删除功能.md
Normal file
@@ -0,0 +1,532 @@
|
||||
# 模块06:文件删除功能
|
||||
|
||||
---
|
||||
|
||||
## 🎨 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(标准布局)
|
||||
|
||||
---
|
||||
|
||||
## 6.1 功能概述
|
||||
实现删除本地文件和SFTP服务器上文件的功能,支持单个文件删除和批量删除,包含删除确认机制。
|
||||
|
||||
## 6.2 后端设计
|
||||
|
||||
### 6.2.1 LocalFileService删除方法
|
||||
|
||||
```java
|
||||
// 删除单个文件或目录
|
||||
public boolean deleteFile(String path) throws Exception {
|
||||
File file = new File(path);
|
||||
if (!file.exists()) {
|
||||
throw new Exception("文件不存在: " + path);
|
||||
}
|
||||
|
||||
if (file.isDirectory()) {
|
||||
return deleteDirectory(file);
|
||||
} else {
|
||||
return file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// 递归删除目录
|
||||
private boolean deleteDirectory(File directory) throws Exception {
|
||||
File[] files = directory.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteDirectory(file);
|
||||
} else {
|
||||
if (!file.delete()) {
|
||||
throw new Exception("删除文件失败: " + file.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return directory.delete();
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
public BatchDeleteResult batchDelete(List<String> paths) {
|
||||
BatchDeleteResult result = new BatchDeleteResult();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
List<String> failedFiles = new ArrayList<>();
|
||||
|
||||
for (String path : paths) {
|
||||
try {
|
||||
deleteFile(path);
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
failCount++;
|
||||
failedFiles.add(path + " - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
result.setSuccessCount(successCount);
|
||||
result.setFailCount(failCount);
|
||||
result.setFailedFiles(failedFiles);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 批量删除结果类
|
||||
public static class BatchDeleteResult {
|
||||
private int successCount;
|
||||
private int failCount;
|
||||
private List<String> failedFiles;
|
||||
|
||||
// getters and setters
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2.2 SftpService删除方法
|
||||
|
||||
```java
|
||||
// 删除单个文件或目录
|
||||
public boolean deleteFile(String sessionId, String path) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
FileInfo fileInfo = getFileInfo(sessionId, path);
|
||||
if (fileInfo.isDirectory()) {
|
||||
deleteDirectoryRecursive(sessionId, path);
|
||||
} else {
|
||||
channel.rm(path);
|
||||
}
|
||||
return true;
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("删除失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 递归删除SFTP目录
|
||||
private void deleteDirectoryRecursive(String sessionId, String path) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
// 列出目录内容
|
||||
Vector<ChannelSftp.LsEntry> entries = channel.ls(path);
|
||||
|
||||
for (ChannelSftp.LsEntry entry : entries) {
|
||||
String fileName = entry.getFilename();
|
||||
if (".".equals(fileName) || "..".equals(fileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String fullPath = path.endsWith("/") ?
|
||||
path + fileName :
|
||||
path + "/" + fileName;
|
||||
|
||||
if (entry.getAttrs().isDir()) {
|
||||
// 递归删除子目录
|
||||
deleteDirectoryRecursive(sessionId, fullPath);
|
||||
} else {
|
||||
// 删除文件
|
||||
channel.rm(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除空目录
|
||||
channel.rmdir(path);
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("删除目录失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
public BatchDeleteResult batchDelete(String sessionId, List<String> paths) {
|
||||
BatchDeleteResult result = new BatchDeleteResult();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
List<String> failedFiles = new ArrayList<>();
|
||||
|
||||
for (String path : paths) {
|
||||
try {
|
||||
deleteFile(sessionId, path);
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
failCount++;
|
||||
failedFiles.add(path + " - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
result.setSuccessCount(successCount);
|
||||
result.setFailCount(failCount);
|
||||
result.setFailedFiles(failedFiles);
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2.3 FileController删除接口
|
||||
|
||||
```java
|
||||
// 删除单个文件
|
||||
@DeleteMapping("/delete")
|
||||
public ApiResponse<Void> deleteFile(@RequestParam String sessionId,
|
||||
@RequestParam String path) {
|
||||
try {
|
||||
if ("local".equals(sessionId)) {
|
||||
localFileService.deleteFile(path);
|
||||
} else {
|
||||
sftpService.deleteFile(sessionId, path);
|
||||
}
|
||||
return ApiResponse.success("删除成功", null);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
@PostMapping("/batch-delete")
|
||||
public ApiResponse<LocalFileService.BatchDeleteResult> batchDelete(@RequestBody Map<String, Object> request) {
|
||||
try {
|
||||
String sessionId = (String) request.get("sessionId");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> paths = (List<String>) request.get("paths");
|
||||
|
||||
LocalFileService.BatchDeleteResult result;
|
||||
if ("local".equals(sessionId)) {
|
||||
result = localFileService.batchDelete(paths);
|
||||
} else {
|
||||
result = sftpService.batchDelete(sessionId, paths);
|
||||
}
|
||||
|
||||
return ApiResponse.success("删除完成", result);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("批量删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6.3 前端设计
|
||||
|
||||
### 6.3.1 删除交互
|
||||
|
||||
```javascript
|
||||
// 删除选中的文件
|
||||
function deleteSelectedFiles(panelId) {
|
||||
const selectedFiles = panelState[panelId].selectedFiles;
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
alert('请先选择要删除的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = panelState[panelId].sessionId;
|
||||
|
||||
// 确认对话框
|
||||
let message;
|
||||
if (selectedFiles.length === 1) {
|
||||
const fileName = getFileNameFromPath(selectedFiles[0]);
|
||||
message = `确定要删除 "${fileName}" 吗?`;
|
||||
} else {
|
||||
message = `确定要删除选中的 ${selectedFiles.length} 个文件吗?`;
|
||||
}
|
||||
|
||||
if (confirm(message)) {
|
||||
deleteFiles(sessionId, selectedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
function deleteFiles(sessionId, paths) {
|
||||
if (paths.length === 1) {
|
||||
// 单个删除
|
||||
$.ajax({
|
||||
url: '/api/files/delete',
|
||||
method: 'DELETE',
|
||||
data: {
|
||||
sessionId: sessionId,
|
||||
path: paths[0]
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('删除成功');
|
||||
refreshCurrentPanel();
|
||||
} else {
|
||||
alert('删除失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: handleError
|
||||
});
|
||||
} else {
|
||||
// 批量删除
|
||||
$.ajax({
|
||||
url: '/api/files/batch-delete',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
sessionId: sessionId,
|
||||
paths: paths
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
const result = response.data;
|
||||
let message = `成功删除 ${result.successCount} 个文件`;
|
||||
if (result.failCount > 0) {
|
||||
message += `,失败 ${result.failCount} 个\n`;
|
||||
message += '失败详情:\n' + result.failedFiles.join('\n');
|
||||
}
|
||||
alert(message);
|
||||
refreshCurrentPanel();
|
||||
} else {
|
||||
alert('批量删除失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: handleError
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新当前面板
|
||||
function refreshCurrentPanel() {
|
||||
const activePanelId = getActivePanelId();
|
||||
loadFiles(activePanelId);
|
||||
}
|
||||
|
||||
// 从路径获取文件名
|
||||
function getFileNameFromPath(path) {
|
||||
const index = path.lastIndexOf('/');
|
||||
if (index === -1) {
|
||||
return path;
|
||||
}
|
||||
return path.substring(index + 1);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3.2 删除按钮
|
||||
|
||||
```html
|
||||
<div class="toolbar">
|
||||
<button onclick="deleteSelectedFiles('left')">删除左侧</button>
|
||||
<button onclick="deleteSelectedFiles('right')">删除右侧</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 6.3.3 右键菜单删除
|
||||
|
||||
```javascript
|
||||
// 文件列表右键菜单
|
||||
function showContextMenu(event, panelId, path) {
|
||||
event.preventDefault();
|
||||
|
||||
// 创建右键菜单
|
||||
const menu = $('<div class="context-menu">');
|
||||
menu.css({
|
||||
position: 'absolute',
|
||||
left: event.pageX + 'px',
|
||||
top: event.pageY + 'px'
|
||||
});
|
||||
|
||||
menu.append('<div class="menu-item" onclick="deleteFile(\'' +
|
||||
panelId + '\', \'' + path + '\')">删除</div>');
|
||||
menu.append('<div class="menu-item" onclick="renameFile(\'' +
|
||||
panelId + '\', \'' + path + '\')">重命名</div>');
|
||||
|
||||
$('body').append(menu);
|
||||
|
||||
// 点击其他地方关闭菜单
|
||||
$(document).one('click', function() {
|
||||
menu.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// 删除单个文件(通过右键菜单)
|
||||
function deleteFile(panelId, path) {
|
||||
const sessionId = panelState[panelId].sessionId;
|
||||
const fileName = getFileNameFromPath(path);
|
||||
|
||||
if (confirm(`确定要删除 "${fileName}" 吗?`)) {
|
||||
deleteFiles(sessionId, [path]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6.4 安全措施
|
||||
|
||||
### 6.4.1 删除确认
|
||||
|
||||
- 必须用户确认后才能执行删除
|
||||
- 显示将被删除的文件数量和名称
|
||||
- 防止误操作
|
||||
|
||||
### 6.4.2 权限检查
|
||||
|
||||
```java
|
||||
// 检查文件删除权限
|
||||
private void checkDeletePermission(File file) throws Exception {
|
||||
if (!file.canWrite()) {
|
||||
throw new Exception("没有删除权限: " + file.getPath());
|
||||
}
|
||||
|
||||
// 检查是否为系统文件
|
||||
String systemPaths = "C:\\Windows,C:\\Program Files,C:\\System32";
|
||||
String[] paths = systemPaths.split(",");
|
||||
for (String systemPath : paths) {
|
||||
if (file.getPath().toLowerCase().startsWith(systemPath.toLowerCase())) {
|
||||
throw new Exception("系统文件,禁止删除: " + file.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4.3 操作日志
|
||||
|
||||
```java
|
||||
// 记录删除操作
|
||||
@Autowired
|
||||
private OperationLogService logService;
|
||||
|
||||
public boolean deleteFile(String path) throws Exception {
|
||||
File file = new File(path);
|
||||
|
||||
try {
|
||||
checkDeletePermission(file);
|
||||
boolean result = deleteFileInternal(file);
|
||||
|
||||
// 记录操作日志
|
||||
logService.logOperation("delete", "local", path, null, result, null);
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
// 记录失败日志
|
||||
logService.logOperation("delete", "local", path, null, false, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4.4 回收站机制(可选)
|
||||
|
||||
```java
|
||||
// 移动到回收站而不是直接删除
|
||||
public boolean moveToRecycleBin(String path) throws Exception {
|
||||
File file = new File(path);
|
||||
if (!file.exists()) {
|
||||
throw new Exception("文件不存在: " + path);
|
||||
}
|
||||
|
||||
// 创建回收站目录
|
||||
File recycleBin = new File(getRecycleBinPath());
|
||||
if (!recycleBin.exists()) {
|
||||
recycleBin.mkdirs();
|
||||
}
|
||||
|
||||
// 生成唯一文件名(避免重名)
|
||||
String newPath = recycleBin.getPath() + File.separator +
|
||||
file.getName() + "_" + System.currentTimeMillis();
|
||||
|
||||
return file.renameTo(new File(newPath));
|
||||
}
|
||||
|
||||
private String getRecycleBinPath() {
|
||||
return System.getProperty("user.home") + File.separator + ".sftp-manager-recycle";
|
||||
}
|
||||
```
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. **更新LocalFileService**:添加删除方法
|
||||
|
||||
2. **更新SftpService**:添加删除方法
|
||||
|
||||
3. **更新FileController**:添加删除接口
|
||||
|
||||
4. **添加前端删除功能**
|
||||
|
||||
5. **编译测试**
|
||||
```
|
||||
mvn clean compile
|
||||
```
|
||||
|
||||
6. **启动服务**
|
||||
```
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 1. 删除单个文件
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8080/sftp-manager/api/files/delete?sessionId=local&path=C:/test/file.txt"
|
||||
```
|
||||
|
||||
### 2. 批量删除
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/sftp-manager/api/files/batch-delete \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"sessionId": "local",
|
||||
"paths": ["C:/test/file1.txt", "C:/test/file2.txt"]
|
||||
}'
|
||||
```
|
||||
|
||||
### 3. 删除目录
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8080/sftp-manager/api/files/delete?sessionId=local&path=C:/test/folder"
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **递归删除**:删除目录时需要递归删除所有子文件和子目录
|
||||
2. **权限检查**:确保有删除权限,避免删除系统文件
|
||||
3. **错误处理**:部分文件删除失败时,继续删除其他文件,最后返回结果
|
||||
4. **确认机制**:必须用户确认后才能执行删除
|
||||
5. **日志记录**:记录所有删除操作,便于审计
|
||||
|
||||
## 下一步
|
||||
|
||||
完成模块06后,继续模块07:文件重命名功能
|
||||
Reference in New Issue
Block a user