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

14 KiB
Raw Blame History

模块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
  • 标准间距16px1rem
  • 组件内边距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删除方法

// 删除单个文件或目录
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删除方法

// 删除单个文件或目录
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删除接口

// 删除单个文件
@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 删除交互

// 删除选中的文件
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 删除按钮

<div class="toolbar">
    <button onclick="deleteSelectedFiles('left')">删除左侧</button>
    <button onclick="deleteSelectedFiles('right')">删除右侧</button>
</div>

6.3.3 右键菜单删除

// 文件列表右键菜单
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 权限检查

// 检查文件删除权限
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 操作日志

// 记录删除操作
@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 回收站机制(可选)

// 移动到回收站而不是直接删除
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. 删除单个文件

curl -X DELETE "http://localhost:8080/sftp-manager/api/files/delete?sessionId=local&path=C:/test/file.txt"

2. 批量删除

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. 删除目录

curl -X DELETE "http://localhost:8080/sftp-manager/api/files/delete?sessionId=local&path=C:/test/folder"

注意事项

  1. 递归删除:删除目录时需要递归删除所有子文件和子目录
  2. 权限检查:确保有删除权限,避免删除系统文件
  3. 错误处理:部分文件删除失败时,继续删除其他文件,最后返回结果
  4. 确认机制:必须用户确认后才能执行删除
  5. 日志记录:记录所有删除操作,便于审计

下一步

完成模块06后继续模块07文件重命名功能