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

687 lines
17 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.
# 模块08新建文件夹功能
---
## 🎨 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标准布局
---
## 8.1 功能概述
实现在本地文件系统和SFTP服务器上创建新文件夹的功能。
## 8.2 后端设计
### 8.2.1 LocalFileService创建目录方法
```java
// 创建目录
public boolean createDirectory(String path) throws Exception {
File directory = new File(path);
if (directory.exists()) {
throw new Exception("目录已存在: " + path);
}
// 验证目录名
String dirName = directory.getName();
if (!isValidDirectoryName(dirName)) {
throw new Exception("目录名包含非法字符: " + dirName);
}
// 检查父目录是否存在
File parentDir = directory.getParentFile();
if (parentDir != null && !parentDir.exists()) {
// 父目录不存在,询问是否创建多级目录
throw new Exception("父目录不存在: " + parentDir.getPath());
}
boolean result = directory.mkdirs();
if (!result) {
throw new Exception("创建目录失败");
}
return true;
}
// 创建多级目录
public boolean createDirectories(String path) throws Exception {
File directory = new File(path);
if (directory.exists()) {
throw new Exception("目录已存在: " + path);
}
// 验证路径中的每个目录名
String[] parts = path.split("[/\\\\]");
for (String part : parts) {
if (!isValidDirectoryName(part)) {
throw new Exception("路径包含非法字符: " + part);
}
}
boolean result = directory.mkdirs();
if (!result) {
throw new Exception("创建目录失败");
}
return true;
}
// 验证目录名是否有效
private boolean isValidDirectoryName(String dirName) {
if (dirName == null || dirName.isEmpty()) {
return false;
}
// Windows非法字符
String illegalChars = "\\/:*?\"<>|";
for (char c : illegalChars.toCharArray()) {
if (dirName.indexOf(c) != -1) {
return false;
}
}
// 检查长度限制
if (dirName.length() > 255) {
return false;
}
// 检查保留名称Windows
String upperName = dirName.toUpperCase();
String[] reservedNames = {"CON", "PRN", "AUX", "NUL"};
for (String reserved : reservedNames) {
if (upperName.equals(reserved)) {
return false;
}
}
return true;
}
```
### 8.2.2 SftpService创建目录方法
```java
// 创建目录
public boolean createDirectory(String sessionId, String path) throws Exception {
ChannelSftp channel = sessionManager.getSession(sessionId);
if (channel == null) {
throw new Exception("会话不存在或已断开");
}
try {
// 检查目录是否已存在
try {
channel.stat(path);
throw new Exception("目录已存在: " + path);
} catch (SftpException e) {
if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) {
throw e;
}
}
// 创建目录
channel.mkdir(path);
return true;
} catch (SftpException e) {
throw new Exception("创建目录失败: " + e.getMessage(), e);
}
}
// 创建多级目录
public boolean createDirectories(String sessionId, String path) throws Exception {
ChannelSftp channel = sessionManager.getSession(sessionId);
if (channel == null) {
throw new Exception("会话不存在或已断开");
}
try {
// 检查目录是否已存在
try {
channel.stat(path);
throw new Exception("目录已存在: " + path);
} catch (SftpException e) {
if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) {
throw e;
}
}
// 递归创建多级目录
createDirectoriesRecursive(channel, path);
return true;
} catch (SftpException e) {
throw new Exception("创建目录失败: " + e.getMessage(), e);
}
}
// 递归创建多级目录
private void createDirectoriesRecursive(ChannelSftp channel, String path) throws SftpException {
// 如果是根目录,直接返回
if (path.equals("/") || path.isEmpty()) {
return;
}
// 检查父目录是否存在
String parentPath = getParentPath(path);
if (!parentPath.equals(path)) {
try {
channel.stat(parentPath);
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
// 父目录不存在,递归创建
createDirectoriesRecursive(channel, parentPath);
} else {
throw e;
}
}
}
// 创建当前目录
channel.mkdir(path);
}
// 获取父路径
private String getParentPath(String path) {
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
int index = path.lastIndexOf("/");
if (index == -1) {
return "/";
}
if (index == 0) {
return "/";
}
return path.substring(0, index);
}
```
### 8.2.3 FileController创建目录接口
```java
// 创建目录
@PostMapping("/mkdir")
public ApiResponse<Void> createDirectory(@RequestBody Map<String, String> request) {
try {
String sessionId = request.get("sessionId");
String path = request.get("path");
if (path == null || path.isEmpty()) {
return ApiResponse.error("路径不能为空");
}
boolean result;
if ("local".equals(sessionId)) {
result = localFileService.createDirectory(path);
} else {
result = sftpService.createDirectory(sessionId, path);
}
if (result) {
return ApiResponse.success("创建成功", null);
} else {
return ApiResponse.error("创建失败");
}
} catch (Exception e) {
return ApiResponse.error("创建失败: " + e.getMessage());
}
}
// 创建多级目录
@PostMapping("/mkdir-p")
public ApiResponse<Void> createDirectories(@RequestBody Map<String, String> request) {
try {
String sessionId = request.get("sessionId");
String path = request.get("path");
if (path == null || path.isEmpty()) {
return ApiResponse.error("路径不能为空");
}
boolean result;
if ("local".equals(sessionId)) {
result = localFileService.createDirectories(path);
} else {
result = sftpService.createDirectories(sessionId, path);
}
if (result) {
return ApiResponse.success("创建成功", null);
} else {
return ApiResponse.error("创建失败");
}
} catch (Exception e) {
return ApiResponse.error("创建失败: " + e.getMessage());
}
}
```
## 8.3 前端设计
### 8.3.1 新建文件夹交互
```javascript
// 显示新建文件夹对话框
function showMkdirDialog(panelId) {
const sessionId = panelState[panelId].sessionId;
const currentPath = panelState[panelId].currentPath;
const folderName = prompt('请输入文件夹名称:', '新建文件夹');
if (folderName && folderName.trim() !== '') {
createDirectory(sessionId, currentPath, folderName);
}
}
// 创建目录
function createDirectory(sessionId, parentPath, folderName) {
// 验证文件夹名称
if (!validateDirectoryName(folderName)) {
return;
}
// 构建完整路径
let fullPath;
if ("local".equals(sessionId)) {
fullPath = parentPath + File.separator + folderName;
} else {
fullPath = parentPath.endsWith('/') ?
parentPath + folderName :
parentPath + '/' + folderName;
}
$.ajax({
url: '/api/files/mkdir',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
sessionId: sessionId,
path: fullPath
}),
success: function(response) {
if (response.success) {
alert('文件夹创建成功');
refreshCurrentPanel();
} else {
alert('创建失败: ' + response.message);
}
},
error: handleError
});
}
// 验证目录名
function validateDirectoryName(dirName) {
if (!dirName || dirName.trim() === '') {
alert('文件夹名称不能为空');
return false;
}
// Windows非法字符
const illegalChars = /\\\/:\*\?"<>\|/;
if (illegalChars.test(dirName)) {
alert('文件夹名称包含非法字符');
return false;
}
// 长度限制
if (dirName.length > 255) {
alert('文件夹名称过长最大255字符');
return false;
}
return true;
}
// 刷新当前面板
function refreshCurrentPanel() {
const activePanelId = getActivePanelId();
loadFiles(activePanelId);
}
```
### 8.3.2 新建文件夹按钮
```html
<div class="toolbar">
<button onclick="showMkdirDialog('left')">新建文件夹</button>
</div>
```
### 8.3.3 右键菜单新建文件夹
```javascript
// 在右键菜单中添加新建文件夹选项
function showContextMenu(event, panelId) {
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="showMkdirDialog(\'' +
panelId + '\')">新建文件夹</div>');
$('body').append(menu);
$(document).one('click', function() {
menu.remove();
});
}
```
### 8.3.4 快捷键支持
```javascript
// 监听键盘事件
$(document).on('keydown', function(e) {
// Ctrl+Shift+N: 新建文件夹
if (e.ctrlKey && e.shiftKey && e.key === 'N') {
const activePanelId = getActivePanelId();
showMkdirDialog(activePanelId);
e.preventDefault();
}
});
```
### 8.3.5 在线输入(高级功能)
```javascript
// 在文件列表中直接输入新文件夹名称
function showInlineMkdir(panelId) {
const fileList = $(`#${panelId}-file-list`);
// 创建新建文件夹项
const newItem = $(`
<div class="file-item new-folder">
<span class="file-icon">📁</span>
<span class="file-name">
<input type="text" class="new-folder-input" placeholder="输入文件夹名称">
</span>
</div>
`);
// 插入到列表顶部
fileList.prepend(newItem);
// 聚焦输入框
const input = newItem.find('.new-folder-input');
input.focus();
// 失去焦点时创建
input.on('blur', function() {
const folderName = input.val().trim();
if (folderName) {
createDirectory(panelState[panelId].sessionId,
panelState[panelId].currentPath,
folderName);
}
newItem.remove();
});
// 按Enter创建
input.on('keypress', function(e) {
if (e.which === 13) {
input.blur();
}
});
// 按Esc取消
input.on('keydown', function(e) {
if (e.which === 27) {
newItem.remove();
}
});
}
```
## 8.4 输入验证
### 8.4.1 前端验证
```javascript
// 验证目录名
function validateDirectoryName(dirName) {
// 基本验证
if (!dirName || dirName.trim() === '') {
alert('文件夹名称不能为空');
return false;
}
// 去除首尾空格
dirName = dirName.trim();
// Windows非法字符
const illegalChars = /\\\/:\*\?"<>\|/;
if (illegalChars.test(dirName)) {
alert('文件夹名称包含非法字符: \\ / : * ? " < > |');
return false;
}
// 不能以点开头或结尾Linux/Mac隐藏目录
if (dirName.startsWith('.') || dirName.endsWith('.')) {
alert('文件夹名称不能以点开头或结尾');
return false;
}
// 长度限制
if (dirName.length > 255) {
alert('文件夹名称过长最大255字符');
return false;
}
// Windows保留名称
const upperName = dirName.toUpperCase();
const reservedNames = ['CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
if (reservedNames.includes(upperName)) {
alert('文件夹名称是系统保留名称');
return false;
}
return true;
}
```
### 8.4.2 后端验证
```java
// 验证目录名(后端)
private boolean isValidDirectoryName(String dirName) {
if (dirName == null || dirName.isEmpty()) {
return false;
}
// 去除首尾空格
dirName = dirName.trim();
// Windows非法字符
String illegalChars = "\\/:*?\"<>|";
for (char c : illegalChars.toCharArray()) {
if (dirName.indexOf(c) != -1) {
return false;
}
}
// 不能以点开头或结尾
if (dirName.startsWith(".") || dirName.endsWith(".")) {
return false;
}
// 长度限制
if (dirName.length() > 255) {
return false;
}
// Windows保留名称
String upperName = dirName.toUpperCase();
String[] reservedNames = {"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"};
for (String reserved : reservedNames) {
if (upperName.equals(reserved)) {
return false;
}
}
return true;
}
```
### 8.4.3 路径处理
```javascript
// 拼接路径
function joinPath(parentPath, childName) {
// 移除子名称的首尾空格
childName = childName.trim();
// 判断是否为本地路径
const isLocal = parentPath.includes('\\') ||
parentPath.includes('C:') ||
parentPath.includes('D:');
if (isLocal) {
// Windows路径
if (parentPath.endsWith('\\') || parentPath.endsWith('/')) {
return parentPath + childName;
} else {
return parentPath + '\\' + childName;
}
} else {
// Linux/SFTP路径
if (parentPath.endsWith('/')) {
return parentPath + childName;
} else {
return parentPath + '/' + childName;
}
}
}
```
## 实施步骤
1. **更新LocalFileService**:添加创建目录方法
2. **更新SftpService**:添加创建目录方法
3. **更新FileController**:添加创建目录接口
4. **添加前端创建目录功能**
5. **编译测试**
```
mvn clean compile
```
6. **启动服务**
```
mvn spring-boot:run
```
## 测试验证
### 1. 创建单级目录
```bash
curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir \
-H "Content-Type: application/json" \
-d '{
"sessionId": "local",
"path": "C:/test/newfolder"
}'
```
### 2. 创建多级目录
```bash
curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir-p \
-H "Content-Type: application/json" \
-d '{
"sessionId": "local",
"path": "C:/test/level1/level2/level3"
}'
```
### 3. 在SFTP上创建目录
```bash
curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir \
-H "Content-Type: application/json" \
-d '{
"sessionId": "sftp-uuid",
"path": "/home/user/newfolder"
}'
```
### 4. 测试非法目录名
```bash
curl -X POST http://localhost:8080/sftp-manager/api/files/mkdir \
-H "Content-Type: application/json" \
-d '{
"sessionId": "local",
"path": "C:/test/folder<name"
}'
```
## 注意事项
1. **目录名验证**:严格验证目录名,防止非法字符
2. **路径拼接**:正确处理不同操作系统的路径分隔符
3. **权限检查**:确保有权限在指定位置创建目录
4. **同名检查**:创建前检查目录是否已存在
5. **父目录存在性**:单级创建时检查父目录是否存在
## 下一步
完成模块08后继续模块09双面板UI界面设计