Skip to content

Commit e8ea0ec

Browse files
feat(ui): 优化
1 parent 064f3e4 commit e8ea0ec

File tree

5 files changed

+606
-643
lines changed

5 files changed

+606
-643
lines changed

README.md

Lines changed: 10 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -9,52 +9,19 @@
99

1010
**现代化 S3 对象存储管理系统**
1111

12-
一个美观、强大、易用的 Web 文件管理器,支持所有 S3 兼容存储
12+
一个基于 Spring Boot 和 Vue.js 构建的 Web 文件管理器,支持所有 S3 兼容的对象存储服务
1313

14-
[✨ 特性](#-核心特性)[🚀 快速开始](#-快速开始)[📖 使用文档](#-功能详解)[🔧 配置说明](#-配置说明)
14+
[🚀 快速开始](#-快速开始)[🔧 配置说明](#-配置说明)[🛠️ 技术栈](#️-技术栈)
1515

1616
</div>
1717

1818
---
1919

20-
## ✨ 核心特性
21-
22-
### 🎨 现代化界面
23-
- **响应式设计** - 完美适配桌面端和移动端
24-
- **双视图模式** - 列表视图和网格视图自由切换
25-
- **深色模式支持** - 优雅的视觉体验
26-
- **流畅动画** - 精心设计的交互动效
27-
28-
### 📁 文件管理
29-
- **基础操作** - 上传、下载、删除、重命名、新建文件夹
30-
- **批量操作** - 支持多选文件进行批量下载、移动、复制、删除
31-
- **右键菜单** - 快捷的上下文菜单操作
32-
- **拖拽上传** - 支持拖拽文件到浏览器上传
33-
- **文件预览** - 支持图片、视频、音频、文档、代码文件预览
34-
- **Markdown 渲染** - 实时渲染 Markdown 文件
35-
- **代码高亮** - 支持多种编程语言语法高亮
36-
37-
### 🔍 搜索与筛选
38-
- **关键词搜索** - 快速查找文件
39-
- **高级筛选** - 按文件类型、时间范围筛选
40-
- **快捷日期选择** - 今天、最近7天、最近30天、最近一年
41-
42-
### 🚀 高级功能
43-
- **跨 Bucket 复制** - 在不同存储桶之间复制文件
44-
- **文件夹移动** - 支持文件和文件夹移动到其他目录
45-
- **分享链接生成** - 生成带过期时间的文件分享链接
46-
- **文件夹大小计算** - 递归计算文件夹总大小
47-
- **懒加载文件夹树** - 按需加载子文件夹,优化性能
48-
- **分页加载** - 每页加载 50 项,提升大量文件时的性能
49-
- **Service Worker 缓存** - 离线访问和快速加载
50-
- **缩略图优化** - 图片懒加载和缓存机制
51-
52-
### 🎯 用户体验
53-
- **面包屑导航** - 清晰的路径导航
54-
- **智能右键菜单** - 自动适配屏幕边界
55-
- **实时进度显示** - 上传、批量操作进度实时反馈
56-
- **Toast 通知** - 优雅的操作反馈提示
57-
- **可调节侧边栏** - 自由调整侧边栏宽度
20+
## 📖 项目简介
21+
22+
S3 File Nexus 是一个轻量级的 Web 文件管理系统,提供了类似操作系统文件管理器的用户体验。通过标准的 S3 协议与对象存储服务通信,无需额外的存储代理或中间层,可以直接管理您的云端文件。
23+
24+
5825

5926
## 🚀 快速开始
6027

@@ -92,90 +59,17 @@ mvn spring-boot:run -Dspring-boot.run.profiles=storage
9259
3. 点击"测试连接"验证配置
9360
4. 保存配置并返回首页
9461

95-
## 📖 功能详解
96-
97-
### 文件操作
98-
99-
#### 单文件操作
100-
- **右键菜单**:右键点击文件,选择操作
101-
- 预览 - 支持图片、视频、音频、文档、代码
102-
- 下载 - 直接下载文件
103-
- 重命名 - 修改文件名
104-
- 移动到... - 移动到其他文件夹
105-
- 复制到... - 复制到其他 Bucket
106-
- 生成分享链接 - 创建临时访问链接
107-
- 删除 - 删除文件
108-
109-
#### 批量操作
110-
1. 点击顶部工具栏的批量操作按钮(复选框图标)
111-
2. 选择多个文件(支持 Ctrl/Shift 多选)
112-
3. 使用底部浮动工具栏进行操作:
113-
- 批量下载
114-
- 批量移动
115-
- 批量复制到其他 Bucket
116-
- 批量删除
117-
118-
### 文件预览
119-
120-
支持以下文件类型的在线预览:
121-
122-
- **图片**:PNG, JPG, JPEG, GIF, BMP, WebP, SVG
123-
- 缩放、旋转、压缩显示
124-
- **视频**:MP4, WebM, OGG, AVI, MOV, WMV, FLV, MKV
125-
- **音频**:MP3, WAV, FLAC, AAC, M4A, WMA, OGG
126-
- **文档**:PDF, TXT, Markdown
127-
- **代码**:JavaScript, TypeScript, Python, Java, Go, Rust, C/C++, HTML, CSS, JSON, YAML 等
128-
- 语法高亮显示
129-
- **Office 文档**:DOC, DOCX, XLS, XLSX, PPT, PPTX
130-
- 通过 Google Docs Viewer 或 Microsoft Office Online 预览
131-
132-
### 搜索与筛选
133-
134-
#### 快速搜索
135-
在顶部搜索框输入关键词,按回车搜索
136-
137-
#### 高级筛选
138-
1. 点击"筛选"按钮展开筛选面板
139-
2. 设置筛选条件:
140-
- **文件类型**:文件夹、图片、视频、音频、文档、压缩包、代码、其他
141-
- **时间范围**:自定义开始和结束日期
142-
- **快速选择**:今天、最近7天、最近30天、最近一年
143-
144-
### 分享链接
145-
146-
1. 右键点击文件,选择"生成分享链接"
147-
2. 设置过期时间(1小时、6小时、1天、3天、7天)
148-
3. 点击"生成链接"
149-
4. 复制链接分享给他人
150-
151-
## 🔧 配置说明
152-
153-
### 存储后端配置
154-
155-
配置文件位置:`src/main/resources/application-storage.yml`
156-
157-
```yaml
158-
storage:
159-
default-backend: minio # 默认使用的存储后端
160-
backends:
161-
minio:
162-
enabled: true
163-
type: s3
164-
endpoint: http://localhost:9000
165-
access-key: your-access-key
166-
secret-key: your-secret-key
167-
region: us-east-1
168-
bucket-name: your-bucket
169-
```
17062

17163
### 支持的存储服务
17264

17365
本系统基于标准 S3 协议开发,理论上支持所有兼容 S3 协议的对象存储服务。
17466

17567
**已测试:**
68+
17669
-**自有 S3 协议的 OSS** - 已验证可用
17770

17871
**理论支持(S3 兼容):**
72+
17973
- 📦 **AWS S3** - Amazon Simple Storage Service
18074
- 📦 **MinIO** - 开源对象存储服务
18175
- 📦 **阿里云 OSS** - Alibaba Cloud Object Storage Service(S3 兼容模式)
@@ -184,56 +78,8 @@ storage:
18478
- 📦 **七牛云 Kodo** - Qiniu Cloud Object Storage(S3 兼容模式)
18579

18680
> 💡 **提示**:只要您的对象存储服务支持标准 S3 API,就可以使用本系统进行管理。如遇到兼容性问题,欢迎提 Issue 反馈。
81+
>
18782
188-
### 多后端配置
189-
190-
支持同时配置多个存储后端,在配置页面动态切换:
191-
192-
```yaml
193-
storage:
194-
default-backend: minio
195-
backends:
196-
minio:
197-
enabled: true
198-
type: s3
199-
# ... MinIO 配置
200-
aws:
201-
enabled: true
202-
type: s3
203-
# ... AWS S3 配置
204-
aliyun:
205-
enabled: true
206-
type: s3
207-
# ... 阿里云 OSS 配置
208-
```
209-
210-
## 🛠️ 技术栈
211-
212-
### 后端
213-
- **Spring Boot 3.4.8** - Java 应用框架
214-
- **AWS SDK for Java 2.x** - S3 客户端
215-
- **Spring Cache** - 缓存支持
216-
- **Caffeine** - 高性能缓存库
217-
218-
### 前端
219-
- **Vue.js 3.4** - 渐进式 JavaScript 框架
220-
- **Tailwind CSS 3.4** - 原子化 CSS 框架
221-
- **Axios** - HTTP 客户端
222-
- **Marked.js** - Markdown 解析器
223-
- **Highlight.js** - 代码语法高亮
224-
- **Bootstrap Icons** - 图标库
225-
226-
## 📝 开发计划
227-
228-
- [ ] 支持文件拖拽上传
229-
- [ ] 支持断点续传
230-
- [ ] 支持文件夹打包下载
231-
- [ ] 支持图片编辑功能
232-
- [ ] 支持在线编辑文本文件
233-
- [ ] 支持文件版本管理
234-
- [ ] 支持用户权限管理
235-
- [ ] 支持文件标签和分类
236-
- [ ] Docker 一键部署
23783

23884
## 📄 开源协议
23985

src/main/java/com/all/in/one/agent/storage/controller/OptimizedStorageController.java

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import java.util.ArrayList;
1616
import java.util.Comparator;
1717
import java.util.List;
18-
import java.util.concurrent.CompletableFuture;
1918
import java.util.stream.Collectors;
2019

2120
/**
@@ -152,24 +151,12 @@ private FileListResponse loadFromS3(String bucket, String prefix, String continu
152151
log.info("S3响应 - bucket: {}, prefix: '{}', CommonPrefixes: {}, Contents: {}",
153152
bucket, prefix, response.commonPrefixes().size(), response.contents().size());
154153

155-
// 添加文件夹(CommonPrefixes)
154+
// 添加文件夹(CommonPrefixes)- 不计算统计信息以提升性能
156155
for (CommonPrefix commonPrefix : response.commonPrefixes()) {
157-
// 异步计算文件夹统计信息(不阻塞主流程)
158-
FolderStats stats = FolderStats.builder()
159-
.calculating(true)
160-
.build();
161-
162-
// 启动异步任务计算统计信息
163156
String folderPrefix = commonPrefix.prefix();
164-
CompletableFuture.runAsync(() -> {
165-
try {
166-
calculateFolderStats(backendKey, bucket, folderPrefix);
167-
} catch (Exception e) {
168-
log.warn("计算文件夹统计信息失败: {}", folderPrefix, e);
169-
}
170-
});
171-
172-
items.add(FileItem.folder(folderPrefix, stats));
157+
158+
// 不计算统计信息,直接添加文件夹(性能优化)
159+
items.add(FileItem.folder(folderPrefix, null));
173160
}
174161

175162
// 添加文件
@@ -252,16 +239,15 @@ public FolderStats calculateFolderStats(String backendKey, String bucket, String
252239
}
253240

254241
/**
255-
* 智能排序:文件夹优先,然后按时间倒序(最新的在前)
242+
* 智能排序:文件夹优先,然后按名称排序
256243
*/
257244
private List<FileItem> sortItems(List<FileItem> items) {
258245
return items.stream()
259246
.sorted(Comparator
260-
// 1. 文件夹优先
247+
// 1. 文件夹优先(true > false,所以文件夹排在前面)
261248
.comparing(FileItem::isFolder, Comparator.reverseOrder())
262-
// 2. 按最后修改时间倒序(最新的在前,null值放最后)
263-
.thenComparing(FileItem::getLastModified,
264-
Comparator.nullsLast(Comparator.reverseOrder())))
249+
// 2. 按名称排序(忽略大小写)
250+
.thenComparing(item -> item.getName().toLowerCase()))
265251
.collect(Collectors.toList());
266252
}
267253

src/main/java/com/all/in/one/agent/storage/service/impl/StorageServiceImpl.java

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -463,19 +463,89 @@ public void deleteFile(String backendName, String bucketName, String objectKey)
463463
S3Client s3Client = s3ClientUtil.createS3Client(backend);
464464
String actualBucketName = bucketName != null ? bucketName : backend.getDefaultBucket();
465465

466-
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
467-
.bucket(actualBucketName)
468-
.key(objectKey)
469-
.build();
466+
// 如果是文件夹(以/结尾),需要递归删除所有子文件
467+
if (objectKey.endsWith("/")) {
468+
deleteFolderRecursively(s3Client, actualBucketName, objectKey);
469+
} else {
470+
// 普通文件直接删除
471+
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
472+
.bucket(actualBucketName)
473+
.key(objectKey)
474+
.build();
470475

471-
s3Client.deleteObject(deleteObjectRequest);
476+
s3Client.deleteObject(deleteObjectRequest);
477+
}
472478

473479
} catch (Exception e) {
474480
log.error("文件删除失败 - backend: {}, bucket: {}, key: {}", backendName, bucketName, objectKey, e);
475481
throw new RuntimeException("文件删除失败: " + e.getMessage());
476482
}
477483
}
478484

485+
/**
486+
* 递归删除文件夹及其所有内容
487+
*/
488+
private void deleteFolderRecursively(S3Client s3Client, String bucketName, String prefix) {
489+
try {
490+
log.info("开始递归删除文件夹: bucket={}, prefix={}", bucketName, prefix);
491+
492+
// 列出所有子对象(不使用delimiter,获取所有递归内容)
493+
software.amazon.awssdk.services.s3.model.ListObjectsV2Request listRequest =
494+
software.amazon.awssdk.services.s3.model.ListObjectsV2Request.builder()
495+
.bucket(bucketName)
496+
.prefix(prefix)
497+
.build();
498+
499+
software.amazon.awssdk.services.s3.model.ListObjectsV2Response listResponse;
500+
int totalDeleted = 0;
501+
502+
do {
503+
listResponse = s3Client.listObjectsV2(listRequest);
504+
log.info("列出对象: 找到 {} 个对象", listResponse.contents().size());
505+
506+
if (!listResponse.contents().isEmpty()) {
507+
// 逐个删除,避免批量删除的URI问题
508+
for (software.amazon.awssdk.services.s3.model.S3Object s3Object : listResponse.contents()) {
509+
try {
510+
DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
511+
.bucket(bucketName)
512+
.key(s3Object.key())
513+
.build();
514+
515+
s3Client.deleteObject(deleteRequest);
516+
totalDeleted++;
517+
518+
if (totalDeleted % 10 == 0) {
519+
log.info("已删除 {} 个对象", totalDeleted);
520+
}
521+
} catch (Exception e) {
522+
log.error("删除对象失败: {}", s3Object.key(), e);
523+
// 继续删除其他文件
524+
}
525+
}
526+
}
527+
528+
// 如果还有更多对象,继续删除
529+
if (listResponse.isTruncated()) {
530+
listRequest = listRequest.toBuilder()
531+
.continuationToken(listResponse.nextContinuationToken())
532+
.build();
533+
}
534+
} while (listResponse.isTruncated());
535+
536+
log.info("文件夹删除完成,共删除 {} 个对象: {}", totalDeleted, prefix);
537+
538+
// 如果一个对象都没删除,说明文件夹可能是空的或不存在
539+
if (totalDeleted == 0) {
540+
log.warn("文件夹为空或不存在: {}", prefix);
541+
}
542+
543+
} catch (Exception e) {
544+
log.error("递归删除文件夹失败 - bucket: {}, prefix: {}", bucketName, prefix, e);
545+
throw new RuntimeException("递归删除文件夹失败: " + e.getMessage());
546+
}
547+
}
548+
479549
@Override
480550
public void previewFile(String backendName, String bucketName, String objectKey, HttpServletResponse response) {
481551
StorageConfigProperties.Backend backend = getBackend(backendName);

0 commit comments

Comments
 (0)