Compare commits

...

129 Commits
2.8.1 ... 3.2.6

Author SHA1 Message Date
zhaojun
082bb07213 fix: 修复 webdav 账号密码错误问题 2022-06-16 20:11:54 +08:00
zhaojun
803b8cdf71 🔖 发布 3.2.5 版本 2022-05-16 21:41:59 +08:00
zhaojun
eca5f7e48b feature: 新增挂载 webdav client pom 依赖 2022-05-16 21:41:36 +08:00
zhaojun
b70c37f3f0 feature: 新增挂载 webdav 功能 2022-05-16 21:40:58 +08:00
zhaojun
5cb2844141 🐛 修复错误的 content_disposition 和 contentType 导致下载文件格式被浏览器识别错误 2022-05-16 20:50:49 +08:00
赵俊
4dd6cdb4b3 合并拉取请求 #348
fix: modify extract driveId regex in WebDavController
2022-05-03 21:16:40 +08:00
quericy
c6127c029f fix: modify extract driveId regex in WebDavController 2022-04-14 10:06:46 +08:00
zhaojun
e149039ecb 🔖 发布 3.2.3 版本 2022-04-13 00:03:49 +08:00
zhaojun
37688d83cf feature: 降低 webdav 日志级别 2022-04-13 00:03:28 +08:00
zhaojun
6692016642 feature: 新增 webdav 开关功能 2022-04-13 00:01:59 +08:00
zhaojun
3f41aeda9a fix(bug): 修复因字段名称修改升级后导致短链异常 bug 2022-04-12 23:38:49 +08:00
赵俊
de86d5c47d 合并拉取请求 #347
feat: support webdav
2022-04-11 09:56:11 +08:00
quericy
c89e072005 feat: support webdav
1, feat:config webDav and milton in configuration;
2, feat:add event handler in WebDavController;
3, feat:check auth by admin in systemConfig;
4, feat:download webDav file by redirect to site DirectLink;
2022-04-10 04:11:34 +08:00
zhaojun
b533b5e959 🔖 发布 3.2.2 版本 2022-04-02 18:50:50 +08:00
zhaojun
d35cf27d47 fix(bug): ftp 默认为被动模式,防止因端口不同,无法读取。 2022-04-02 18:50:26 +08:00
zhaojun
796c4c1fb0 页面更新 2022-04-02 18:49:26 +08:00
zhaojun
1033d6c1a9 fix(bug): 修复某些情况下直链无法正常使用的 BUG 2022-04-02 18:36:44 +08:00
zhaojun
e09c6b4e58 fix(bug): 修复又拍云文件中包含特殊字符时,调用 upyun 接口未进行 url encode 返回 400,影响又拍云单文件直链无法使用。 2022-04-02 18:26:32 +08:00
zhaojun
168b0b08f3 fix(bug): 修复本地存储可以通过特殊路径符获取任意目录的 BUG. 2022-04-02 18:24:49 +08:00
zhaojun
60a6a5348c fix(bug): 修复获取音频信息时,因文件链接 302, 导致无法正常获取音频文件信息的 BUG. 2022-04-02 18:24:00 +08:00
zhaojun
2ec8a5df1f fix(bug): 修复 sharepoint 未自动刷新 accessToken 和 refreshToken 的 bug 2022-02-02 20:50:13 +08:00
zhaojun
47b5f6ac12 🔖 发布 3.2.1 版本 2022-02-02 20:46:45 +08:00
zhaojun
c64c8465f2 fix(bug): 升级相关有漏洞的依赖版本为最新版 2022-02-02 20:46:08 +08:00
zhaojun
f636681dd8 fix(bug): 修复本地存储可通过特殊命令访问到非指定目录的文件 2022-02-02 20:45:05 +08:00
赵俊
eadd2434e0 新增 ZFILE 社区地址 2021-10-30 21:05:11 +08:00
赵俊
b84c0bff42 Merge remote-tracking branch 'origin/master' 2021-10-07 17:53:19 +08:00
赵俊
4cb5b84bfe 🐛 修复 S3 协议无法正常保存的 BUG 2021-10-07 17:52:45 +08:00
赵俊
0351b4401c Merge pull request #286 from pippen57/master
转义SQL关键字, 防止创建表时出现错误
2021-09-25 18:29:42 +08:00
pippen
48cb14be8a 转义SQL关键字, 防止创建表时出现错误 2021-09-25 12:48:31 +08:00
赵俊
eea2ff11f9 ✏️ 修改文档拼写错误 2021-09-21 11:22:00 +08:00
赵俊
325ec1a348 对于因前端 vue history 路由模式下, 直接访问路径时, 出现的 404 响应码, 转化为 200 响应码. (404 下会将请求转发到首页) 2021-09-20 17:06:27 +08:00
赵俊
93205266d3 更新文档 2021-09-20 16:35:30 +08:00
赵俊
6849a4347f 去除自定义 404 错误页面 2021-09-20 16:33:54 +08:00
赵俊
cb5c6a5945 🎨 提交前端页面 2021-09-20 16:33:31 +08:00
赵俊
5ed45c3bb3 🔒 修复因升级 springboot 版本导致的安全和兼容问题 2021-09-20 16:20:25 +08:00
赵俊
4442ec3165 修复因前端 vue history 路由模式下, 直接访问路径时, 请求 /admin/ 下的路由会被权限框架拦截并校验登陆状态, 导致静态页面报错问题. 2021-09-20 16:19:21 +08:00
赵俊
b268a24333 修复因前端 vue history 路由模式下, 直接访问路径时, 接口和路径名冲突问题 2021-09-20 16:17:59 +08:00
赵俊
84f9354d4e 对于因前端 vue history 路由模式下, 直接访问路径时, 出现的 404 响应码, 转化为 200 响应码. (404 下会将请求转发到首页) 2021-09-20 16:17:16 +08:00
赵俊
8dfc4f8004 跨域请求过滤器修改为自注解注册方式 2021-09-20 16:14:45 +08:00
赵俊
1158f5c2b9 🐛 修复 OneDrive 加速域名在直链下未生效的 BUG 2021-09-20 11:18:13 +08:00
赵俊
c5f0e15207 🐛 修复因升级 spring boot 后部分参数过时, 导致的第一次启动无法初始化参数的 BUG 2021-09-19 21:33:07 +08:00
赵俊
68a842ce75 🐛 修复页面路径和接口地址冲突的 BUG 2021-09-19 16:08:19 +08:00
赵俊
3bd4f74dae 🔖 发布 3.2 版本 2021-09-19 15:37:40 +08:00
赵俊
4a0bdc3baf 💬 修改文档和启动日志描述,取消 /#/ 提示 2021-09-19 15:37:10 +08:00
赵俊
603c1b4654 🎨 提交前端页面 2021-09-19 15:36:19 +08:00
赵俊
1136d582df 增加异常处理器,细化异常提示 2021-09-19 14:10:09 +08:00
赵俊
187544fc06 当密码文件无法正常打开时,给予友好提示 2021-09-19 14:09:52 +08:00
赵俊
aa3cde8f59 minio 和 s3 存储引擎支持自定义 region 功能 2021-09-19 11:30:05 +08:00
赵俊
e29a702b6e ✏️ 拼写错误修改 2021-09-19 11:28:40 +08:00
赵俊
e4f663c9f0 🔒 新增自动检测驱动器已存储参数和驱动器支持所有参数比对功能, 防止驱动器新增参数后, 系统已存在的驱动器因 NPE 问题无法正常加载的问题. 2021-09-19 11:27:57 +08:00
赵俊
fb08ef6e78 🐛 如果请求的密码文件链接是 http 的, 且会自动跳转到 https 时, 密码文件无法正常加载的 BUG (原因是 restTemplate http -> https 不会自动重定向) 2021-09-19 10:01:24 +08:00
赵俊
10c465d159 ♻️ 修改 Spring Security 为 Sa-Token 框架. 2021-09-19 09:57:09 +08:00
赵俊
d15d1203b7 跨域配置 2021-09-19 09:56:12 +08:00
赵俊
de48ed3b61 兼容前端 history 模式, 对于 404 请求, 指向到 /index.html 页面 2021-09-19 09:54:23 +08:00
赵俊
d22e2e872a ⬆️ 因旧版本 Spring 的安全问题, 升级 Spring 为 2.5.4 版本, 兼容新版本 2021-09-19 09:53:19 +08:00
赵俊
7da1b454dc 🐛 修复非个人版的 OneDrive CF 加速域名无法正常使用的 BUG 2021-09-18 22:22:50 +08:00
赵俊
463f311dd3 增加本地文件下载/预览行为控制 2021-07-13 21:43:25 +08:00
赵俊
73b42cf654 🔖 发布 3.1 版本 2021-05-30 17:43:37 +08:00
赵俊
1fac59c4cd 🎨 更新静态页面 2021-05-30 17:41:46 +08:00
赵俊
6ed6b4a019 驱动器添加时,默认 ID 修改为 1 2021-05-30 17:40:48 +08:00
赵俊
7bd02437f0 数据库初始化参数增加默认值 2021-05-30 17:40:14 +08:00
赵俊
57aeb5771c 驱动器列表中返回是否已初始化信息 2021-05-30 17:39:40 +08:00
赵俊
695c03a530 Merge remote-tracking branch 'origin/master' 2021-05-30 16:24:58 +08:00
赵俊
6ebc403572 🔧 添加 ZFile Banner 文件 2021-05-30 16:24:28 +08:00
赵俊
7ff6fe43b5 新增自定义直链名称功能功能 2021-05-30 16:23:41 +08:00
赵俊
b34f141181 🔥 移除无用代码 2021-05-30 16:21:11 +08:00
赵俊
87dd7b58d1 🔥 移除无用代码 2021-05-30 16:19:11 +08:00
赵俊
2a949db5d2 m3u8 响应头兼容部分浏览器直接解析 m3u8 文件 2021-05-30 15:53:55 +08:00
赵俊
b889e91fb5 📝 更新文档,更新赞助信息位置 2021-05-14 21:15:54 +08:00
赵俊
b5c757f9f0 📝 更新文档,更新赞助信息位置 2021-04-21 21:04:02 +08:00
赵俊
a465f48b94 📝 更新文档,增加项目 LOGO,修改项目描述信息。 2021-04-21 20:23:54 +08:00
赵俊
797a0a0e4c 新增直链和短链是否显示功能 2021-04-21 20:21:47 +08:00
赵俊
e7ff159b6d 🐛 修复某些下载地址带密钥的存储策略,m3u8 视频无法正常播放的功能 2021-04-11 19:15:50 +08:00
赵俊
a9fbf54bb2 🔊 完善日志输出 2021-04-11 16:02:49 +08:00
赵俊
81f9e262f5 增加获取站点域名方法 2021-04-11 16:00:01 +08:00
赵俊
23bb3960ab 已关闭的驱动器不允许下载 2021-04-11 15:56:08 +08:00
赵俊
c4a17985a4 优化下载地址获取逻辑,直接列表时不直接调用 API 获取下载地址,访问单文件时再调用。 2021-04-11 15:49:33 +08:00
赵俊
75ddcd47f4 :zip: 优化存储器保存逻辑,防止新增加的字段无法正常保存的 BUG。 2021-04-10 20:21:19 +08:00
赵俊
2dd03ae490 🐛 修复 FTP 模式在 Linux 环境下无法正常读取的 BUG 2021-04-10 20:20:03 +08:00
赵俊
5b383c8741 🐛 日志文件无法正常下载功能修复 2021-04-10 20:19:26 +08:00
赵俊
73198d7852 优化本地文件下载功能, 支持断点续传和多线程下载 2021-04-10 18:06:10 +08:00
赵俊
fb0d9721aa OneDrive 和 SharePoint 反代功能 2021-04-10 15:38:16 +08:00
赵俊
b24c663fd6 更新 star 趋势链接 2021-03-27 12:01:37 +08:00
zhaojun1998
6e62cfc84d 📝 更新文档,修改赞助二维码,增加服务器赞助商链接 2021-03-27 11:52:59 +08:00
zhaojun1998
eee22e9dc9 📝 更新文档 2021-03-26 18:24:02 +08:00
zhaojun1998
5109c51ffc 🔖 发布 3.0 版 2021-03-10 21:43:14 +08:00
zhaojun1998
66d6d311ea 🎨 更新静态页面 2021-03-10 21:42:57 +08:00
zhaojun1998
ef7cbdcbd7 🐛 修复旧版本时,直链多次创建后,根据条件查询时数据不唯一的 BUG 2021-03-10 21:42:36 +08:00
zhaojun1998
63bcbebb4b OneDrive 回调地址增加旧版本兼容 2021-03-10 21:41:42 +08:00
zhaojun1998
50ce1bb6db 修改直链 Key 时检测是否重复功能 2021-03-10 21:41:14 +08:00
zhaojun1998
3c88659679 🎨 更新静态页面 2021-03-09 21:14:42 +08:00
zhaojun1998
d79a993eea 新增设置默认打开图片模式功能 2021-03-09 21:14:21 +08:00
zhaojun1998
afafb311b8 合并文件列表和参数信息接口 2021-03-09 21:13:48 +08:00
zhaojun1998
2e280e4931 新增批量删除直链功能,修改直链 Key 功能 2021-03-08 21:12:42 +08:00
zhaojun1998
ed6efac8b7 🐛 修复切换存储策略时,存储参数值匹配错误的 BUG 2021-03-05 23:00:15 +08:00
zhaojun1998
7409df85d7 🐛 修复切换存储器 ID 时,为级联修改或清理缓存、过滤器、短链的问题 2021-03-05 22:59:08 +08:00
zhaojun1998
4d42529c4d 短链生成逻辑优化, 同链接不重复生成 2021-03-05 22:24:28 +08:00
zhaojun1998
65224685c8 短链生成逻辑优化, 同链接不重复生成 2021-03-05 22:23:41 +08:00
zhaojun1998
080a84986e 💩 优化代码, 添加注释, 优化方法命名, 2021-02-24 18:42:17 +08:00
zhaojun1998
3f8beb2f0b debug 模式优化, 增加到系统参数接口中 2021-02-24 18:40:10 +08:00
zhaojun1998
410a87c426 debug 模式优化, 增加到系统参数接口中 2021-02-24 18:39:47 +08:00
zhaojun1998
c14b8343d2 文件解析异常提示信息优化 2021-02-08 17:58:37 +08:00
zhaojun1998
bea440f6c3 文件解析异常提示信息优化 2021-02-08 17:50:25 +08:00
zhaojun1998
6a5fe15121 密码错误提示信息优化 2021-02-08 17:46:29 +08:00
zhaojun1998
e920ab0ec0 🐛 修复因缓存功能导致密码文件失效的 BUG 2021-02-08 17:46:04 +08:00
zhaojun1998
537e3e0563 🐛 修复 S3 协议新增时的 BUG 2021-02-03 22:01:56 +08:00
赵俊
e208dc7c4c Merge pull request #169 from tianyanli/master
修复S3协议没有指定region问题
2021-02-03 21:56:40 +08:00
zhaojun1998
ed64910a53 🔇 无效的驱动器不输出异常信息,仅返回给页面错误消息 2021-02-03 21:06:38 +08:00
zhaojun1998
5b075c3505 完善 SharePoint 支持 2021-02-03 21:05:46 +08:00
zhaojun1998
a8e6d9af6a 🐛 修复某些存储引擎 API 中文件夹不返回时间和大小时,排序的 NPE 问题。 2021-02-03 21:04:28 +08:00
zhaojun1998
2b21d8a73c 完善 SharePoint 自助获取 SiteId 的逻辑 2021-02-03 21:03:28 +08:00
zhaojun1998
e30289d21b 🔖 发布 2.9.0 版 2021-02-01 23:15:29 +08:00
zhaojun1998
3b6e2be7fe 新增 debug 模式, 可访问数据库控制台和重置密码 2021-02-01 23:14:29 +08:00
zhaojun1998
43c12aa8a7 🎨 更新静态页面 2021-02-01 23:13:29 +08:00
zhaojun1998
c03a7710c0 🎨 更新静态页面 2021-02-01 23:13:18 +08:00
zhaojun1998
1833b23d84 新增 sharepoint 及自助获取 siteId 功能 2021-02-01 23:11:49 +08:00
zhaojun1998
f3e393972d Merge remote-tracking branch 'origin/master' 2021-01-23 23:33:21 +08:00
zhaojun1998
4f46c13369 完善异常处理机制,新增异常类 2021-01-23 13:48:04 +08:00
zhaojun1998
f181959218 支持 SharePoint 2021-01-23 13:46:31 +08:00
zhaojun1998
11effc0ae7 支持自定义 client_id 和 client_secret 2021-01-23 13:45:26 +08:00
zhaojun1998
c8397e17bf 💩 优化代码, 抽取公共方法 2021-01-23 13:44:33 +08:00
zhaojun1998
ed32b9f1d4 限制直链不允许下载密码文件 2021-01-23 13:42:53 +08:00
zhaojun1998
4e184936db 💩 优化代码, 更新注释格式, 去除无用 import 2021-01-23 13:42:14 +08:00
zhaojun1998
fe6aebfdee 自带直链功能 2021-01-23 13:40:16 +08:00
zhaojun1998
d65e1a442d 🔥 移除系统检测功能 2021-01-23 13:36:56 +08:00
zhaojun1998
34647793c8 支持修改驱动器 ID 2021-01-23 13:35:52 +08:00
zhaojun1998
e8c249b9ea 🔥 移除系统检测功能 2021-01-23 13:22:41 +08:00
yanli
d1e613dc10 Update S3ServiceImpl.java 2020-12-17 17:26:55 +08:00
赵俊
1adcfee96f Update README.md 2020-10-07 17:19:26 +08:00
163 changed files with 3626 additions and 1458 deletions

View File

@@ -16,7 +16,7 @@ assignees: ''
请确认你已经做了下面这些事情,若 bug 还是未解决,那么请尽可详细地描述你的问题。
- 我已经安装了最新版的 ZFile
- 我已经阅读了 ZFile 的文档http://docs.zhaojun.im/zfile
- 我已经阅读了 ZFile 的文档https://docs.zfile.vip
- 我已经搜索了已有的 Issues 列表中有关的信息
- 我已经清理过浏览器缓存并重试
-->

125
README.md
View File

@@ -1,32 +1,49 @@
# Z-File
<p align = "center">
<img alt="ZFile" src="https://cdn.jun6.net/2021/04/21/69a89344e2a84.png" height="150px">
<br><br>
基于 Java 的在线网盘程序,支持对接 S3、OneDrive、SharePoint、又拍云、本地存储、FTP 等存储源,支持在线浏览图片、播放音视频,文本文件等文件类型。
<br><br>
<img src="https://img.shields.io/badge/license-MIT-blue.svg?longCache=true&style=flat-square">
<img src="https://api.codacy.com/project/badge/Grade/70b793267f7941d58cbd93f50c9a8e0a">
<img src="https://img.shields.io/github/last-commit/zhaojun1998/zfile.svg?style=flat-square">
<img src="https://img.shields.io/github/downloads/zhaojun1998/zfile/total?style=flat-square">
<img src="https://img.shields.io/github/v/release/zhaojun1998/zfile?style=flat-square">
<img src="https://img.shields.io/github/commit-activity/y/zhaojun1998/zfile?style=flat-square">
<br>
<img src="https://img.shields.io/github/issues/zhaojun1998/zfile?style=flat-square">
<img src="https://img.shields.io/github/issues-closed-raw/zhaojun1998/zfile?style=flat-square">
<img src="https://img.shields.io/github/forks/zhaojun1998/zfile?style=flat-square">
<img src="https://img.shields.io/github/stars/zhaojun1998/zfile?style=flat-square">
<img src="https://img.shields.io/github/watchers/zhaojun1998/zfile?style=flat-square">
</p>
![https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square](https://img.shields.io/badge/license-MIT-blue.svg?longCache=true&style=flat-square)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/70b793267f7941d58cbd93f50c9a8e0a)](https://www.codacy.com/manual/zhaojun1998/zfile?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=zhaojun1998/zfile&amp;utm_campaign=Badge_Grade)
![https://img.shields.io/badge/springboot-2.0.6-orange.svg?style=flat-square](https://img.shields.io/badge/springboot-2.0.6-yellow.svg?longCache=true&style=popout-square)
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/zhaojun1998/zfile.svg?style=flat-square)
## 相关地址
此项目是一个在线文件目录的程序, 支持各种对象存储和本地存储, 使用定位是个人放常用工具下载, 或做公共的文件库. 不会向多账户方向开发.
预览地址: [https://zfile.vip](https://zfile.vip)
前端基于 [h5ai](https://larsjung.de/h5ai/) 的原有功能使用 Vue 重新开发了一遍. 后端采用 SpringBoot, 数据库采用内嵌数据库.
文档地址: [https://docs.zfile.vip](https://docs.zfile.vip)
预览地址: [https://zfile.jun6.net](https://zfile.jun6.net)
社区地址: [https://bbs.zfile.vip](https://bbs.zfile.vip)
文档地址: [http://docs.zhaojun.im/zfile](http://docs.zhaojun.im/zfile)
项目源码: [https://github.com/zhaojun1998/zfile](https://github.com/zhaojun1998/zfile)
前端源码: [https://github.com/zhaojun1998/zfile-vue](https://github.com/zhaojun1998/zfile-vue)
## 系统特色
* 内存缓存 (免安装)
* 内存数据库 (免安装)
* 个性化配置
* 自定义目录的 readme 说明文件
* 自定义 JS, CSS
* 文件夹密码
* 目录 README 说明
* 文件直链(短链,永久直链,二维码)
* 支持在线浏览文本文件, 视频, 图片, 音乐. (支持 FLV 和 HLS)
* 文件/目录二维码
* 图片模式
* Docker 支持
* 隐藏指定文件夹(通配符支持)
* 自定义 JS, CSS
* 自定义目录 README 说明文件和密码文件名称
* 同时挂载多个存储策略
* 缓存动态开启, ~~缓存自动刷新 (v2.2 及以前版本支持)~~
* ~~全局搜索 (v2.2 及以前版本支持)~~
* 同时挂载多个存储策略
* 支持 阿里云 OSS, FTP, 华为云 OBS, 本地存储, MINIO, OneDrive 国际/家庭/个人版, OneDrive 世纪互联版, 七牛云 KODO, 腾讯云 COS, 又拍云 USS.
* 支持 S3 协议, 阿里云 OSS, FTP, 华为云 OBS, 本地存储, MINIO, OneDrive 国际/家庭/个人版/世纪互联版/SharePoint, , 七牛云 KODO, 腾讯云 COS, 又拍云 USS.
## 快速开始
@@ -54,14 +71,14 @@ apt update && apt install -y adoptopenjdk-8-hotspot-jre
> 如为更新程序, 则请先执行 `~/zfile/bin/stop.sh && rm -rf ~/zfile` 清理旧程序. 首次安装请忽略此选项.
下载项目:
```bash
cd ~
export ZFILE_INSTALL_PATH=~/zfile
mkdir -p $ZFILE_INSTALL_PATH && cd $ZFILE_INSTALL_PATH
wget https://c.jun6.net/ZFILE/zfile-release.war
mkdir zfile && unzip zfile-release.war -d zfile && rm -rf zfile-release.war
chmod +x zfile/bin/*.sh
unzip zfile-release.war && rm -rf zfile-release.war
chmod +x $ZFILE_INSTALL_PATH/bin/*.sh
```
> 下载指定版本可以将 `zfile-release.war` 改为 `zfile-x.x.war`,如 `zfile-2.2.war`。
@@ -83,62 +100,38 @@ chmod +x zfile/bin/*.sh
~/zfile/bin/start.sh
```
篇幅有限, 更详细的安装教程及介绍请参考: [ZFile 文档](http://zhaojun.im/zfile-install)
篇幅有限, 更详细的安装教程及介绍请参考: [ZFile 文档](https://docs.zfile.vip)
访问地址:
用户前台: http://127.0.0.1:8080/#/main
用户前台: http://127.0.0.1:8080/main
初始安装: http://127.0.0.1:8080/#/install
初始安装: http://127.0.0.1:8080/install
管理后台: http://127.0.0.1:8080/#/admin
管理后台: http://127.0.0.1:8080/admin
## 预览
![前台首页](https://cdn.jun6.net/2020/04/19/d590d2bde13bb.png)
![后台设置-驱动器设置](https://cdn.jun6.net/2020/04/19/d58fc2debcce8.png)
![后台设置-驱动器设置](https://cdn.jun6.net/2020/04/19/0f321e47fc18c.png)
![后台设置-显示设置](https://cdn.jun6.net/2020/04/19/6d7c300b89671.png)
## 常见问题
### 默认路径
默认 H2 数据库文件地址: `~/.zfile/db/`, `~` 表示用户目录
windows 为 `C:/Users/用户名/`
linux 为 `/home/用户名/`, root 用户为 `/root/`
> 2.3 及以后版本路径为 `~/.zfile-new/db/`
### 文档文件和加密文件
- 目录文档显示文件名为 `readme.md`
- 目录需要密码访问, 添加文件 `password.txt` (无法拦截此文件被下载, 但可以改名文件)
## 开发计划
- [x] API 支持 [点击查看文档](https://github.com/zhaojun1998/zfile/blob/master/API.md)
- [x] 更方便的部署方式
- [x] 布局优化 - 自定义操作按钮 (现为右键实现)
- [x] 后台优化 - 设置按照其功能进行分离
- [x] 体验优化 - 支持前后端分离部署
- [x] 体验优化 - 文本预览更换 vscode 同款编辑器 monaco editor
- [x] 新功能 - Docker 支持
- [x] 架构调整 - 支持多存储策略
- [ ] 新功能 - 后台支持上传、编辑、删除等操作
- [ ] 新功能 - WebDav 支持
- [ ] 新功能 - 离线下载 (aria2)
- [ ] 体验优化 - 忽略文件列表 (正则表达式)
- [ ] 体验优化 - 自定义支持预览的文件后缀 (正则表达式)
- [ ] 体验优化 - 一键安装脚本
![前台首页](https://cdn.jun6.net/2021/03/23/c1f4631ee2de4.png)
![图片预览](https://cdn.jun6.net/2021/03/23/713741d43b939.png)
![视频预览](https://cdn.jun6.net/2021/03/23/9c724383bb506.png)
![文本预览](https://cdn.jun6.net/2021/03/23/b00efdfb4892e.png)
![音频预览](https://cdn.jun6.net/2021/03/23/d15b14378d3f0.png)
![后台设置-驱动器列表](https://cdn.jun6.net/2021/03/23/b4f76f20ea73a.png)
![后台设置-新增驱动器](https://cdn.jun6.net/2021/03/23/e70e04f8cc5b6.png)
![后台设置-站点设置](https://cdn.jun6.net/2021/03/23/fd946991bb6b9.png)
## 支持作者
如果本项目对你有帮助,请作者喝杯咖啡吧。
<img src="http://cdn.jun6.net/alipay.png" width="200" height="312">
<img src="http://cdn.jun6.net/wechat.png" width="222" height="300">
<img src="https://cdn.jun6.net/2021/03/27/152704e91f13d.png" width="400" alt="赞助我">
## Stargazers over time
[![starcharts stargazers over time](https://starchart.cc/zhaojun1998/zfile.svg)](https://starchart.cc/zhaojun1998/zfile.svg)
## 开发工具赞助
<a href="https://www.jetbrains.com/?from=zfile"><img src="https://cdn.jun6.net/2021/04/21/26e410d60b0b0.png?1=1" width="100px"></a>

49
pom.xml
View File

@@ -6,19 +6,20 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<version>2.5.4</version>
<relativePath/>
</parent>
<groupId>im.zhaojun</groupId>
<artifactId>zfile</artifactId>
<version>2.8.1</version>
<version>3.2.5</version>
<name>zfile</name>
<packaging>war</packaging>
<description>一个在线的文件浏览系统</description>
<properties>
<java.version>1.8</java.version>
<log4j2.version>2.17.1</log4j2.version>
</properties>
<dependencies>
@@ -32,10 +33,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
@@ -60,11 +57,13 @@
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<version>1.4.197</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.27</version>
</dependency>
<!-- 工具类 -->
@@ -88,7 +87,6 @@
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.8</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
@@ -96,6 +94,31 @@
<version>3.6</version>
</dependency>
<!-- WebDav -->
<dependency>
<groupId>io.milton</groupId>
<artifactId>milton-server-ce</artifactId>
<version>3.1.1.413</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
<exclusion>
<artifactId>json</artifactId>
<groupId>org.json</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- webdav client -->
<dependency>
<groupId>com.github.lookfirst</groupId>
<artifactId>sardine</artifactId>
<version>5.10</version>
</dependency>
<!-- 其他工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
@@ -112,7 +135,15 @@
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.61</version>
<version>1.2.69</version>
</dependency>
<!-- Sa-Token 权限认证, 在线文档http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.26.0</version>
</dependency>
</dependencies>
@@ -145,4 +176,4 @@
</plugins>
</build>
</project>
</project>

View File

@@ -3,6 +3,7 @@ package im.zhaojun.zfile;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableAsync;
/**
@@ -10,6 +11,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
*/
@EnableAsync
@SpringBootApplication
@EnableJpaRepositories("im.zhaojun.zfile.repository")
@EnableAspectJAutoProxy(exposeProxy = true)
public class ZfileApplication {

View File

@@ -11,6 +11,7 @@ import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
/**
@@ -50,7 +51,7 @@ public class FileListCacheAspect {
if (enableCache) {
List<FileItemDTO> cacheFileList = zFileCache.get(driveId, path);
if (cacheFileList == null) {
result = (List<FileItemDTO>) point.proceed();
result = Collections.unmodifiableList((List<FileItemDTO>) point.proceed());
zFileCache.put(driveId, path, result);
} else {
result = cacheFileList;

View File

@@ -0,0 +1,36 @@
package im.zhaojun.zfile.config;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
/**
* 应用上下文配置
*
* @author me
* @date 2022/4/9
*/
@Configuration
public class ApplicationContextConfigure implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextConfigure.applicationContext = applicationContext;
}
/**
* bean名称获取对象
*/
public static Object getBean(String name) throws BeansException {
return applicationContext.getBean(name);
}
/**
* bean类型获取对象
*/
public static <T> T getBean(Class<T> clazz) throws BeansException {
return applicationContext.getBean(clazz);
}
}

View File

@@ -11,7 +11,7 @@ import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* @author zhaojun
@@ -31,7 +31,7 @@ public class OneDriveConfig {
ClientHttpRequestInterceptor interceptor = (httpRequest, bytes, clientHttpRequestExecution) -> {
HttpHeaders headers = httpRequest.getHeaders();
Integer driveId = Integer.valueOf(((LinkedList)headers.get("driveId")).get(0).toString());
Integer driveId = Integer.valueOf(((List)headers.get("driveId")).get(0).toString());
StorageConfig accessTokenConfig =
storageConfigService.findByDriveIdAndKey(driveId, StorageConfigConstant.ACCESS_TOKEN_KEY);

View File

@@ -0,0 +1 @@
package im.zhaojun.zfile.config;

View File

@@ -2,12 +2,19 @@ package im.zhaojun.zfile.config;
import im.zhaojun.zfile.model.enums.StorageTypeEnumDeSerializerConvert;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.HashSet;
import java.util.Set;
/**
* @author zhaojun
*/
@@ -31,4 +38,16 @@ public class WebMvcConfig implements WebMvcConfigurer {
return webServerFactory;
}
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
return factory -> {
ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/index.html");
ErrorPage error200Page = new ErrorPage(HttpStatus.OK, "/index.html");
Set<ErrorPage> errorPages = new HashSet<>();
errorPages.add(error404Page);
errorPages.add(error200Page);
factory.setErrorPages(errorPages);
};
}
}

View File

@@ -1,9 +1,14 @@
package im.zhaojun.zfile.config;
import im.zhaojun.zfile.filter.CorsFilter;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@@ -18,7 +23,10 @@ public class ZFileConfiguration {
@Bean
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
HttpClient httpClient = HttpClientBuilder.create().build();
httpRequestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> {

View File

@@ -0,0 +1,60 @@
package im.zhaojun.zfile.config.webdav;
import im.zhaojun.zfile.config.webdav.adapter.WebDavUrlAdapterImpl;
import im.zhaojun.zfile.config.webdav.auth.SystemConfigSecurityManager;
import im.zhaojun.zfile.config.webdav.resolver.WebDavRedirectViewResolver;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.service.SystemConfigService;
import io.milton.http.ResourceFactory;
import io.milton.http.SecurityManager;
import io.milton.http.annotated.AnnotationResourceFactory;
import io.milton.http.fs.NullSecurityManager;
import io.milton.servlet.DefaultMiltonConfigurator;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* Milton(webDav)配置
*
* @author me
* @date 2022/4/9
*/
@Configuration
public class MiltonConfiguration extends DefaultMiltonConfigurator implements InitializingBean {
/**
* 安全管理器
*/
private static SecurityManager securityManager = new NullSecurityManager();
@Resource
private SystemConfigService systemConfigService;
/**
* 构建milton初始化配置
*/
@Override
protected void build() {
builder.setSecurityManager(securityManager);
builder.setContextPath(ZFileConstant.WEB_DAV_PREFIX);
builder.setUrlAdapter(new WebDavUrlAdapterImpl());
final ResourceFactory resourceFactory = builder.getResourceFactory();
if (resourceFactory instanceof AnnotationResourceFactory) {
((AnnotationResourceFactory) resourceFactory).setViewResolver(new WebDavRedirectViewResolver());
}
super.build();
}
/**
* 属性初始化完成后,更新安全管理器,使用系统配置鉴权
*/
@Override
public void afterPropertiesSet() throws Exception {
final SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
if (systemConfig != null) {
securityManager = new SystemConfigSecurityManager(systemConfig);
}
}
}

View File

@@ -0,0 +1,33 @@
package im.zhaojun.zfile.config.webdav;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import io.milton.http.annotated.AnnotationResourceFactory;
import io.milton.servlet.MiltonFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* WebDav配置
*
* @author me
* @date 2022/4/9
*/
@Configuration
public class WebDavConfiguration {
@Bean
public FilterRegistrationBean miltonFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new MiltonFilter());
registration.setName("miltonFilter");
registration.addUrlPatterns(ZFileConstant.WEB_DAV_PREFIX + "/*");
registration.addInitParameter("resource.factory.class", AnnotationResourceFactory.class.getName());
registration.addInitParameter("milton.configurator", MiltonConfiguration.class.getName());
registration.addInitParameter("controllerPackagesToScan", "im.zhaojun.zfile.controller.home");
registration.setOrder(1);
return registration;
}
}

View File

@@ -0,0 +1,41 @@
package im.zhaojun.zfile.config.webdav.adapter;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.util.RegexMatchUtils;
import io.milton.http.HttpManager;
import io.milton.http.Request;
import io.milton.http.UrlAdapter;
import java.util.regex.Matcher;
/**
* WebDav路径适配器实现
*
* @author me
* @date 2022/4/10
*/
public class WebDavUrlAdapterImpl implements UrlAdapter {
/**
* 获取url
* eg: domain.com/{webdavPrefix}/{driveId}/{folders}
*
* @param request 请求
* @return {@link String}
*/
@Override
public String getUrl(Request request) {
// 匹配url前缀和驱动器ID
Matcher matcher = RegexMatchUtils.match("^" + ZFileConstant.WEB_DAV_PREFIX + "/(\\d+)(.*)",
HttpManager.decodeUrl(request.getAbsolutePath()));
final String driveId = RegexMatchUtils.getIndexResult(matcher, 1);
if (driveId == null) {
return "";
}
// 获取摘除前缀和驱动器ID后的文件路径
final String realPath = RegexMatchUtils.getIndexResult(matcher, 2);
return realPath != null ? realPath : "";
}
}

View File

@@ -0,0 +1,130 @@
package im.zhaojun.zfile.config.webdav.auth;
import cn.hutool.core.map.MapUtil;
import cn.hutool.crypto.SecureUtil;
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
import io.milton.http.Auth;
import io.milton.http.Request;
import io.milton.http.Request.Method;
import io.milton.http.http11.auth.DigestResponse;
import io.milton.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
/**
* 基于当前系统配置的WebDav鉴权管理器
*
* @author me
* @date 2022/4/10
* @see io.milton.http.fs.SimpleSecurityManager
*/
public class SystemConfigSecurityManager implements io.milton.http.SecurityManager {
private static final Logger log = LoggerFactory.getLogger(SystemConfigSecurityManager.class);
private String realm = "SystemConfig";
private Map<String, String> nameAndPasswords;
/**
* 根据系统配置创建安全管理器
*
* @param systemConfig 系统配置DTO
*/
public SystemConfigSecurityManager(SystemConfigDTO systemConfig) {
if (systemConfig != null) {
this.nameAndPasswords = MapUtil.of(systemConfig.getUsername(), systemConfig.getPassword());
}
}
public Object getUserByName(String name) {
String actualPassword = nameAndPasswords.get(name);
if (actualPassword != null) {
return name;
}
return null;
}
/**
* 用户名+密码身份验证
*
* @param user 用户
* @param password 密码
* @return {@link Object}
*/
@Override
public Object authenticate(String user, String password) {
if (user.contains("@")) {
user = user.substring(0, user.indexOf("@"));
}
String actualPassword = nameAndPasswords.get(user);
if (actualPassword == null) {
log.debug("user not found: " + user);
return null;
} else {
//比对密码MD5摘要
return (actualPassword.equals(SecureUtil.md5(password))) ? user : null;
}
}
/**
* 请求摘要身份验证(不进行换算)
*
* @param digestRequest 消化的请求
* @return {@link Object}
*/
@Override
public Object authenticate(DigestResponse digestRequest) {
String serverResponse = nameAndPasswords.get(digestRequest.getUser());
String clientResponse = digestRequest.getResponseDigest();
//比对密码MD5摘要
if (serverResponse.equals(SecureUtil.md5(clientResponse))) {
return "ok";
} else {
return null;
}
}
@Override
public boolean authorise(Request request, Method method, Auth auth, Resource resource) {
if (auth == null) {
log.trace("authorise: declining because there is no auth object");
return false;
} else {
if (auth.getTag() == null) {
log.trace("authorise: declining because there is no auth.getTag() object");
return false;
} else {
log.trace("authorise: permitting because there is an authenticated user associated with this request");
return true;
}
}
}
@Override
public String getRealm(String host) {
return realm;
}
/**
* @param realm the realm to set
*/
public void setRealm(String realm) {
this.realm = realm;
}
public void setNameAndPasswords(Map<String, String> nameAndPasswords) {
this.nameAndPasswords = nameAndPasswords;
}
@Override
public boolean isDigestAllowed() {
// 关闭请求摘要换算client端请求时若换算为摘要则无法和系统设置中获取的密码MD5比对
return false;
}
}

View File

@@ -0,0 +1,62 @@
package im.zhaojun.zfile.config.webdav.resolver;
import cn.hutool.core.util.URLUtil;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.entity.webdav.WebDavFile;
import io.milton.common.View;
import io.milton.http.template.TemplateProcessor;
import io.milton.http.template.ViewResolver;
import io.milton.servlet.OutputStreamWrappingHttpServletResponse;
import io.milton.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
/**
* WebDav重定向视图处理器
* Get注解handler返回字符串时将使用本ViewResolver处理
*
* @author me
* @date 2022/4/9
*/
public class WebDavRedirectViewResolver implements ViewResolver {
@Override
public TemplateProcessor resolveView(View view) {
return new RedirectTemplateProcessor();
}
/**
* 重定向模板处理程序
*
* @author me
* @date 2022/04/10
*/
public static class RedirectTemplateProcessor implements TemplateProcessor {
@Override
public void execute(Map<String, Object> model, OutputStream out) {
try {
// 获取要下载的资源文件
final Object resource = model.get("resource");
if (!(resource instanceof WebDavFile)) {
throw new RuntimeException("couldn't get direct url.");
}
final WebDavFile file = (WebDavFile) resource;
// 构造文件直链的路径
final String redirectPath = String.format("/%s/%s%s", ZFileConstant.DIRECT_LINK_PREFIX, file.getDriveId(), file.getFullPath());
// 重定向到直链
HttpServletResponse resp = new OutputStreamWrappingHttpServletResponse(ServletResponse.getResponse(), out);
resp.setStatus(301);
resp.setHeader("Location", URLUtil.encode(redirectPath));
resp.setHeader("Connection", "close");
resp.flushBuffer();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -128,4 +128,20 @@ public class DriveContext implements ApplicationContextAware {
return null;
}
/**
* 更新上下文环境中的驱动器 ID
*
* @param updateId
* 驱动器原 ID
*
* @param newId
* 驱动器新 ID
*/
public void updateDriveId(Integer updateId, Integer newId) {
AbstractBaseFileService fileService = drivesServiceMap.remove(updateId);
fileService.setDriveId(newId);
drivesServiceMap.put(newId, fileService);
}
}

View File

@@ -0,0 +1,33 @@
package im.zhaojun.zfile.controller.admin;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.service.SystemConfigService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
@Controller
public class DebugController {
@Value("${zfile.debug}")
private Boolean debug;
@Resource
private SystemConfigService systemConfigService;
@ResponseBody
@GetMapping("/debug/resetPwd")
public ResultBean resetPwd() {
if (debug) {
systemConfigService.updateUsernameAndPwd("admin", "123456");
return ResultBean.success();
} else {
return ResultBean.error("未开启 DEBUG 模式,不允许进行此操作。");
}
}
}

View File

@@ -32,6 +32,7 @@ public class DriveController {
@Resource
private FilterConfigService filterConfigService;
/**
* 获取所有驱动器列表
*
@@ -47,14 +48,14 @@ public class DriveController {
/**
* 获取指定驱动器基本信息及其参数
*
* @param id
* @param driveId
* 驱动器 ID
*
* @return 驱动器基本信息
*/
@GetMapping("/drive/{id}")
public ResultBean driveItem(@PathVariable Integer id) {
DriveConfigDTO driveConfig = driveConfigService.findDriveConfigDTOById(id);
@GetMapping("/drive/{driveId}")
public ResultBean driveItem(@PathVariable Integer driveId) {
DriveConfigDTO driveConfig = driveConfigService.findDriveConfigDTOById(driveId);
return ResultBean.success(driveConfig);
}
@@ -72,12 +73,12 @@ public class DriveController {
/**
* 删除驱动器设置
*
* @param id
* @param driveId
* 驱动器 ID
*/
@DeleteMapping("/drive/{id}")
public ResultBean deleteDriveItem(@PathVariable Integer id) {
driveConfigService.deleteById(id);
@DeleteMapping("/drive/{driveId}")
public ResultBean deleteDriveItem(@PathVariable Integer driveId) {
driveConfigService.deleteById(driveId);
return ResultBean.success();
}
@@ -85,14 +86,14 @@ public class DriveController {
/**
* 启用驱动器
*
* @param id
* @param driveId
* 驱动器 ID
*/
@PostMapping("/drive/{id}/enable")
public ResultBean enable(@PathVariable("id") Integer id) {
DriveConfig driveConfig = driveConfigService.findById(id);
@PostMapping("/drive/{driveId}/enable")
public ResultBean enable(@PathVariable Integer driveId) {
DriveConfig driveConfig = driveConfigService.findById(driveId);
driveConfig.setEnable(true);
driveConfigService.saveOrUpdate(driveConfig);
driveConfigService.updateDriveConfig(driveConfig);
return ResultBean.success();
}
@@ -100,33 +101,73 @@ public class DriveController {
/**
* 停止驱动器
*
* @param id
* @param driveId
* 驱动器 ID
*/
@PostMapping("/drive/{id}/disable")
public ResultBean disable(@PathVariable("id") Integer id) {
DriveConfig driveConfig = driveConfigService.findById(id);
@PostMapping("/drive/{driveId}/disable")
public ResultBean disable(@PathVariable Integer driveId) {
DriveConfig driveConfig = driveConfigService.findById(driveId);
driveConfig.setEnable(false);
driveConfigService.saveOrUpdate(driveConfig);
driveConfigService.updateDriveConfig(driveConfig);
return ResultBean.success();
}
@GetMapping("/drive/{id}/filters")
public ResultBean getFilters(@PathVariable("id") Integer id) {
return ResultBean.success(filterConfigService.findByDriveId(id));
/**
* 根据驱动器 ID 获取过滤文件列表
*
* @param driveId
* 驱动器 ID
*/
@GetMapping("/drive/{driveId}/filters")
public ResultBean getFilters(@PathVariable Integer driveId) {
return ResultBean.success(filterConfigService.findByDriveId(driveId));
}
@PostMapping("/drive/{id}/filters")
public ResultBean saveFilters(@RequestBody List<FilterConfig> filter, @PathVariable("id") Integer driveId) {
/**
* 停止驱动器
*
* @param driveId
* 驱动器 ID
*/
@PostMapping("/drive/{driveId}/filters")
public ResultBean saveFilters(@RequestBody List<FilterConfig> filter, @PathVariable Integer driveId) {
filterConfigService.batchSave(filter, driveId);
return ResultBean.success();
}
/**
* 保存拖拽排序信息
*
* @param driveConfigs
* 拖拽排序信息
*/
@PostMapping("/drive/drag")
public ResultBean saveDriveDrag(@RequestBody List<JSONObject> driveConfigs) {
driveConfigService.saveDriveDrag(driveConfigs);
return ResultBean.success();
}
/**
* 更新驱动器 ID
*
* @param updateId
* 驱动器原 ID
*
* @param newId
* 驱动器新 ID
*/
@PostMapping("/drive/updateId")
public ResultBean updateDriveId(Integer updateId, Integer newId) {
DriveConfig driveConfig = driveConfigService.findById(newId);
if (driveConfig != null) {
return ResultBean.error("已存在的 ID请更换 ID 后重试。");
}
driveConfigService.updateId(updateId, newId);
return ResultBean.success();
}
}

View File

@@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.util.Date;
@@ -26,14 +25,14 @@ public class LogController {
* 系统日志下载
*/
@GetMapping("/log")
public ResponseEntity<Object> downloadLog(HttpServletResponse response) {
public ResponseEntity<Object> downloadLog() {
if (log.isDebugEnabled()) {
log.debug("下载诊断日志");
}
String userHome = System.getProperty("user.home");
File fileZip = ZipUtil.zip(userHome + "/.zfile/logs");
String currentDate = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss");
return FileUtil.export(fileZip, "ZFile 诊断日志 - " + currentDate + ".zip");
return FileUtil.exportSingleThread(fileZip, "ZFile 诊断日志 - " + currentDate + ".zip");
}
}

View File

@@ -0,0 +1 @@
package im.zhaojun.zfile.controller.admin;

View File

@@ -1,26 +0,0 @@
package im.zhaojun.zfile.controller.admin;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.model.support.SystemMonitorInfo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 系统监控 Controller
* @author zhaojun
*/
@RestController
@RequestMapping("/admin")
public class MonitorController {
/**
* 获取系统监控信息
*/
@GetMapping("monitor")
public ResultBean monitor() {
return ResultBean.success(new SystemMonitorInfo());
}
}

View File

@@ -0,0 +1,45 @@
package im.zhaojun.zfile.controller.admin;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.service.ShortLinkConfigService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
/**
* 直链管理 Controller
*
* @author zhaojun
*/
@Controller
@RequestMapping("/admin")
public class ShortLinkManagerController {
@Resource
private ShortLinkConfigService shortLinkConfigService;
@GetMapping("/link/list")
@ResponseBody
public ResultBean list(String key,
String url,
String dateFrom,
String dateTo,
Integer page,
Integer limit,
@RequestParam(required = false, defaultValue = "createDate") String orderBy,
@RequestParam(required = false, defaultValue = "desc") String orderDirection) {
return ResultBean.success(shortLinkConfigService.find(key, url, dateFrom, dateTo, page, limit, orderBy, orderDirection));
}
@GetMapping("/link/delete/{id}")
@ResponseBody
public ResultBean deleteById(@PathVariable Integer id) {
shortLinkConfigService.deleteById(id);
return ResultBean.success();
}
}

View File

@@ -1,11 +1,17 @@
package im.zhaojun.zfile.controller.home;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import im.zhaojun.zfile.context.DriveContext;
import im.zhaojun.zfile.exception.NotEnabledDriveException;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.entity.DriveConfig;
import im.zhaojun.zfile.model.enums.FileTypeEnum;
import im.zhaojun.zfile.service.DriveConfigService;
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
import im.zhaojun.zfile.util.HttpUtil;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.GetMapping;
@@ -14,9 +20,13 @@ import org.springframework.web.servlet.HandlerMapping;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Objects;
/**
* 直链 Controller
* @author Zhao Jun
*/
@Controller
@@ -25,6 +35,9 @@ public class DirectLinkController {
@Resource
private DriveContext driveContext;
@Resource
private DriveConfigService driveConfigService;
/**
* 获取指定驱动器, 某个文件的直链, 然后重定向过去.
* @param driveId
@@ -32,8 +45,16 @@ public class DirectLinkController {
*
* @return 重定向至文件直链
*/
@GetMapping("/directlink/{driveId}/**")
public String directlink(@PathVariable("driveId") Integer driveId, final HttpServletRequest request) {
@GetMapping("/${zfile.directLinkPrefix}/{driveId}/**")
public String directlink(@PathVariable("driveId") Integer driveId,
final HttpServletRequest request,
final HttpServletResponse response) throws IOException {
DriveConfig driveConfig = driveConfigService.findById(driveId);
Boolean enable = driveConfig.getEnable();
if (!enable) {
throw new NotEnabledDriveException();
}
String path = (String) request.getAttribute(
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
@@ -49,6 +70,16 @@ public class DirectLinkController {
String url = fileItem.getUrl();
if (StrUtil.equalsIgnoreCase(FileUtil.extName(fileItem.getName()), "m3u8")) {
String textContent = HttpUtil.getTextContent(url);
response.setContentType("application/vnd.apple.mpegurl;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(textContent);
out.flush();
out.close();
return null;
}
int queryIndex = url.indexOf('?');
if (queryIndex != -1) {

View File

@@ -1,15 +1,19 @@
package im.zhaojun.zfile.controller.home;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import im.zhaojun.zfile.context.DriveContext;
import im.zhaojun.zfile.exception.NotExistFileException;
import im.zhaojun.zfile.exception.NotEnabledDriveException;
import im.zhaojun.zfile.exception.PasswordVerifyException;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.dto.DriveListDTO;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.dto.FileListDTO;
import im.zhaojun.zfile.model.dto.SystemFrontConfigDTO;
import im.zhaojun.zfile.model.entity.DriveConfig;
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import im.zhaojun.zfile.model.support.FilePageModel;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.model.support.VerifyResult;
import im.zhaojun.zfile.service.DriveConfigService;
import im.zhaojun.zfile.service.FilterConfigService;
import im.zhaojun.zfile.service.SystemConfigService;
@@ -18,6 +22,7 @@ import im.zhaojun.zfile.util.FileComparator;
import im.zhaojun.zfile.util.HttpUtil;
import im.zhaojun.zfile.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -27,10 +32,9 @@ import org.springframework.web.client.HttpClientErrorException;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* 前台文件管理
@@ -41,6 +45,9 @@ import java.util.Objects;
@RestController
public class FileController {
@Value("${zfile.debug}")
private Boolean debug;
@Resource
private SystemConfigService systemConfigService;
@@ -53,11 +60,6 @@ public class FileController {
@Resource
private FilterConfigService filterConfigService;
/**
* 滚动加载每页条数.
*/
private static final Integer PAGE_SIZE = 30;
/**
* 获取所有已启用的驱动器
@@ -66,11 +68,14 @@ public class FileController {
*/
@GetMapping("/drive/list")
public ResultBean drives() {
return ResultBean.success(driveConfigService.listOnlyEnable());
List<DriveConfig> driveList = driveConfigService.listOnlyEnable();
boolean isInstall = systemConfigService.getIsInstall();
DriveListDTO driveListDTO = new DriveListDTO(driveList, isInstall);
return ResultBean.success(driveListDTO);
}
/**
* 获取某个驱动器下, 指定路径的数据, 每页固定 {@link #PAGE_SIZE} 条数据.
* 获取某个驱动器下, 指定路径的数据
*
* @param driveId
* 驱动器 ID
@@ -81,19 +86,75 @@ public class FileController {
* @param password
* 文件夹密码, 某些文件夹需要密码才能访问, 当不需要密码时, 此参数可以为空
*
* @param page
* 页数
*
* @return 当前路径下所有文件及文件夹
*/
@GetMapping("/list/{driveId}")
public ResultBean list(@PathVariable(name = "driveId") Integer driveId,
@RequestParam(defaultValue = "/") String path,
@RequestParam(required = false) String password,
@RequestParam(defaultValue = "1") Integer page) throws Exception {
@RequestParam(required = false) String orderBy,
@RequestParam(required = false, defaultValue = "asc") String orderDirection) throws Exception {
AbstractBaseFileService fileService = driveContext.get(driveId);
List<FileItemDTO> fileItemList = fileService.fileList(StringUtils.removeDuplicateSeparator(ZFileConstant.PATH_SEPARATOR + path + ZFileConstant.PATH_SEPARATOR));
// 创建副本, 防止排序和过滤对原数据产生影响
List<FileItemDTO> copyList = new ArrayList<>(fileItemList);
// 校验密码, 如果校验不通过, 则返回错误消息
VerifyResult verifyResult = verifyPassword(copyList, driveId, path, password);
if (!verifyResult.isPassed()) {
return ResultBean.error(verifyResult.getMsg(), verifyResult.getCode());
}
// 过滤掉驱动器配置的表达式中要隐藏的数据
filterFileList(copyList, driveId);
// 按照自然排序
copyList.sort(new FileComparator(orderBy, orderDirection));
// 开始获取参数信息
SystemFrontConfigDTO systemConfig = systemConfigService.getSystemFrontConfig(driveId);
DriveConfig driveConfig = driveConfigService.findById(driveId);
Boolean enable = driveConfig.getEnable();
if (!enable) {
throw new NotEnabledDriveException();
}
systemConfig.setDebugMode(debug);
systemConfig.setDefaultSwitchToImgMode(driveConfig.getDefaultSwitchToImgMode());
systemConfig.setDirectLinkPrefix(ZFileConstant.DIRECT_LINK_PREFIX);
// 如果不是 FTP 模式,则尝试获取当前文件夹中的 README 文件,有则读取,没有则停止
if (!Objects.equals(driveConfig.getType(), StorageTypeEnum.FTP)) {
fileItemList.stream()
.filter(fileItemDTO -> Objects.equals(ZFileConstant.README_FILE_NAME, fileItemDTO.getName()))
.findFirst()
.ifPresent(fileItemDTO -> {
String readme = HttpUtil.getTextContent(fileItemDTO.getUrl());
systemConfig.setReadme(readme);
});
}
return ResultBean.successData(new FileListDTO(copyList, systemConfig));
}
/**
* 校验密码
* @param fileItemList
* 文件列表
* @param driveId
* 驱动器 ID
* @param path
* 请求路径
* @param inputPassword
* 用户输入的密码
* @return 是否校验通过
*/
private VerifyResult verifyPassword(List<FileItemDTO> fileItemList, Integer driveId, String path, String inputPassword) {
AbstractBaseFileService fileService = driveContext.get(driveId);
List<FileItemDTO> fileItemList =
fileService.fileList(StringUtils.removeDuplicateSeparator(ZFileConstant.PATH_SEPARATOR + path + ZFileConstant.PATH_SEPARATOR));
for (FileItemDTO fileItemDTO : fileItemList) {
if (ZFileConstant.PASSWORD_FILE_NAME.equals(fileItemDTO.getName())) {
@@ -101,143 +162,82 @@ public class FileController {
try {
expectedPasswordContent = HttpUtil.getTextContent(fileItemDTO.getUrl());
} catch (HttpClientErrorException httpClientErrorException) {
log.error("尝试重新获取密码文件缓存中链接后仍失败, driveId: {}, path: {}, inputPassword: {}, passwordFile:{} ",
driveId, path, password, JSON.toJSONString(fileItemDTO), httpClientErrorException);
log.trace("尝试重新获取密码文件缓存中链接后仍失败, driveId: {}, path: {}, inputPassword: {}, passwordFile:{} ",
driveId, path, inputPassword, JSON.toJSONString(fileItemDTO), httpClientErrorException);
try {
String fullPath = StringUtils.removeDuplicateSeparator(fileItemDTO.getPath() + ZFileConstant.PATH_SEPARATOR + fileItemDTO.getName());
FileItemDTO fileItem = fileService.getFileItem(fullPath);
expectedPasswordContent = HttpUtil.getTextContent(fileItem.getUrl());
String pwdFileFullPath = StringUtils.removeDuplicateSeparator(fileItemDTO.getPath() + ZFileConstant.PATH_SEPARATOR + fileItemDTO.getName());
FileItemDTO pwdFileItem = fileService.getFileItem(pwdFileFullPath);
expectedPasswordContent = HttpUtil.getTextContent(pwdFileItem.getUrl());
} catch (Exception e) {
log.error("尝试重新获取密码文件链接后仍失败, 已暂时取消密码", e);
break;
throw new PasswordVerifyException("此文件夹为加密文件夹, 但密码检查异常, 请联系管理员检查密码设置", e);
}
} catch (Exception e) {
throw new PasswordVerifyException("此文件夹为加密文件夹, 但密码检查异常, 请联系管理员检查密码设置", e);
}
if (Objects.equals(expectedPasswordContent, password)) {
if (matchPassword(expectedPasswordContent, inputPassword)) {
break;
}
if (password != null && !"".equals(password)) {
return ResultBean.error("密码错误.", ResultBean.INVALID_PASSWORD);
if (StrUtil.isEmpty(inputPassword)) {
return VerifyResult.fail("此文件夹需要密码.", ResultBean.REQUIRED_PASSWORD);
}
return ResultBean.error("此文件夹需要密码.", ResultBean.REQUIRED_PASSWORD);
return VerifyResult.fail("密码错误.", ResultBean.INVALID_PASSWORD);
}
}
// 过滤掉表达式中不存在的数据.
fileItemList.removeIf(next -> filterConfigService.filterResultIsHidden(driveId, StringUtils.concatUrl(next.getPath(), next.getName())));
return ResultBean.successData(getSortedPagingData(fileItemList, page));
return VerifyResult.success();
}
/**
* 获取系统配置信息和当前页的标题, 页面文档信息
* 校验两个密码是否相同, 忽略空白字符
*
* @param driveId
* 驱动器 ID
* @param expectedPasswordContent
* 预期密码
*
* @return 返回指定驱动器的系统配置信息
* @param password
* 实际输入密码
*
* @return 是否匹配
*/
@GetMapping("/config/{driveId}")
public ResultBean getConfig(@PathVariable(name = "driveId") Integer driveId, String path) {
SystemFrontConfigDTO systemConfig = systemConfigService.getSystemFrontConfig(driveId);
AbstractBaseFileService fileService = driveContext.get(driveId);
DriveConfig driveConfig = driveConfigService.findById(driveId);
String fullPath = StringUtils.removeDuplicateSeparator(path + ZFileConstant.PATH_SEPARATOR + ZFileConstant.README_FILE_NAME);
FileItemDTO fileItem = null;
try {
fileItem = fileService.getFileItem(fullPath);
if (!Objects.equals(driveConfig.getType(), StorageTypeEnum.FTP)) {
String readme = HttpUtil.getTextContent(fileItem.getUrl());
systemConfig.setReadme(readme);
}
} catch (Exception e) {
if (e instanceof NotExistFileException) {
log.debug("不存在 README 文件, 已跳过, fullPath: {}, fileItem: {}", fullPath, JSON.toJSONString(fileItem));
} else {
log.error("获取 README 文件异常, fullPath: {}, fileItem: {}", fullPath, JSON.toJSONString(fileItem), e);
}
private boolean matchPassword(String expectedPasswordContent, String password) {
if (Objects.equals(expectedPasswordContent, password)) {
return true;
}
return ResultBean.successData(systemConfig);
}
if (expectedPasswordContent == null) {
return false;
}
if (password == null) {
return false;
}
@GetMapping("/search/{driveId}")
public ResultBean search(@RequestParam(value = "name", defaultValue = "/") String name,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "asc") String order,
@RequestParam(defaultValue = "1") Integer page,
@PathVariable("driveId") Integer driveId) {
return ResultBean.error("暂不支持搜索功能");
expectedPasswordContent = expectedPasswordContent.replace("\n", "").trim();
password = password.replace("\n", "").trim();
return Objects.equals(expectedPasswordContent, password);
}
/**
* 过滤文件列表, 去除密码, 文档文件.
* 过滤文件列表, 去除密码, 文档文件和此驱动器通过规则过滤的文件.
*
* @param fileItemList
* 文件列表
* @param driveId
* 驱动器 ID
*/
private void filterFileList(List<FileItemDTO> fileItemList) {
private void filterFileList(List<FileItemDTO> fileItemList, Integer driveId) {
if (fileItemList == null) {
return;
}
fileItemList.removeIf(fileItem -> ZFileConstant.PASSWORD_FILE_NAME.equals(fileItem.getName())
|| ZFileConstant.README_FILE_NAME.equals(fileItem.getName()));
}
/**
* 对传入的文件列表, 按照文件名进行排序, 然后取相应页数的文件
*
* @param fileItemList
* 文件列表
*
* @param page
* 要取的页数
*
* @return 排序及分页后的那段数据
*/
private FilePageModel getSortedPagingData(List<FileItemDTO> fileItemList, Integer page) {
ArrayList<FileItemDTO> copy = new ArrayList<>(Arrays.asList(new FileItemDTO[fileItemList.size()]));
Collections.copy(copy, fileItemList);
// 排序, 先按照文件类型比较, 文件夹在前, 文件在后, 然后根据 sortBy 字段排序, 默认为升序;
copy.sort(new FileComparator());
filterFileList(copy);
int total = copy.size();
int totalPage = (total + PAGE_SIZE - 1) / PAGE_SIZE;
if (page > totalPage) {
return new FilePageModel(totalPage, Collections.emptyList());
}
int start = (page - 1) * PAGE_SIZE;
int end = page * PAGE_SIZE;
end = Math.min(end, total);
return new FilePageModel(totalPage, copy.subList(start, end));
}
/**
* 获取指定路径下的文件信息内容
*
* @param driveId
* 驱动器 ID
*
* @param path
* 文件全路径
*
* @return 该文件的名称, 路径, 大小, 下载地址等信息.
*/
@GetMapping("/directlink/{driveId}")
public ResultBean directlink(@PathVariable(name = "driveId") Integer driveId, String path) {
AbstractBaseFileService fileService = driveContext.get(driveId);
return ResultBean.successData(fileService.getFileItem(path));
fileItemList.removeIf(
fileItem -> ZFileConstant.PASSWORD_FILE_NAME.equals(fileItem.getName())
|| ZFileConstant.README_FILE_NAME.equals(fileItem.getName())
|| filterConfigService.filterResultIsHidden(driveId, StringUtils.concatUrl(fileItem.getPath(), fileItem.getName()))
);
}
}

View File

@@ -5,7 +5,6 @@ import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.service.impl.LocalServiceImpl;
import im.zhaojun.zfile.util.FileUtil;
import im.zhaojun.zfile.util.StringUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.GetMapping;
@@ -15,6 +14,7 @@ import org.springframework.web.servlet.HandlerMapping;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
/**
@@ -32,18 +32,21 @@ public class LocalController {
*
* @param driveId
* 驱动器 ID
*
* @return 文件
* @param type
* 附件预览类型:
* download:下载
* default: 浏览器默认行为
*/
@GetMapping("/file/{driveId}/**")
@ResponseBody
public ResponseEntity<Object> downAttachment(@PathVariable("driveId") Integer driveId, final HttpServletRequest request) {
public void downAttachment(@PathVariable("driveId") Integer driveId, String type, final HttpServletRequest request, final HttpServletResponse response) {
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
AntPathMatcher apm = new AntPathMatcher();
String filePath = apm.extractPathWithinPattern(bestMatchPattern, path);
LocalServiceImpl localService = (LocalServiceImpl) driveContext.get(driveId);
return FileUtil.export(new File(StringUtils.removeDuplicateSeparator(localService.getFilePath() + ZFileConstant.PATH_SEPARATOR + filePath)));
File file = new File(StringUtils.removeDuplicateSeparator(localService.getFilePath() + ZFileConstant.PATH_SEPARATOR + filePath));
FileUtil.export(request, response, file, type);
}
}

View File

@@ -0,0 +1,107 @@
package im.zhaojun.zfile.controller.home;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.URLUtil;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.model.entity.ShortLinkConfig;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.service.ShortLinkConfigService;
import im.zhaojun.zfile.service.SystemConfigService;
import im.zhaojun.zfile.util.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
/**
* 短链 Controller
* @author zhao
*/
@Controller
public class ShortLinkController {
@Resource
private SystemConfigService systemConfigService;
@Resource
private ShortLinkConfigService shortLinkConfigService;
@GetMapping("/api/short-link")
@ResponseBody
public ResultBean shortLink(String driveId, String path) {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
String domain = systemConfig.getDomain();
// 拼接直链地址.
String fullPath = StringUtils.concatUrl(StringUtils.DELIMITER_STR, ZFileConstant.DIRECT_LINK_PREFIX, driveId, path);
ShortLinkConfig shortLinkConfig = shortLinkConfigService.findByUrl(fullPath);
if (shortLinkConfig == null) {
String randomKey;
do {
// 获取短链
randomKey = RandomUtil.randomString(6);
shortLinkConfig = shortLinkConfigService.findByKey(randomKey);
} while (shortLinkConfig != null);
shortLinkConfig = new ShortLinkConfig();
shortLinkConfig.setKey(randomKey);
shortLinkConfig.setUrl(fullPath);
shortLinkConfigService.save(shortLinkConfig);
}
String shortUrl = StringUtils.removeDuplicateSeparator(domain + "/s/" + shortLinkConfig.getKey());
return ResultBean.successData(shortUrl);
}
@GetMapping("/s/{key}")
public String parseShortKey(@PathVariable String key) {
ShortLinkConfig shortLinkConfig = shortLinkConfigService.findByKey(key);
if (shortLinkConfig == null) {
throw new RuntimeException("此直链不存在或已失效.");
}
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
String domain = systemConfig.getDomain();
String url = URLUtil.encode(StringUtils.removeDuplicateSeparator(domain + shortLinkConfig.getUrl()));
return "redirect:" + url;
}
@GetMapping("admin/api/short-link/key")
@ResponseBody
public ResultBean updateShortKey(Integer id, String newKey) {
ShortLinkConfig newShortLinkConfig = shortLinkConfigService.findByKey(newKey);
if (newShortLinkConfig != null) {
throw new RuntimeException("您输入的 Key 已存在,请重新输入");
}
ShortLinkConfig shortLinkConfig = shortLinkConfigService.findById(id);
if (shortLinkConfig == null) {
throw new RuntimeException("此直链不存在或已失效.");
}
shortLinkConfig.setKey(newKey);
shortLinkConfigService.save(shortLinkConfig);
return ResultBean.success();
}
/**
* 批量删除直链
*/
@DeleteMapping("admin/api/short-link")
@ResponseBody
public ResultBean batchDelete(@RequestParam("id[]") Integer[] ids) {
for (Integer id : ids) {
shortLinkConfigService.deleteById(id);
}
return ResultBean.success();
}
}

View File

@@ -0,0 +1,159 @@
package im.zhaojun.zfile.controller.home;
import com.alibaba.fastjson.JSON;
import im.zhaojun.zfile.config.ApplicationContextConfigure;
import im.zhaojun.zfile.context.DriveContext;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.entity.webdav.WebDavEntity;
import im.zhaojun.zfile.model.entity.webdav.WebDavFile;
import im.zhaojun.zfile.model.entity.webdav.WebDavFolder;
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
import im.zhaojun.zfile.util.RegexMatchUtils;
import io.milton.annotations.*;
import io.milton.http.HttpManager;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* WebDav控制器
*
* @author me
* @date 2022/4/9
*/
@Slf4j
@ResourceController
@ConditionalOnProperty(value = "webdav.enable", havingValue = "true")
public class WebDavController {
private static final Logger LOGGER = LoggerFactory.getLogger(WebDavController.class);
/**
* 获取根目录文件夹
*
* @return {@link WebDavFolder} WebDav文件夹
*/
@Root
public WebDavFolder getRootFolder() {
return new WebDavFolder(ZFileConstant.PATH_SEPARATOR, getDriveId());
}
/**
* 获取根目录子文件/文件夹(控制器)
*
* @param rootFolder 根文件夹
* @return {@link WebDavController} 根控制器
*/
@ChildrenOf
public WebDavController getChildren(WebDavController rootFolder) {
return this;
}
/**
* 获取子文件/文件夹
*
* @param parent 父文件夹
* @return {@link List}<{@link WebDavEntity}> WebDav实体
*/
@ChildrenOf
public List<WebDavEntity> getChildren(WebDavFolder parent) {
if (parent == null) {
return Collections.emptyList();
}
try {
// 获取驱动器文件服务
AbstractBaseFileService fileService = ApplicationContextConfigure.getBean(DriveContext.class).get(parent.getDriveId());
if (fileService == null) {
return Collections.emptyList();
}
// 获取文件列表
List<FileItemDTO> fileItemList = fileService.fileList(parent.getFullPath());
// 转换FileItemDTO为WebDavEntity
return WebDavEntity.convertFromFileItemDTO(fileItemList, parent);
} catch (Exception e) {
LOGGER.warn("get webDav children failed,parent:{},msg:{}", JSON.toJSONString(parent), e.getMessage(), e);
return Collections.emptyList();
}
}
/**
* 获取子文件内容
*
* @param webDavFile WebDav文件
* @return {@link String} ViewResolver模板名称
*/
@Get
public String getChild(WebDavFile webDavFile) {
return JSON.toJSONString(webDavFile);
}
/**
* 获取WebDav实体文件名
*/
@Name
public String getWebDavFile(WebDavEntity webDavEntity) {
return webDavEntity.getName();
}
/**
* 获取WebDav实体展示名称
*/
@DisplayName
public String getDisplayName(WebDavEntity webDavEntity) {
return webDavEntity.getName();
}
/**
* 获取WebDav实体唯一id
*/
@UniqueId
public String getUniqueId(WebDavEntity entity) {
return entity.getId().toString();
}
/**
* 获取WebDav实体修改日期
*/
@ModifiedDate
public Date getModifiedDate(WebDavEntity webDavEntity) {
return webDavEntity.getModifiedDate();
}
/**
* 获取WebDav实体创建日期
*/
@CreatedDate
public Date getCreatedDate(WebDavEntity webDavEntity) {
return webDavEntity.getCreatedDate();
}
/**
* 获取WebDav实体大小
*/
@ContentLength
public Long getContentLength(WebDavEntity entity) {
if (entity instanceof WebDavFile) {
return ((WebDavFile) entity).getSize();
}
// 性能考虑,文件夹暂不进行大小统计
return null;
}
/**
* 获取驱动器id
*
* @return {@link Integer}
*/
private Integer getDriveId() {
String requestUrl = HttpManager.decodeUrl(HttpManager.request().getAbsolutePath());
final String driveId = RegexMatchUtils.matchByIndex("^" + ZFileConstant.WEB_DAV_PREFIX + "/(\\d+)(.*)", requestUrl, 1);
return driveId != null ? Integer.valueOf(driveId) : null;
}
}

View File

@@ -30,7 +30,7 @@ public class InstallController {
}
@PostMapping("/install")
@PostMapping("/doInstall")
public ResultBean install(SystemConfigDTO systemConfigDTO) {
if (!StringUtils.isEmpty(systemConfigService.getAdminUsername())) {
return ResultBean.error("请勿重复初始化.");

View File

@@ -14,7 +14,7 @@ import javax.annotation.Resource;
* @author zhaojun
*/
@Controller
@RequestMapping("/onedrive")
@RequestMapping(value = {"/onedrive", "/onedirve"})
public class OneDriveCallbackController {
@Resource
@@ -31,6 +31,13 @@ public class OneDriveCallbackController {
return "callback";
}
@GetMapping("/authorize")
public String authorize() {
return "redirect:https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + oneDriveServiceImpl.getClientId() +
"&response_type=code&redirect_uri=" + oneDriveServiceImpl.getRedirectUri() +
"&scope=" + oneDriveServiceImpl.getScope();
}
@GetMapping("/china-callback")
public String oneDriveChinaCallback(String code, Model model) {
@@ -40,4 +47,11 @@ public class OneDriveCallbackController {
return "callback";
}
@GetMapping("/china-authorize")
public String authorizeChina() {
return "redirect:https://login.chinacloudapi.cn/common/oauth2/v2.0/authorize?client_id=" + oneDriveChinaServiceImpl.getClientId() +
"&response_type=code&redirect_uri=" + oneDriveChinaServiceImpl.getRedirectUri() +
"&scope=" + oneDriveChinaServiceImpl.getScope();
}
}

View File

@@ -0,0 +1,134 @@
package im.zhaojun.zfile.controller.onedrive;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import im.zhaojun.zfile.model.dto.SharePointInfoVO;
import im.zhaojun.zfile.model.support.ResultBean;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Objects;
/**
* @author zhaojun
* SharePoint 工具类
*/
@Controller
@RequestMapping("/sharepoint")
public class SharePointHelperController {
/**
* 根据 AccessToken 获取域名前缀
*/
@PostMapping("/getDomainPrefix")
@ResponseBody
public ResultBean getDomainPrefix(@RequestBody SharePointInfoVO sharePointInfoVO) {
String host = "";
// 判断是标准版还是世纪互联版
if (Objects.equals(sharePointInfoVO.getType(), "Standard")) {
host = "graph.microsoft.com";
} else if (Objects.equals(sharePointInfoVO.getType(), "China")) {
host = "microsoftgraph.chinacloudapi.cn";
}
// 请求 URL
String requestUrl = StrUtil.format("https://{}/v1.0/sites/root", host);
// 构建请求认证 Token 信息
String tokenValue = String.format("%s %s", "Bearer", sharePointInfoVO.getAccessToken());
HashMap<String, String> headers = new HashMap<>();
headers.put("Authorization", tokenValue);
// 请求接口
HttpRequest getRequest = HttpUtil.createGet(requestUrl);
HttpResponse execute = getRequest.addHeaders(headers).execute();
String body = execute.body();
if (execute.getStatus() != HttpStatus.OK.value()) {
return ResultBean.error(body);
}
// 解析前缀
JSONObject jsonObject = JSONObject.parseObject(body);
String hostname = jsonObject.getJSONObject("siteCollection").getString("hostname");
String domainPrefix = StrUtil.subBefore(hostname, ".sharepoint", false);
return ResultBean.successData(domainPrefix);
}
@PostMapping("/getSiteId")
@ResponseBody
public ResultBean getSiteId(@RequestBody SharePointInfoVO sharePointInfoVO) {
// 判断必填参数
if (sharePointInfoVO == null || sharePointInfoVO.getAccessToken() == null || sharePointInfoVO.getSiteName() == null) {
return ResultBean.error("参数不全");
}
String host = "";
// 判断是标准版还是世纪互联版
if (Objects.equals(sharePointInfoVO.getType(), "Standard")) {
host = "graph.microsoft.com";
sharePointInfoVO.setDomainType("com");
} else if (Objects.equals(sharePointInfoVO.getType(), "China")) {
host = "microsoftgraph.chinacloudapi.cn";
sharePointInfoVO.setDomainType("cn");
} else {
return ResultBean.error("参数不全");
}
// 构建请求认证 Token 信息
String tokenValue = String.format("%s %s", "Bearer", sharePointInfoVO.getAccessToken());
HashMap<String, String> authorizationHeaders = new HashMap<>();
authorizationHeaders.put("Authorization", tokenValue);
// 如果没有域名前缀, 则先获取
if (sharePointInfoVO.getDomainPrefix() == null || sharePointInfoVO.getDomainType() == null) {
String requestUrl = StrUtil.format("https://{}/v1.0/sites/root", host);
HttpRequest getRequest = HttpUtil.createGet(requestUrl);
HttpResponse execute = getRequest.addHeaders(authorizationHeaders).execute();
String body = execute.body();
if (execute.getStatus() != HttpStatus.OK.value()) {
return ResultBean.error(body);
}
JSONObject jsonObject = JSONObject.parseObject(body);
String hostname = jsonObject.getJSONObject("siteCollection").getString("hostname");
String domainPrefix = StrUtil.subBefore(hostname, ".sharepoint", false);
sharePointInfoVO.setDomainPrefix(domainPrefix);
}
if (StrUtil.isEmpty(sharePointInfoVO.getSiteType())) {
sharePointInfoVO.setSiteType("/sites/");
}
// 请求接口
String requestUrl = StrUtil.format("https://{}/v1.0/sites/{}.sharepoint.{}:/{}/{}", host,
sharePointInfoVO.getDomainPrefix(),
sharePointInfoVO.getDomainType(),
sharePointInfoVO.getSiteType(),
sharePointInfoVO.getSiteName());
HttpRequest getRequest = HttpUtil.createGet(requestUrl);
HttpResponse execute = getRequest.addHeaders(authorizationHeaders).execute();
String body = execute.body();
// 解析数据
if (execute.getStatus() != HttpStatus.OK.value()) {
return ResultBean.error(body);
}
JSONObject jsonObject = JSONObject.parseObject(body);
return ResultBean.successData(jsonObject.getString("id"));
}
}

View File

@@ -1,16 +1,18 @@
package im.zhaojun.zfile.exception;
import cn.dev33.satoken.exception.NotLoginException;
import im.zhaojun.zfile.model.support.ResultBean;
import org.apache.catalina.connector.ClientAbortException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.net.ConnectException;
/**
* 全局异常处理器
* @author zhaojun
@@ -20,12 +22,22 @@ public class GlobleExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobleExceptionHandler.class);
/**
* 不存在的文件异常
*/
@ExceptionHandler({NotEnabledDriveException.class})
@ResponseBody
public ResultBean notEnabledDrive() {
return ResultBean.error("驱动器已关闭");
}
/**
* 不存在的文件异常
*/
@ExceptionHandler({NotExistFileException.class})
@ResponseBody
public ResultBean notExistFile(Exception ex) {
public ResultBean notExistFile() {
return ResultBean.error("文件不存在");
}
@@ -36,12 +48,33 @@ public class GlobleExceptionHandler {
@ExceptionHandler({HttpMediaTypeNotAcceptableException.class, ClientAbortException.class})
@ResponseBody
@ResponseStatus
public void clientAbortException(Exception ex) {
public void clientAbortException() {
// if (log.isDebugEnabled()) {
// log.debug("出现了断开异常:", ex);
// }
}
/**
* 文件预览异常
*/
@ExceptionHandler({PasswordVerifyException.class})
@ResponseBody
@ResponseStatus
public ResultBean passwordVerifyException(PasswordVerifyException ex) {
return ResultBean.error(ex.getMessage());
}
/**
* 无效的驱动器异常
*/
@ExceptionHandler({InvalidDriveException.class})
@ResponseBody
@ResponseStatus
public ResultBean invalidDriveException(InvalidDriveException ex) {
return ResultBean.error(ex.getMessage());
}
/**
* 文件预览异常
@@ -64,11 +97,32 @@ public class GlobleExceptionHandler {
return ResultBean.error(ex.getMessage());
}
/**
* 登录异常拦截器
*/
@ExceptionHandler(NotLoginException.class)
@ResponseBody
@ResponseStatus
public ResultBean handlerNotLoginException(NotLoginException e) {
return ResultBean.error("未登录");
}
/**
* 登录异常拦截器
*/
@ExceptionHandler(ConnectException.class)
@ResponseBody
public ResultBean handlerConnectException(ConnectException e) {
return ResultBean.error("请求失败, 清稍后再试");
}
@ExceptionHandler
@ResponseBody
@ResponseStatus(code= HttpStatus.INTERNAL_SERVER_ERROR)
public ResultBean searchDisableExceptionHandler(Exception e) {
@ResponseStatus
public ResultBean extraExceptionHandler(Exception e) {
log.error(e.getMessage(), e);
if (e.getClass() == Exception.class) {

View File

@@ -0,0 +1,26 @@
package im.zhaojun.zfile.exception;
/**
* 无效的直链异常
* @author zhaojun
*/
public class InvalidShortLinkException extends RuntimeException {
public InvalidShortLinkException() {
}
public InvalidShortLinkException(String message) {
super(message);
}
public InvalidShortLinkException(String message, Throwable cause) {
super(message, cause);
}
public InvalidShortLinkException(Throwable cause) {
super(cause);
}
public InvalidShortLinkException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -0,0 +1,26 @@
package im.zhaojun.zfile.exception;
/**
* 文件不允许下载异常
* @author zhaojun
*/
public class NotAllowedDownloadException extends RuntimeException {
public NotAllowedDownloadException() {
}
public NotAllowedDownloadException(String message) {
super(message);
}
public NotAllowedDownloadException(String message, Throwable cause) {
super(message, cause);
}
public NotAllowedDownloadException(Throwable cause) {
super(cause);
}
public NotAllowedDownloadException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -0,0 +1,27 @@
package im.zhaojun.zfile.exception;
/**
* 未启用的驱动器异常
*/
public class NotEnabledDriveException extends RuntimeException {
public NotEnabledDriveException() {
}
public NotEnabledDriveException(String message) {
super(message);
}
public NotEnabledDriveException(String message, Throwable cause) {
super(message, cause);
}
public NotEnabledDriveException(Throwable cause) {
super(cause);
}
public NotEnabledDriveException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -1,6 +1,7 @@
package im.zhaojun.zfile.exception;
/**
* 不存在的文件异常
* @author zhaojun
*/
public class NotExistFileException extends RuntimeException {

View File

@@ -0,0 +1,27 @@
package im.zhaojun.zfile.exception;
/**
* 密码校验失败异常
* @author zhaojun
*/
public class PasswordVerifyException extends RuntimeException {
public PasswordVerifyException() {
}
public PasswordVerifyException(String message) {
super(message);
}
public PasswordVerifyException(String message, Throwable cause) {
super(message, cause);
}
public PasswordVerifyException(Throwable cause) {
super(cause);
}
public PasswordVerifyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -0,0 +1,29 @@
package im.zhaojun.zfile.exception;
/**
* 文件解析异常
* @author zhaojun
*/
public class TextParseException extends RuntimeException {
public TextParseException() {
super();
}
public TextParseException(String message) {
super(message);
}
public TextParseException(String message, Throwable cause) {
super(message, cause);
}
public TextParseException(Throwable cause) {
super(cause);
}
protected TextParseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -1,6 +1,8 @@
package im.zhaojun.zfile.filter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.filter.GenericFilterBean;
@@ -8,6 +10,7 @@ import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@@ -16,6 +19,9 @@ import java.io.IOException;
* 开启跨域支持. 一般用于开发环境, 或前后端分离部署时开启.
* @author zhaojun
*/
@Order(1)
@WebFilter(value = "/*")
@Component
public class CorsFilter extends GenericFilterBean {
@Override

View File

@@ -1,27 +0,0 @@
package im.zhaojun.zfile.filter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @author zhaojun
*/
@Configuration
public class MyCorsFilter {
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}

View File

@@ -0,0 +1 @@
package im.zhaojun.zfile.filter;

View File

@@ -0,0 +1 @@
package im.zhaojun.zfile.model.constant;

View File

@@ -23,6 +23,12 @@ public class StorageConfigConstant {
public static final String PASSWORD_KEY = "password";
public static final String WEBDAV_USERNAME = "webdavUsername";
public static final String WEBDAV_PASSWORD = "webdavPassword";
public static final String WEBDAV_URL = "webdavUrl";
public static final String HOST_KEY = "host";
public static final String PORT_KEY = "port";
@@ -33,8 +39,16 @@ public class StorageConfigConstant {
public static final String REFRESH_TOKEN_KEY = "refreshToken";
public static final String SHAREPOINT_SITE_ID = "siteId";
public static final String SHAREPOINT_SITE_NAME = "siteName";
public static final String PATH_STYLE = "pathStyle";
public static final String IS_PRIVATE = "isPrivate";
public static final String PROXY_DOMAIN = "proxyDomain";
public static final String REGION_KEY = "region";
}

View File

@@ -16,6 +16,15 @@ public class ZFileConstant {
public static final String PATH_SEPARATOR = "/";
/**
* 直链前缀名称
*/
public static String DIRECT_LINK_PREFIX = "directlink";
/**
* WebDav前缀
*/
public static String WEB_DAV_PREFIX = "/webdav";
/**
* 系统产生的临时文件路径
@@ -68,5 +77,9 @@ public class ZFileConstant {
ZFileConstant.TEXT_MAX_FILE_SIZE_KB = maxFileSizeKb;
}
@Autowired(required = false)
public void setDirectLinkPrefix(@Value("${zfile.directLinkPrefix}") String directLinkPrefix) {
ZFileConstant.DIRECT_LINK_PREFIX = directLinkPrefix;
}
}

View File

@@ -34,4 +34,6 @@ public class DriveConfigDTO {
private StorageStrategyConfig storageStrategyConfig;
private boolean defaultSwitchToImgMode;
}

View File

@@ -0,0 +1,21 @@
package im.zhaojun.zfile.model.dto;
import im.zhaojun.zfile.model.entity.DriveConfig;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
/**
* @author Zhao Jun
* 2021/5/26 15:17
*/
@Data
@AllArgsConstructor
public class DriveListDTO {
private List<DriveConfig> driveList;
private Boolean isInstall;
}

View File

@@ -0,0 +1,18 @@
package im.zhaojun.zfile.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FileListDTO {
private List<FileItemDTO> files;
private SystemFrontConfigDTO config;
}

View File

@@ -0,0 +1,20 @@
package im.zhaojun.zfile.model.dto;
import lombok.Data;
@Data
public class SharePointInfoVO {
private String type;
private String accessToken;
private String domainPrefix;
private String siteType;
private String siteName;
private String domainType;
}

View File

@@ -40,4 +40,20 @@ public class StorageStrategyConfig {
private String basePath;
private String siteId;
private String siteName;
private String siteType;
private String proxyDomain;
private String region;
private String webdavUsername;
private String webdavPassword;
private String webdavUrl;
}

View File

@@ -46,4 +46,10 @@ public class SystemConfigDTO {
private Boolean showAnnouncement;
private String layout;
private Boolean showLinkBtn;
private Boolean showShortLink;
private Boolean showPathLink;
}

View File

@@ -41,4 +41,16 @@ public class SystemFrontConfigDTO {
private String readme;
private Boolean debugMode;
private Boolean defaultSwitchToImgMode;
private Boolean showLinkBtn;
private Boolean showShortLink;
private Boolean showPathLink;
private String directLinkPrefix;
}

View File

@@ -4,8 +4,6 @@ import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/**
@@ -18,7 +16,6 @@ import javax.persistence.Id;
public class DriveConfig {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private Boolean enable;
@@ -39,4 +36,6 @@ public class DriveConfig {
private Integer orderNum;
private Boolean defaultSwitchToImgMode;
}

View File

@@ -2,12 +2,10 @@ package im.zhaojun.zfile.model.entity;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
/**
* @author zhaojun

View File

@@ -0,0 +1,22 @@
package im.zhaojun.zfile.model.entity;
import lombok.Data;
import javax.persistence.*;
import java.util.Date;
@Entity(name = "SHORT_LINK")
@Data
public class ShortLinkConfig {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String key;
private String url;
private Date createDate;
}

View File

@@ -2,6 +2,7 @@ package im.zhaojun.zfile.model.entity;
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -15,6 +16,7 @@ import javax.persistence.Lob;
*/
@Entity(name = "STORAGE_CONFIG")
@Data
@NoArgsConstructor
public class StorageConfig {
@Id

View File

@@ -0,0 +1,112 @@
package im.zhaojun.zfile.model.entity.webdav;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.enums.FileTypeEnum;
import im.zhaojun.zfile.util.StringUtils;
import lombok.Data;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* WebDav实体
*
* @author me
* @date 2022/4/9
*/
@Data
public class WebDavEntity {
/**
* 唯一ID
*/
private UUID id;
/**
* 驱动器ID
*/
private Integer driveId;
/**
* 名称
*/
private String name;
/**
* 创建时间
*/
private Date createdDate;
/**
* 修改时间
*/
private Date modifiedDate;
/**
* 是否是目录
*/
private boolean isDirectory;
/**
* 父文件夹
*/
private WebDavFolder parent;
public WebDavEntity() {
}
public WebDavEntity(String name, WebDavFolder parent) {
this.id = UUID.randomUUID();
this.name = name;
this.parent = parent;
this.createdDate = new Date();
this.modifiedDate = new Date();
this.isDirectory = true;
}
public WebDavEntity(UUID id, String name, Date createdDate, Date modifiedDate,
WebDavFolder parent) {
this.id = id;
this.name = name;
this.parent = parent;
this.createdDate = createdDate;
this.modifiedDate = modifiedDate;
this.isDirectory = true;
}
/**
* 获取全路径
*/
public String getFullPath() {
if (this.getParent() != null) {
final String parentFullPath = this.getParent().getFullPath();
return StringUtils.removeDuplicateSeparator(parentFullPath + ZFileConstant.PATH_SEPARATOR + this.getName());
} else {
return ZFileConstant.PATH_SEPARATOR;
}
}
public static List<WebDavEntity> convertFromFileItemDTO(List<FileItemDTO> fileItemList, WebDavFolder parent) {
List<WebDavEntity> result = new ArrayList<>();
if (fileItemList == null || fileItemList.size() == 0) {
return result;
}
for (FileItemDTO each : fileItemList) {
WebDavEntity entity = convertFromFileItemDTO(each, parent);
result.add(entity);
}
return result;
}
public static WebDavEntity convertFromFileItemDTO(FileItemDTO fileItemDTO, WebDavFolder parent) {
if (fileItemDTO == null) {
return null;
}
WebDavEntity entity;
if (fileItemDTO.getType() == FileTypeEnum.FOLDER) {
entity = new WebDavFolder(fileItemDTO.getName(), parent);
} else {
entity = new WebDavFile(fileItemDTO.getName(), fileItemDTO.getSize(), parent);
}
entity.setModifiedDate(fileItemDTO.getTime());
entity.setDriveId(parent.getDriveId());
return entity;
}
}

View File

@@ -0,0 +1,37 @@
package im.zhaojun.zfile.model.entity.webdav;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.UUID;
/**
* WebDav文件实体
*
* @author me
* @date 2022/4/9
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class WebDavFile extends WebDavEntity {
/**
* 大小
*/
private Long size;
/**
* 内容类型
*/
private String contentType;
public WebDavFile(String fileName, Long size, WebDavFolder parent) {
super(fileName, parent);
this.setSize(size);
this.setDirectory(false);
}
public WebDavFile(UUID id, String name, Date createdDate, Date modifiedDate, WebDavFolder parent) {
super(id, name, createdDate, modifiedDate, parent);
this.setDirectory(false);
}
}

View File

@@ -0,0 +1,41 @@
package im.zhaojun.zfile.model.entity.webdav;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.UUID;
/**
* WebDav文件夹实体
*
* @author me
* @date 2022/4/9
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class WebDavFolder extends WebDavEntity {
public WebDavFolder(String folderName, WebDavFolder parent) {
super(folderName, parent);
}
public WebDavFolder(String folderName, Integer driveId) {
super(folderName, null);
setDriveId(driveId);
}
public WebDavFolder(UUID id, String name, Date createdDate, Date modifiedDate, WebDavFolder parent) {
super(id, name, createdDate, modifiedDate, parent);
}
public WebDavFile addFile(String fileName, Long size) {
WebDavFile file = new WebDavFile(fileName, size, this);
file.setDirectory(false);
return file;
}
public WebDavFolder addFolder(String folderName) {
return new WebDavFolder(folderName, this);
}
}

View File

@@ -17,6 +17,7 @@ public enum StorageTypeEnum {
LOCAL("local", "本地存储"),
ALIYUN("aliyun", "阿里云 OSS"),
TENCENT("tencent", "腾讯云 COS"),
WebDAV("webdav", "WebDAV"),
UPYUN("upyun", "又拍云 USS"),
FTP("ftp", "FTP"),
UFILE("ufile", "UFile"),
@@ -25,6 +26,8 @@ public enum StorageTypeEnum {
S3("s3", "S3通用协议"),
ONE_DRIVE("onedrive", "OneDrive"),
ONE_DRIVE_CHINA("onedrive-china", "OneDrive 世纪互联"),
SHAREPOINT_DRIVE("sharepoint", "SharePoint"),
SHAREPOINT_DRIVE_CHINA("sharepoint-china", "SharePoint 世纪互联"),
QINIU("qiniu", "七牛云 KODO");
private String key;

View File

@@ -1,39 +0,0 @@
package im.zhaojun.zfile.model.support;
import lombok.Data;
/**
* @author zhaojun
*/
@Data
public class Jvm {
/**
* 当前 JVM 占用的内存总数 (M)
*/
private double total;
/**
* JVM 最大可用内存总数 (M)
*/
private double max;
/**
* JVM 空闲内存 (M)
*/
private double free;
/**
* JDK 版本
*/
private String version;
public Jvm() {
Runtime runtime = Runtime.getRuntime();
this.total = runtime.totalMemory();
this.free = runtime.freeMemory();
this.max = runtime.maxMemory();
this.version = System.getProperty("java.version");
}
}

View File

@@ -1,41 +0,0 @@
package im.zhaojun.zfile.model.support;
import com.sun.management.OperatingSystemMXBean;
import lombok.Data;
import java.lang.management.ManagementFactory;
/**
* @author zhaojun
*/
@Data
public class Mem {
/**
* 内存总量
*/
private double total;
/**
* 已用内存
*/
private double used;
/**
* 剩余内存
*/
private double free;
public Mem() {
OperatingSystemMXBean osb = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
// 总的物理内存+虚拟内存
long totalVirtualMemory = osb.getTotalSwapSpaceSize();
// 剩余的物理内存
long freePhysicalMemorySize = osb.getFreePhysicalMemorySize();
this.total = totalVirtualMemory;
this.free = freePhysicalMemorySize;
this.used = totalVirtualMemory - freePhysicalMemorySize;
}
}

View File

@@ -1,58 +0,0 @@
package im.zhaojun.zfile.model.support;
import cn.hutool.core.date.BetweenFormater;
import cn.hutool.core.date.DateUtil;
import lombok.Data;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.lang.management.ManagementFactory;
/**
* @author zhaojun
*/
@Data
public class Sys {
/**
* 项目路径
*/
private String projectDir;
/**
* 操作系统
*/
private String osName;
/**
* 系统架构
*/
private String osArch;
/**
* 系统版本
*/
private String osVersion;
/**
* 启动时间
*/
private String upTime;
public Sys() {
this.osName = System.getProperty("os.name");
this.osArch = System.getProperty("os.arch");
this.osVersion = System.getProperty("os.version");
Resource resource = new ClassPathResource("");
try {
this.projectDir = resource.getFile().getAbsolutePath();
} catch (IOException e) {
e.printStackTrace();
}
long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
this.upTime = DateUtil.formatBetween(uptime, BetweenFormater.Level.SECOND);
}
}

View File

@@ -1,34 +0,0 @@
package im.zhaojun.zfile.model.support;
import lombok.Data;
import java.io.Serializable;
/**
* @author zhaojun
*/
@Data
public class SystemMonitorInfo implements Serializable {
/**
* 服务器基本信息
*/
private Sys sys;
/**
* JVM 信息
*/
private Jvm jvm;
/**
* 系统内存
*/
private Mem mem;
public SystemMonitorInfo() {
this.jvm = new Jvm();
this.sys = new Sys();
this.mem = new Mem();
}
}

View File

@@ -0,0 +1,47 @@
package im.zhaojun.zfile.model.support;
import lombok.Data;
/**
* 用于表示校验结果的类
*/
@Data
public class VerifyResult {
/**
* 是否成功
*/
private boolean passed;
/**
* 消息
*/
private String msg;
/**
* 代码
*/
private Integer code;
public static VerifyResult success() {
VerifyResult verifyResult = new VerifyResult();
verifyResult.setPassed(true);
return verifyResult;
}
public static VerifyResult fail(String msg) {
VerifyResult verifyResult = new VerifyResult();
verifyResult.setPassed(false);
verifyResult.setMsg(msg);
return verifyResult;
}
public static VerifyResult fail(String msg, Integer code) {
VerifyResult verifyResult = new VerifyResult();
verifyResult.setPassed(false);
verifyResult.setMsg(msg);
verifyResult.setCode(code);
return verifyResult;
}
}

View File

@@ -26,8 +26,40 @@ public interface DriverConfigRepository extends JpaRepository<DriveConfig, Integ
List<DriveConfig> findByType(StorageTypeEnum type);
/**
* 更新驱动器 ID 的排序值
*
* @param orderNum
* 排序值
*
* @param id
* 驱动器 ID
*/
@Modifying
@Query(value="update DRIVER_CONFIG set orderNum = :orderNum where id = :id")
void updateSetOrderNumById(Integer orderNum, Integer id);
/**
* 查询驱动器最大的 ID
*
* @return 驱动器最大 ID
*/
@Query(nativeQuery = true, value = "select max(id) max from DRIVER_CONFIG")
Integer selectMaxId();
/**
* 更新驱动器 ID
*
* @param updateId
* 驱动器原 ID
*
* @param newId
* 驱动器新 ID
*/
@Modifying
@Query(value="update DRIVER_CONFIG set id = :newId where id = :updateId")
void updateId(Integer updateId, Integer newId);
}

View File

@@ -2,6 +2,8 @@ package im.zhaojun.zfile.repository;
import im.zhaojun.zfile.model.entity.FilterConfig;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@@ -14,14 +16,29 @@ public interface FilterConfigRepository extends JpaRepository<FilterConfig, Inte
/**
* 获取驱动器下的所有规则
* @param driveId 驱动器 ID
* @param driveId
* 驱动器 ID
*/
List<FilterConfig> findByDriveId(Integer driveId);
/**
* 根据驱动器 ID 删除其所有的规则
* @param driveId 驱动器 ID
* @param driveId
* 驱动器 ID
*/
void deleteByDriveId(Integer driveId);
/**
* 更新驱动器 ID 对应的参数设置为新的驱动器 ID
*
* @param updateId
* 驱动器原 ID
*
* @param newId
* 驱动器新 ID
*/
@Modifying
@Query(value="update FILTER_CONFIG set driveId = :newId where driveId = :updateId")
void updateDriveId(Integer updateId, Integer newId);
}

View File

@@ -0,0 +1,75 @@
package im.zhaojun.zfile.repository;
import im.zhaojun.zfile.model.entity.ShortLinkConfig;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.Date;
/**
* @author zhaojun
*/
@Repository
public interface ShortLinkConfigRepository extends JpaRepository<ShortLinkConfig, Integer>, JpaSpecificationExecutor<ShortLinkConfig> {
/**
* 获取驱动器下的所有规则
*
* @param key
* 短链 Key
*/
ShortLinkConfig findFirstByKey(String key);
@Query(nativeQuery = true,
value = " select * from SHORT_LINK where " +
" key like concat('%', :key,'%') " +
" and url like concat('%', :url,'%') " +
" and (:dateFrom is null or create_date >= :dateFrom" +
" and (:dateTo is null or create_date <= :dateTo) ",
countQuery = " select count(1) from SHORT_LINK where " +
" key like concat('%', :key,'%') " +
" and url like concat('%', :url,'%') " +
" and (:dateFrom is null or create_date >= :dateFrom" +
" and (:dateTo is null or create_date <= :dateTo) "
)
// @Query(nativeQuery = true,
// value = " select * from SHORT_LINK where " +
// " key like concat('%', :key,'%') " +
// " and url like concat('%', :url,'%') " +
// " and (:dateFrom is null or date_format(create_date, '%Y-%m-%d') >= date_format(:dateFrom, '%Y-%m-%d'))" +
// " and (:dateTo is null or date_format(create_date, '%Y-%m-%d') <= date_format(:dateTo, '%Y-%m-%d')) ) ",
// countQuery = " select count(1) from SHORT_LINK where " +
// " key like concat('%', :key,'%') " +
// " and url like concat('%', :url,'%') " +
// " and (:dateFrom is null or date_format(create_date, '%Y-%m-%d') >= date_format(:dateFrom, '%Y-%m-%d'))" +
// " and (:dateTo is null or date_format(create_date, '%Y-%m-%d') <= date_format(:dateTo, '%Y-%m-%d')) ) "
// )
Page<ShortLinkConfig> findByPage(String key, String url, Date dateFrom, Date dateTo, Pageable pageable);
/**
* 获取驱动器下的所有规则
*
* @param url
* 短链 URL
*/
ShortLinkConfig findFirstByUrl(String url);
/**
* 更新驱动器 ID
*
* @param updateSubPath
* 原路径部分名称
*
* @param newSubPath
* 修改后路径部分名称
*/
@Modifying
@Query(value="update SHORT_LINK set url = replace(url, :updateSubPath, :newSubPath)")
void updateUrlDriveId(String updateSubPath, String newSubPath);
}

View File

@@ -3,6 +3,8 @@ package im.zhaojun.zfile.repository;
import im.zhaojun.zfile.model.entity.StorageConfig;
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@@ -68,4 +70,18 @@ public interface StorageConfigRepository extends JpaRepository<StorageConfig, In
*/
StorageConfig findByDriveIdAndKey(Integer driveId, String key);
/**
* 更新驱动器 ID 对应的参数设置为新的驱动器 ID
*
* @param updateId
* 驱动器原 ID
*
* @param newId
* 驱动器新 ID
*/
@Modifying
@Query(value="update STORAGE_CONFIG set driveId = :newId where driveId = :updateId")
void updateDriveId(Integer updateId, Integer newId);
}

View File

@@ -40,11 +40,11 @@ public class OneDriveTokenRefreshSchedule {
List<DriveConfig> driveConfigList = driveConfigService.findByType(StorageTypeEnum.ONE_DRIVE);
driveConfigList.addAll(driveConfigService.findByType(StorageTypeEnum.ONE_DRIVE_CHINA));
driveConfigList.addAll(driveConfigService.findByType(StorageTypeEnum.SHAREPOINT_DRIVE));
driveConfigList.addAll(driveConfigService.findByType(StorageTypeEnum.SHAREPOINT_DRIVE_CHINA));
driveConfigList.forEach(driveConfig -> {
StorageTypeEnum storageType = driveConfig.getType();
String name = driveConfig.getName();
try {
AbstractOneDriveServiceBase driveService = (AbstractOneDriveServiceBase) driveContext.get(driveConfig.getId());
driveService.refreshOneDriveToken();
@@ -52,7 +52,6 @@ public class OneDriveTokenRefreshSchedule {
} catch (Exception e) {
log.error("刷新 OneDrive Token 失败, DriveInfo: {}", JSON.toJSONString(driveConfig), e);
}
});
} catch (Throwable e) {

View File

@@ -1,20 +0,0 @@
package im.zhaojun.zfile.security;
import cn.hutool.crypto.SecureUtil;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author zhaojun
*/
public class Md5PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return SecureUtil.md5(rawPassword.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return SecureUtil.md5(rawPassword.toString()).equals(encodedPassword);
}
}

View File

@@ -1,134 +0,0 @@
package im.zhaojun.zfile.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import im.zhaojun.zfile.model.support.ResultBean;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* 自定义 Security 配置类
*
* @author zhaojun
*/
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private ObjectMapper objectMapper;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// .authenticationProvider(authenticationProvider())
.exceptionHandling()
// 未登录时,进行 json 格式的提示.
.authenticationEntryPoint((request, response, authException) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
out.write(objectMapper.writeValueAsString(ResultBean.error("未登录")));
out.flush();
out.close();
})
.and()
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/admin/**").authenticated()
.and()
.formLogin() // 使用自带的登录
// 登录失败返回json
.failureHandler((request, response, ex) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
String msg;
if (ex instanceof UsernameNotFoundException || ex instanceof BadCredentialsException) {
msg = "用户名或密码错误";
} else {
msg = "登录失败";
}
out.write(objectMapper.writeValueAsString(ResultBean.error(msg)));
out.flush();
out.close();
})
// 登录成功返回json
.successHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(objectMapper.writeValueAsString(ResultBean.success(authentication)));
out.flush();
out.close();
})
.and()
.exceptionHandling()
// 没有权限返回json
.accessDeniedHandler((request, response, ex) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
out.write(objectMapper.writeValueAsString(ResultBean.error("权限不足")));
out.flush();
out.close();
})
.and()
.logout()
// 退出成功,返回 json
.logoutSuccessHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(objectMapper.writeValueAsString(ResultBean.error("注销成功")));
out.flush();
out.close();
})
.and()
.logout().permitAll();
http.cors();
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
}
@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedPercent(true);
return firewall;
}
@Override
public void configure(AuthenticationManagerBuilder web) throws Exception {
web.userDetailsService(myUserDetailsServiceImpl()).passwordEncoder(passwordEncoder());
}
@Bean
public MyUserDetailsServiceImpl myUserDetailsServiceImpl() {
return new MyUserDetailsServiceImpl();
}
@Override
public void configure(WebSecurity web) {
// 对于在 header 里面增加 token 等类似情况,放行所有 OPTIONS 请求。
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
web.httpFirewall(allowUrlEncodedSlashHttpFirewall());
}
@Bean
public static PasswordEncoder passwordEncoder() {
return new Md5PasswordEncoder();
}
}

View File

@@ -1,34 +0,0 @@
package im.zhaojun.zfile.security;
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.service.SystemConfigService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Objects;
/**
* @author zhaojun
*/
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Resource
private SystemConfigService systemConfigService;
/**
* 授权的时候是对角色授权,认证的时候应该基于资源,而不是角色,因为资源是不变的,而用户的角色是会变的
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
if (!Objects.equals(systemConfig.getUsername(), username)) {
throw new UsernameNotFoundException("用户名不存在");
}
return new User(systemConfig.getUsername(), systemConfig.getPassword(), Collections.emptyList());
}
}

View File

@@ -6,6 +6,7 @@ import im.zhaojun.zfile.context.DriveContext;
import im.zhaojun.zfile.context.StorageTypeContext;
import im.zhaojun.zfile.exception.InitializeDriveException;
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.dto.CacheInfoDTO;
import im.zhaojun.zfile.model.dto.DriveConfigDTO;
import im.zhaojun.zfile.model.dto.StorageStrategyConfig;
@@ -13,8 +14,11 @@ import im.zhaojun.zfile.model.entity.DriveConfig;
import im.zhaojun.zfile.model.entity.StorageConfig;
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import im.zhaojun.zfile.repository.DriverConfigRepository;
import im.zhaojun.zfile.repository.FilterConfigRepository;
import im.zhaojun.zfile.repository.ShortLinkConfigRepository;
import im.zhaojun.zfile.repository.StorageConfigRepository;
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
import im.zhaojun.zfile.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Example;
@@ -42,6 +46,12 @@ public class DriveConfigService {
@Resource
private StorageConfigRepository storageConfigRepository;
@Resource
private FilterConfigRepository filterConfigRepository;
@Resource
private ShortLinkConfigRepository shortLinkConfigRepository;
@Resource
private DriveContext driveContext;
@@ -50,13 +60,14 @@ public class DriveConfigService {
public static final Class<StorageStrategyConfig> STORAGE_STRATEGY_CONFIG_CLASS = StorageStrategyConfig.class;
/**
* 获取所有驱动器列表
*
* @return 驱动器列表
*/
public List<DriveConfig> list() {
Sort sort = new Sort(Sort.Direction.ASC,"orderNum");
Sort sort = Sort.by(Sort.Direction.ASC,"orderNum");
return driverConfigRepository.findAll(sort);
}
@@ -70,10 +81,11 @@ public class DriveConfigService {
DriveConfig driveConfig = new DriveConfig();
driveConfig.setEnable(true);
Example<DriveConfig> example = Example.of(driveConfig);
Sort sort = new Sort(Sort.Direction.ASC,"orderNum");
Sort sort = Sort.by(Sort.Direction.ASC,"orderNum");
return driverConfigRepository.findAll(example, sort);
}
/**
* 获取指定驱动器设置
*
@@ -101,6 +113,10 @@ public class DriveConfigService {
DriveConfigDTO driveConfigDTO = new DriveConfigDTO();
List<StorageConfig> storageConfigList = storageConfigRepository.findByDriveId(driveConfig.getId());
Boolean defaultSwitchToImgMode = driveConfig.getDefaultSwitchToImgMode();
if (defaultSwitchToImgMode == null) {
driveConfig.setDefaultSwitchToImgMode(false);
}
BeanUtils.copyProperties(driveConfig, driveConfigDTO);
StorageStrategyConfig storageStrategyConfig = new StorageStrategyConfig();
@@ -127,6 +143,7 @@ public class DriveConfigService {
return driveConfigDTO;
}
/**
* 获取指定驱动器的存储策略.
*
@@ -141,12 +158,11 @@ public class DriveConfigService {
/**
* 新增或设置驱动器设置
* 新驱动器设置
* @param driveConfig 驱动器设置
* @return 保存后的驱动器设置
*/
public DriveConfig saveOrUpdate(DriveConfig driveConfig) {
return driverConfigRepository.save(driveConfig);
public void updateDriveConfig(DriveConfig driveConfig) {
driverConfigRepository.save(driveConfig);
}
@@ -165,6 +181,11 @@ public class DriveConfigService {
DriveConfig driveConfig = new DriveConfig();
StorageTypeEnum storageType = driveConfigDTO.getType();
BeanUtils.copyProperties(driveConfigDTO, driveConfig);
if (driveConfig.getId() == null) {
Integer nextId = selectNextId();
driveConfig.setId(nextId);
}
driverConfigRepository.save(driveConfig);
// 保存存储策略设置.
@@ -172,12 +193,8 @@ public class DriveConfigService {
AbstractBaseFileService storageTypeService = StorageTypeContext.getStorageTypeService(storageType);
List<StorageConfig> storageConfigList;
if (updateFlag) {
storageConfigList = storageConfigRepository.findByDriveId(driveConfigDTO.getId());
} else {
storageConfigList = storageTypeService.storageStrategyConfigList();
}
List<StorageConfig> storageConfigList = storageTypeService.storageStrategyConfigList();
storageConfigRepository.deleteByDriveId(driveConfigDTO.getId());
for (StorageConfig storageConfig : storageConfigList) {
String key = storageConfig.getKey();
@@ -213,6 +230,46 @@ public class DriveConfigService {
}
/**
* 查询驱动器最大的 ID
*
* @return 驱动器最大 ID
*/
public Integer selectNextId() {
Integer maxId = driverConfigRepository.selectMaxId();
if (maxId == null) {
return 1;
} else {
return maxId + 1;
}
}
/**
* 更新驱动器 ID
*
* @param updateId
* 驱动器原 ID
*
* @param newId
* 驱动器新 ID
*/
@Transactional
public void updateId(Integer updateId, Integer newId) {
zFileCache.clear(updateId);
driverConfigRepository.updateId(updateId, newId);
storageConfigRepository.updateDriveId(updateId, newId);
filterConfigRepository.updateDriveId(updateId, newId);
String updateSubPath = StringUtils.concatUrl(StringUtils.DELIMITER_STR, ZFileConstant.DIRECT_LINK_PREFIX, String.valueOf(updateId));
String newSubPath = StringUtils.concatUrl(StringUtils.DELIMITER_STR, ZFileConstant.DIRECT_LINK_PREFIX, String.valueOf(newId));
shortLinkConfigRepository.updateUrlDriveId(updateSubPath, newSubPath);
driveContext.updateDriveId(updateId, newId);
}
/**
* 删除指定驱动器设置, 会级联删除其参数设置
*
@@ -269,24 +326,6 @@ public class DriveConfigService {
}
/**
* 更新指定驱动器的缓存启用状态
*
* @param driveId
* 驱动器 ID
*
* @param autoRefreshCache
* 是否启用缓存自动刷新
*/
public void updateAutoRefreshCacheStatus(Integer driveId, Boolean autoRefreshCache) {
DriveConfig driveConfig = findById(driveId);
if (driveConfig != null) {
driveConfig.setAutoRefreshCache(autoRefreshCache);
driverConfigRepository.save(driveConfig);
}
}
/**
* 获取指定驱动器的缓存信息
* @param driveId

View File

@@ -1,7 +1,5 @@
package im.zhaojun.zfile.service;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import im.zhaojun.zfile.model.entity.FilterConfig;
import im.zhaojun.zfile.repository.FilterConfigRepository;
@@ -11,13 +9,9 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
/**
* @author zhaojun

View File

@@ -0,0 +1,84 @@
package im.zhaojun.zfile.service;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import im.zhaojun.zfile.model.entity.ShortLinkConfig;
import im.zhaojun.zfile.repository.ShortLinkConfigRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@Service
public class ShortLinkConfigService {
@Resource
private ShortLinkConfigRepository shortLinkConfigRepository;
public ShortLinkConfig findByKey(String key) {
return shortLinkConfigRepository.findFirstByKey(key);
}
public ShortLinkConfig findById(Integer id) {
Optional<ShortLinkConfig> shortLinkConfigOptional = shortLinkConfigRepository.findById(id);
return shortLinkConfigOptional.orElse(null);
}
public ShortLinkConfig findByUrl(String url) {
return shortLinkConfigRepository.findFirstByUrl(url);
}
public void save(ShortLinkConfig shortLinkConfig) {
shortLinkConfig.setCreateDate(new Date());
shortLinkConfigRepository.save(shortLinkConfig);
}
public Page<ShortLinkConfig> find(String key,
String url,
String dateFrom,
String dateTo,
Integer page,
Integer limit,
String orderBy,
String orderDirection) {
Sort sort = Sort.by("desc".equals(orderDirection) ? Sort.Direction.DESC : Sort.Direction.ASC, orderBy);
Pageable pageable = PageRequest.of(page - 1, limit, sort);
Specification<ShortLinkConfig> specification = (root, criteriaQuery, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if (StrUtil.isNotEmpty(dateFrom) && StrUtil.isNotEmpty(dateTo)) {
predicates.add(criteriaBuilder.between(root.get("createDate"),
DateUtil.parseDateTime(dateFrom + " 00:00:00"),
DateUtil.parseDateTime(dateTo + " 23:59:59")));
}
if (StrUtil.isNotEmpty(key)) {
predicates.add(criteriaBuilder.like(root.get("key"), "%" + key + '%'));
}
if (StrUtil.isNotEmpty(url)) {
predicates.add(criteriaBuilder.like(root.get("url"), "%" + url + '%'));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
};
return shortLinkConfigRepository.findAll(specification, pageable);
}
public void deleteById(Integer id) {
shortLinkConfigRepository.deleteById(id);
}
}

View File

@@ -1,6 +1,7 @@
package im.zhaojun.zfile.service;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import im.zhaojun.zfile.cache.ZFileCache;
import im.zhaojun.zfile.exception.InvalidDriveException;
@@ -163,4 +164,24 @@ public class SystemConfigService {
return systemConfigDTO.getUsername();
}
/**
* 获取站点域名
*
* @return 站点域名
*/
public String getDomain() {
SystemConfigDTO systemConfigDTO = getSystemConfig();
return systemConfigDTO.getDomain();
}
/**
* 获取是否已安装初始化
*
* @return 是否已安装初始化
*/
public boolean getIsInstall() {
SystemConfigDTO systemConfigDTO = getSystemConfig();
return StrUtil.isNotEmpty(systemConfigDTO.getUsername());
}
}

View File

@@ -10,6 +10,9 @@ import org.springframework.beans.factory.annotation.Value;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author zhaojun
@@ -121,6 +124,30 @@ public abstract class AbstractBaseFileService implements BaseFileService {
public abstract List<StorageConfig> storageStrategyConfigList();
/**
* 合并数据库查询到的驱动器参数和驱动器本身支持的参数列表, 防止获取新增参数字段时出现空指针异常
*
* @param dbStorageConfigList
* 数据库查询到的存储列表
*/
public void mergeStrategyConfig(Map<String, StorageConfig> dbStorageConfigList) {
// 获取驱动器支持的参数列表
List<StorageConfig> storageConfigs = this.storageStrategyConfigList();
// 比对数据库已存储的参数列表和驱动器支持的参数列表, 找出新增的支持项
Set<String> dbConfigKeySet = dbStorageConfigList.keySet();
Set<String> allKeySet = storageConfigs.stream().map(StorageConfig::getKey).collect(Collectors.toSet());
allKeySet.removeAll(dbConfigKeySet);
// 对于新增的参数, put 到数据库查询的 Map 中, 防止程序获取时出现 NPE.
for (String key : allKeySet) {
StorageConfig storageConfig = new StorageConfig();
storageConfig.setValue("");
dbStorageConfigList.put(key, storageConfig);
}
}
/**
* 搜索文件
*
@@ -144,4 +171,11 @@ public abstract class AbstractBaseFileService implements BaseFileService {
*/
public abstract FileItemDTO getFileItem(String path);
public Integer getDriveId() {
return driveId;
}
public void setDriveId(Integer driveId) {
this.driveId = driveId;
}
}

View File

@@ -1,260 +1,26 @@
package im.zhaojun.zfile.service.base;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.entity.StorageConfig;
import im.zhaojun.zfile.model.enums.FileTypeEnum;
import im.zhaojun.zfile.model.support.OneDriveToken;
import im.zhaojun.zfile.repository.StorageConfigRepository;
import im.zhaojun.zfile.service.StorageConfigService;
import im.zhaojun.zfile.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* @author Zhao Jun
*/
@Slf4j
public abstract class AbstractOneDriveServiceBase extends AbstractBaseFileService {
protected static final String DRIVER_INFO_URL = "https://{graphEndPoint}/v1.0/me/drives";
protected static final String DRIVER_ROOT_URL = "https://{graphEndPoint}/v1.0/me/drive/root/children";
protected static final String DRIVER_ITEMS_URL = "https://{graphEndPoint}/v1.0/me/drive/root:{path}:/children";
protected static final String DRIVER_ITEM_URL = "https://{graphEndPoint}/v1.0/me/drive/root:{path}";
protected static final String AUTHENTICATE_URL = "https://{authenticateEndPoint}/common/oauth2/v2.0/token";
private static final String ONE_DRIVE_FILE_FLAG = "file";
@Resource
@Lazy
private RestTemplate oneDriveRestTemplate;
@Resource
private StorageConfigRepository storageConfigRepository;
@Resource
private StorageConfigService storageConfigService;
/**
* 根据 RefreshToken 刷新 AccessToken, 返回刷新后的 Token.
*
* @return 刷新后的 Token
*/
public OneDriveToken getRefreshToken() {
StorageConfig refreshStorageConfig =
storageConfigRepository.findByDriveIdAndKey(driveId, StorageConfigConstant.REFRESH_TOKEN_KEY);
String param = "client_id=" + getClientId() +
"&redirect_uri=" + getRedirectUri() +
"&client_secret=" + getClientSecret() +
"&refresh_token=" + refreshStorageConfig.getValue() +
"&grant_type=refresh_token";
String fullAuthenticateUrl = AUTHENTICATE_URL.replace("{authenticateEndPoint}", getAuthenticateEndPoint());
HttpRequest post = HttpUtil.createPost(fullAuthenticateUrl);
post.body(param, "application/x-www-form-urlencoded");
HttpResponse response = post.execute();
return JSONObject.parseObject(response.body(), OneDriveToken.class);
}
/**
* OAuth2 协议中, 根据 code 换取 access_token 和 refresh_token.
*
* @param code
* 代码
*
* @return 获取的 Token 信息.
*/
public OneDriveToken getToken(String code) {
String param = "client_id=" + getClientId() +
"&redirect_uri=" + getRedirectUri() +
"&client_secret=" + getClientSecret() +
"&code=" + code +
"&scope=" + getScope() +
"&grant_type=authorization_code";
String fullAuthenticateUrl = AUTHENTICATE_URL.replace("{authenticateEndPoint}", getAuthenticateEndPoint());
HttpRequest post = HttpUtil.createPost(fullAuthenticateUrl);
post.body(param, "application/x-www-form-urlencoded");
HttpResponse response = post.execute();
return JSONObject.parseObject(response.body(), OneDriveToken.class);
}
public abstract class AbstractOneDriveServiceBase extends MicrosoftDriveServiceBase {
@Override
public List<FileItemDTO> fileList(String path) {
path = StringUtils.removeFirstSeparator(path);
String fullPath = StringUtils.getFullPath(basePath, path);
List<FileItemDTO> result = new ArrayList<>();
String nextLink = null;
do {
String requestUrl;
if (nextLink != null) {
nextLink = nextLink.replace("+", "%2B");
requestUrl = URLUtil.decode(nextLink);
}else if (ZFileConstant.PATH_SEPARATOR.equalsIgnoreCase(fullPath) || "".equalsIgnoreCase(fullPath)) {
requestUrl = DRIVER_ROOT_URL;
} else {
requestUrl = DRIVER_ITEMS_URL;
}
fullPath = StringUtils.removeLastSeparator(fullPath);
JSONObject root;
HttpHeaders headers = new HttpHeaders();
headers.set("driveId", driveId.toString());
HttpEntity<Object> entity = new HttpEntity<>(headers);
try {
root = oneDriveRestTemplate.exchange(requestUrl, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), fullPath).getBody();
} catch (HttpClientErrorException e) {
log.debug("调用 OneDrive 时出现了网络异常, 已尝试重新刷新 token 后再试.", e);
refreshOneDriveToken();
root = oneDriveRestTemplate.exchange(requestUrl, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), fullPath).getBody();
}
if (root == null) {
return Collections.emptyList();
}
nextLink = root.getString("@odata.nextLink");
JSONArray fileList = root.getJSONArray("value");
for (int i = 0; i < fileList.size(); i++) {
FileItemDTO fileItemDTO = new FileItemDTO();
JSONObject fileItem = fileList.getJSONObject(i);
fileItemDTO.setName(fileItem.getString("name"));
fileItemDTO.setSize(fileItem.getLong("size"));
fileItemDTO.setTime(fileItem.getDate("lastModifiedDateTime"));
if (fileItem.containsKey("file")) {
fileItemDTO.setUrl(fileItem.getString("@microsoft.graph.downloadUrl"));
fileItemDTO.setType(FileTypeEnum.FILE);
} else {
fileItemDTO.setType(FileTypeEnum.FOLDER);
}
fileItemDTO.setPath(path);
result.add(fileItemDTO);
}
} while (nextLink != null);
return result;
public String getType() {
return "me";
}
@Override
public FileItemDTO getFileItem(String path) {
String fullPath = StringUtils.getFullPath(basePath, path);
HttpHeaders headers = new HttpHeaders();
headers.set("driveId", driveId.toString());
HttpEntity<Object> entity = new HttpEntity<>(headers);
JSONObject fileItem = oneDriveRestTemplate.exchange(DRIVER_ITEM_URL, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), fullPath).getBody();
if (fileItem == null) {
return null;
}
FileItemDTO fileItemDTO = new FileItemDTO();
fileItemDTO.setName(fileItem.getString("name"));
fileItemDTO.setSize(fileItem.getLong("size"));
fileItemDTO.setTime(fileItem.getDate("lastModifiedDateTime"));
if (fileItem.containsKey(ONE_DRIVE_FILE_FLAG)) {
fileItemDTO.setUrl(fileItem.getString("@microsoft.graph.downloadUrl"));
fileItemDTO.setType(FileTypeEnum.FILE);
} else {
fileItemDTO.setType(FileTypeEnum.FOLDER);
}
fileItemDTO.setPath(path);
return fileItemDTO;
}
/**
* 获取 GraphEndPoint, 对于不同版本的 OneDrive, 此地址会不同.
* @return Graph 连接点
*/
public abstract String getGraphEndPoint();
/**
* 获取 AuthenticateEndPoint, 对于不同版本的 OneDrive, 此地址会不同.
* @return Authenticate 连接点
*/
public abstract String getAuthenticateEndPoint();
/**
* 获取 Client ID.
* @return Client Id
*/
public abstract String getClientId();
/**
* 获取重定向地址.
* @return 重定向地址
*/
public abstract String getRedirectUri();
/**
* 获取 Client Secret 密钥.
* @return Client Secret 密钥.
*/
public abstract String getClientSecret();
/**
* 获取 API Scope.
* @return Scope
*/
public abstract String getScope();
public void refreshOneDriveToken() {
OneDriveToken refreshToken = getRefreshToken();
if (refreshToken.getAccessToken() == null || refreshToken.getRefreshToken() == null) {
return;
}
StorageConfig accessTokenConfig =
storageConfigService.findByDriveIdAndKey(driveId, StorageConfigConstant.ACCESS_TOKEN_KEY);
StorageConfig refreshTokenConfig =
storageConfigService.findByDriveIdAndKey(driveId, StorageConfigConstant.REFRESH_TOKEN_KEY);
accessTokenConfig.setValue(refreshToken.getAccessToken());
refreshTokenConfig.setValue(refreshToken.getRefreshToken());
storageConfigService.updateStorageConfig(Arrays.asList(accessTokenConfig, refreshTokenConfig));
public String getDownloadUrl(String path) {
return null;
}
@Override
@@ -263,7 +29,7 @@ public abstract class AbstractOneDriveServiceBase extends AbstractBaseFileServic
add(new StorageConfig("accessToken", "访问令牌"));
add(new StorageConfig("refreshToken", "刷新令牌"));
add(new StorageConfig("basePath", "基路径"));
add(new StorageConfig("proxyDomain", "反代域名"));
}};
}
}

View File

@@ -73,7 +73,11 @@ public abstract class AbstractS3BaseFileService extends AbstractBaseFileService
fileItemDTO.setTime(s.getLastModified());
fileItemDTO.setType(FileTypeEnum.FILE);
fileItemDTO.setPath(path);
fileItemDTO.setUrl(getDownloadUrl(StringUtils.concatUrl(path, fileItemDTO.getName())));
String fullPathAndName = StringUtils.concatUrl(path, fileItemDTO.getName());
String directlink = StringUtils.generatorLink(driveId, fullPathAndName);
fileItemDTO.setUrl(directlink);
fileItemList.add(fileItemDTO);
}
@@ -129,6 +133,7 @@ public abstract class AbstractS3BaseFileService extends AbstractBaseFileService
for (FileItemDTO fileItemDTO : list) {
String fullPath = StringUtils.concatUrl(fileItemDTO.getPath(), fileItemDTO.getName());
if (Objects.equals(fullPath, path)) {
fileItemDTO.setUrl(getDownloadUrl(path));
return fileItemDTO;
}
}

View File

@@ -0,0 +1,34 @@
package im.zhaojun.zfile.service.base;
import im.zhaojun.zfile.model.entity.StorageConfig;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractSharePointServiceBase extends MicrosoftDriveServiceBase {
protected String siteId;
@Override
public String getType() {
return "sites/" + siteId;
}
@Override
public String getDownloadUrl(String path) {
return null;
}
@Override
public List<StorageConfig> storageStrategyConfigList() {
return new ArrayList<StorageConfig>() {{
add(new StorageConfig("accessToken", "访问令牌"));
add(new StorageConfig("refreshToken", "刷新令牌"));
add(new StorageConfig("proxyDomain", "反代域名"));
add(new StorageConfig("basePath", "基路径"));
add(new StorageConfig("siteName", "站点名称"));
add(new StorageConfig("siteId", "SiteId"));
add(new StorageConfig("siteType", "siteType"));
}};
}
}

View File

@@ -0,0 +1,298 @@
package im.zhaojun.zfile.service.base;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.entity.StorageConfig;
import im.zhaojun.zfile.model.enums.FileTypeEnum;
import im.zhaojun.zfile.model.support.OneDriveToken;
import im.zhaojun.zfile.repository.StorageConfigRepository;
import im.zhaojun.zfile.service.StorageConfigService;
import im.zhaojun.zfile.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Slf4j
public abstract class MicrosoftDriveServiceBase extends AbstractBaseFileService {
/**
* 获取根文件 API URI
*/
protected static final String DRIVER_ROOT_URL = "https://{graphEndPoint}/v1.0/{type}/drive/root/children";
/**
* 获取非根文件 API URI
*/
protected static final String DRIVER_ITEMS_URL = "https://{graphEndPoint}/v1.0/{type}/drive/root:{path}:/children";
/**
* 获取单文件 API URI
*/
protected static final String DRIVER_ITEM_URL = "https://{graphEndPoint}/v1.0/{type}/drive/root:{path}";
/**
* 根据 RefreshToken 获取 AccessToken API URI
*/
protected static final String AUTHENTICATE_URL = "https://{authenticateEndPoint}/common/oauth2/v2.0/token";
/**
* OneDrive 文件类型
*/
private static final String ONE_DRIVE_FILE_FLAG = "file";
protected String proxyDomain;
@Resource
@Lazy
private RestTemplate oneDriveRestTemplate;
@Resource
private StorageConfigRepository storageConfigRepository;
@Resource
private StorageConfigService storageConfigService;
/**
* 根据 RefreshToken 刷新 AccessToken, 返回刷新后的 Token.
*
* @return 刷新后的 Token
*/
public OneDriveToken getRefreshToken() {
StorageConfig refreshStorageConfig =
storageConfigRepository.findByDriveIdAndKey(driveId, StorageConfigConstant.REFRESH_TOKEN_KEY);
String param = "client_id=" + getClientId() +
"&redirect_uri=" + getRedirectUri() +
"&client_secret=" + getClientSecret() +
"&refresh_token=" + refreshStorageConfig.getValue() +
"&grant_type=refresh_token";
String fullAuthenticateUrl = AUTHENTICATE_URL.replace("{authenticateEndPoint}", getAuthenticateEndPoint());
HttpRequest post = HttpUtil.createPost(fullAuthenticateUrl);
post.body(param, "application/x-www-form-urlencoded");
HttpResponse response = post.execute();
return JSONObject.parseObject(response.body(), OneDriveToken.class);
}
/**
* OAuth2 协议中, 根据 code 换取 access_token 和 refresh_token.
*
* @param code
* 代码
*
* @return 获取的 Token 信息.
*/
public OneDriveToken getToken(String code) {
String param = "client_id=" + getClientId() +
"&redirect_uri=" + getRedirectUri() +
"&client_secret=" + getClientSecret() +
"&code=" + code +
"&scope=" + getScope() +
"&grant_type=authorization_code";
String fullAuthenticateUrl = AUTHENTICATE_URL.replace("{authenticateEndPoint}", getAuthenticateEndPoint());
HttpRequest post = HttpUtil.createPost(fullAuthenticateUrl);
post.body(param, "application/x-www-form-urlencoded");
HttpResponse response = post.execute();
return JSONObject.parseObject(response.body(), OneDriveToken.class);
}
@Override
public List<FileItemDTO> fileList(String path) {
path = StringUtils.removeFirstSeparator(path);
String fullPath = StringUtils.getFullPath(basePath, path);
List<FileItemDTO> result = new ArrayList<>();
String nextLink = null;
do {
String requestUrl;
if (nextLink != null) {
nextLink = nextLink.replace("+", "%2B");
requestUrl = URLUtil.decode(nextLink);
}else if (ZFileConstant.PATH_SEPARATOR.equalsIgnoreCase(fullPath) || "".equalsIgnoreCase(fullPath)) {
requestUrl = DRIVER_ROOT_URL;
} else {
requestUrl = DRIVER_ITEMS_URL;
}
fullPath = StringUtils.removeLastSeparator(fullPath);
JSONObject root;
HttpHeaders headers = new HttpHeaders();
headers.set("driveId", driveId.toString());
HttpEntity<Object> entity = new HttpEntity<>(headers);
try {
root = oneDriveRestTemplate.exchange(requestUrl, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
} catch (HttpClientErrorException e) {
log.debug("调用 OneDrive 时出现了网络异常, 响应信息: {}, 已尝试重新刷新 token 后再试.", e.getResponseBodyAsString());
refreshOneDriveToken();
root = oneDriveRestTemplate.exchange(requestUrl, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
}
if (root == null) {
return Collections.emptyList();
}
nextLink = root.getString("@odata.nextLink");
JSONArray fileList = root.getJSONArray("value");
for (int i = 0; i < fileList.size(); i++) {
FileItemDTO fileItemDTO = new FileItemDTO();
JSONObject fileItem = fileList.getJSONObject(i);
fileItemDTO.setName(fileItem.getString("name"));
fileItemDTO.setSize(fileItem.getLong("size"));
fileItemDTO.setTime(fileItem.getDate("lastModifiedDateTime"));
if (fileItem.containsKey("file")) {
String originUrl = fileItem.getString("@microsoft.graph.downloadUrl");
if (StringUtils.isNotNullOrEmpty(proxyDomain)) {
originUrl = StringUtils.replaceHost(originUrl, proxyDomain);
}
fileItemDTO.setUrl(originUrl);
fileItemDTO.setType(FileTypeEnum.FILE);
} else {
fileItemDTO.setType(FileTypeEnum.FOLDER);
}
fileItemDTO.setPath(path);
result.add(fileItemDTO);
}
} while (nextLink != null);
return result;
}
@Override
public FileItemDTO getFileItem(String path) {
String fullPath = StringUtils.getFullPath(basePath, path);
HttpHeaders headers = new HttpHeaders();
headers.set("driveId", driveId.toString());
HttpEntity<Object> entity = new HttpEntity<>(headers);
JSONObject fileItem;
try {
fileItem = oneDriveRestTemplate.exchange(DRIVER_ITEM_URL, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
} catch (HttpClientErrorException e) {
log.debug("调用 OneDrive 时出现了网络异常, 响应信息: {}, 已尝试重新刷新 token 后再试.", e.getResponseBodyAsString());
refreshOneDriveToken();
fileItem = oneDriveRestTemplate.exchange(DRIVER_ITEM_URL, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
}
if (fileItem == null) {
return null;
}
FileItemDTO fileItemDTO = new FileItemDTO();
fileItemDTO.setName(fileItem.getString("name"));
fileItemDTO.setSize(fileItem.getLong("size"));
fileItemDTO.setTime(fileItem.getDate("lastModifiedDateTime"));
if (fileItem.containsKey(ONE_DRIVE_FILE_FLAG)) {
String originUrl = fileItem.getString("@microsoft.graph.downloadUrl");
if (StringUtils.isNotNullOrEmpty(proxyDomain)) {
originUrl = StringUtils.replaceHost(originUrl, proxyDomain);
}
fileItemDTO.setUrl(originUrl);
fileItemDTO.setType(FileTypeEnum.FILE);
} else {
fileItemDTO.setType(FileTypeEnum.FOLDER);
}
fileItemDTO.setPath(path);
return fileItemDTO;
}
/**
* 获取存储类型, 对于 OneDrive 或 SharePoint, 此地址会不同.
* @return Graph 连接点
*/
public abstract String getType();
/**
* 获取 GraphEndPoint, 对于不同版本的 OneDrive, 此地址会不同.
* @return Graph 连接点
*/
public abstract String getGraphEndPoint();
/**
* 获取 AuthenticateEndPoint, 对于不同版本的 OneDrive, 此地址会不同.
* @return Authenticate 连接点
*/
public abstract String getAuthenticateEndPoint();
/**
* 获取 Client ID.
* @return Client Id
*/
public abstract String getClientId();
/**
* 获取重定向地址.
* @return 重定向地址
*/
public abstract String getRedirectUri();
/**
* 获取 Client Secret 密钥.
* @return Client Secret 密钥.
*/
public abstract String getClientSecret();
/**
* 获取 API Scope.
* @return Scope
*/
public abstract String getScope();
/**
* 刷新当前驱动器 AccessToken
*/
public void refreshOneDriveToken() {
OneDriveToken refreshToken = getRefreshToken();
if (refreshToken.getAccessToken() == null || refreshToken.getRefreshToken() == null) {
return;
}
StorageConfig accessTokenConfig =
storageConfigService.findByDriveIdAndKey(driveId, StorageConfigConstant.ACCESS_TOKEN_KEY);
StorageConfig refreshTokenConfig =
storageConfigService.findByDriveIdAndKey(driveId, StorageConfigConstant.REFRESH_TOKEN_KEY);
accessTokenConfig.setValue(refreshToken.getAccessToken());
refreshTokenConfig.setValue(refreshToken.getRefreshToken());
storageConfigService.updateStorageConfig(Arrays.asList(accessTokenConfig, refreshTokenConfig));
}
}

View File

@@ -35,6 +35,7 @@ public class AliyunServiceImpl extends AbstractS3BaseFileService implements Base
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String accessKey = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_KEY).getValue();
String secretKey = stringStorageConfigMap.get(StorageConfigConstant.SECRET_KEY).getValue();
String endPoint = stringStorageConfigMap.get(StorageConfigConstant.ENDPOINT_KEY).getValue();

View File

@@ -2,6 +2,7 @@ package im.zhaojun.zfile.service.impl;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.ftp.Ftp;
import cn.hutool.extra.ftp.FtpMode;
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.entity.StorageConfig;
@@ -13,7 +14,6 @@ import im.zhaojun.zfile.service.base.BaseFileService;
import im.zhaojun.zfile.util.StringUtils;
import lombok.SneakyThrows;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
@@ -23,6 +23,7 @@ import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -55,6 +56,7 @@ public class FtpServiceImpl extends AbstractBaseFileService implements BaseFileS
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
host = stringStorageConfigMap.get(StorageConfigConstant.HOST_KEY).getValue();
port = stringStorageConfigMap.get(StorageConfigConstant.PORT_KEY).getValue();
username = stringStorageConfigMap.get(StorageConfigConstant.USERNAME_KEY).getValue();
@@ -65,8 +67,8 @@ public class FtpServiceImpl extends AbstractBaseFileService implements BaseFileS
isInitialized = false;
} else {
ftp = new Ftp(host, Integer.parseInt(port), username, password, StandardCharsets.UTF_8);
ftp.getClient().configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX));
ftp.getClient().type(FTP.BINARY_FILE_TYPE);
ftp.setMode(FtpMode.Passive);
testConnection();
isInitialized = true;
}
@@ -83,13 +85,16 @@ public class FtpServiceImpl extends AbstractBaseFileService implements BaseFileS
ftpFiles = ftp.getClient().listFiles(fullPath);
} catch (Exception e) {
e.printStackTrace();
// ignore
}
List<FileItemDTO> fileItemList = new ArrayList<>();
for (FTPFile ftpFile : ftpFiles) {
FileItemDTO fileItemDTO = new FileItemDTO();
// 跳过 ftp 的本目录和上级目录
if (Arrays.asList(".", "..").contains(ftpFile.getName())) {
continue;
}
fileItemDTO.setName(ftpFile.getName());
fileItemDTO.setSize(ftpFile.getSize());
fileItemDTO.setTime(ftpFile.getTimestamp().getTime());

View File

@@ -35,6 +35,7 @@ public class HuaweiServiceImpl extends AbstractS3BaseFileService implements Base
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String accessKey = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_KEY).getValue();
String secretKey = stringStorageConfigMap.get(StorageConfigConstant.SECRET_KEY).getValue();
String endPoint = stringStorageConfigMap.get(StorageConfigConstant.ENDPOINT_KEY).getValue();

View File

@@ -1,5 +1,6 @@
package im.zhaojun.zfile.service.impl;
import cn.hutool.core.util.StrUtil;
import im.zhaojun.zfile.exception.InitializeDriveException;
import im.zhaojun.zfile.exception.NotExistFileException;
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
@@ -25,6 +26,7 @@ import javax.annotation.Resource;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -52,6 +54,7 @@ public class LocalServiceImpl extends AbstractBaseFileService implements BaseFil
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
filePath = stringStorageConfigMap.get(StorageConfigConstant.FILE_PATH_KEY).getValue();
if (Objects.isNull(filePath)) {
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
@@ -71,6 +74,9 @@ public class LocalServiceImpl extends AbstractBaseFileService implements BaseFil
@Override
public List<FileItemDTO> fileList(String path) throws FileNotFoundException {
if (StrUtil.startWith(path, "..") || StrUtil.startWith(path, "/..")) {
return Collections.emptyList();
}
List<FileItemDTO> fileItemList = new ArrayList<>();
String fullPath = StringUtils.removeDuplicateSeparator(filePath + path);
@@ -123,7 +129,7 @@ public class LocalServiceImpl extends AbstractBaseFileService implements BaseFil
@Override
public FileItemDTO getFileItem(String path) {
String fullPath = StringUtils.concatPath(filePath, path);
String fullPath = filePath + path;
File file = new File(fullPath);

View File

@@ -35,9 +35,12 @@ public class MinIOServiceImpl extends AbstractS3BaseFileService implements BaseF
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String accessKey = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_KEY).getValue();
String secretKey = stringStorageConfigMap.get(StorageConfigConstant.SECRET_KEY).getValue();
String endPoint = stringStorageConfigMap.get(StorageConfigConstant.ENDPOINT_KEY).getValue();
String region = stringStorageConfigMap.get(StorageConfigConstant.REGION_KEY).getValue();
bucketName = stringStorageConfigMap.get(StorageConfigConstant.BUCKET_NAME_KEY).getValue();
basePath = stringStorageConfigMap.get(StorageConfigConstant.BASE_PATH).getValue();
isPrivate = Convert.toBool(stringStorageConfigMap.get(StorageConfigConstant.IS_PRIVATE).getValue(), true);
@@ -50,7 +53,7 @@ public class MinIOServiceImpl extends AbstractS3BaseFileService implements BaseF
s3Client = AmazonS3ClientBuilder.standard()
.withPathStyleAccessEnabled(true)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint, "minio")).build();
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint, region)).build();
testConnection();
isInitialized = true;
@@ -71,6 +74,7 @@ public class MinIOServiceImpl extends AbstractS3BaseFileService implements BaseF
add(new StorageConfig("secretKey", "SecretKey"));
add(new StorageConfig("endPoint", "服务地址"));
add(new StorageConfig("bucketName", "存储空间名称"));
add(new StorageConfig("region", "地域"));
add(new StorageConfig("basePath", "基路径"));
add(new StorageConfig("isPrivate", "是否是私有空间"));
}};

View File

@@ -44,9 +44,15 @@ public class OneDriveChinaServiceImpl extends AbstractOneDriveServiceBase implem
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String accessToken = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_TOKEN_KEY).getValue();
String refreshToken = stringStorageConfigMap.get(StorageConfigConstant.REFRESH_TOKEN_KEY).getValue();
super.basePath = stringStorageConfigMap.get(StorageConfigConstant.BASE_PATH).getValue();
StorageConfig proxyDomainStorageConfig = stringStorageConfigMap.get(StorageConfigConstant.PROXY_DOMAIN);
if (proxyDomainStorageConfig != null) {
super.proxyDomain = proxyDomainStorageConfig.getValue();
}
if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(refreshToken)) {
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
@@ -58,11 +64,6 @@ public class OneDriveChinaServiceImpl extends AbstractOneDriveServiceBase implem
}
}
@Override
public String getDownloadUrl(String path) {
return null;
}
@Override
public StorageTypeEnum getStorageTypeEnum() {
return StorageTypeEnum.ONE_DRIVE_CHINA;

View File

@@ -44,9 +44,14 @@ public class OneDriveServiceImpl extends AbstractOneDriveServiceBase implements
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String accessToken = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_TOKEN_KEY).getValue();
String refreshToken = stringStorageConfigMap.get(StorageConfigConstant.REFRESH_TOKEN_KEY).getValue();
super.basePath = stringStorageConfigMap.get(StorageConfigConstant.BASE_PATH).getValue();
StorageConfig proxyDomainStorageConfig = stringStorageConfigMap.get(StorageConfigConstant.PROXY_DOMAIN);
if (proxyDomainStorageConfig != null) {
super.proxyDomain = proxyDomainStorageConfig.getValue();
}
if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(refreshToken)) {
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
@@ -58,11 +63,6 @@ public class OneDriveServiceImpl extends AbstractOneDriveServiceBase implements
}
}
@Override
public String getDownloadUrl(String path) {
return null;
}
@Override
public StorageTypeEnum getStorageTypeEnum() {
return StorageTypeEnum.ONE_DRIVE;

View File

@@ -35,6 +35,7 @@ public class QiniuServiceImpl extends AbstractS3BaseFileService implements BaseF
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String accessKey = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_KEY).getValue();
String secretKey = stringStorageConfigMap.get(StorageConfigConstant.SECRET_KEY).getValue();
String endPoint = stringStorageConfigMap.get(StorageConfigConstant.ENDPOINT_KEY).getValue();

View File

@@ -1,6 +1,7 @@
package im.zhaojun.zfile.service.impl;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
@@ -35,9 +36,11 @@ public class S3ServiceImpl extends AbstractS3BaseFileService implements BaseFile
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String accessKey = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_KEY).getValue();
String secretKey = stringStorageConfigMap.get(StorageConfigConstant.SECRET_KEY).getValue();
String endPoint = stringStorageConfigMap.get(StorageConfigConstant.ENDPOINT_KEY).getValue();
String region = stringStorageConfigMap.get(StorageConfigConstant.REGION_KEY).getValue();
super.domain = stringStorageConfigMap.get(StorageConfigConstant.DOMAIN_KEY).getValue();
super.basePath = stringStorageConfigMap.get(StorageConfigConstant.BASE_PATH).getValue();
@@ -52,11 +55,14 @@ public class S3ServiceImpl extends AbstractS3BaseFileService implements BaseFile
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
isInitialized = false;
} else {
if (StrUtil.isNotEmpty(endPoint) && StrUtil.isEmpty(region)) {
region = endPoint.split("\\.")[1];
}
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
s3Client = AmazonS3ClientBuilder.standard()
.withPathStyleAccessEnabled(isPathStyle)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint, "")).build();
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint, region)).build();
testConnection();
isInitialized = true;
@@ -75,6 +81,7 @@ public class S3ServiceImpl extends AbstractS3BaseFileService implements BaseFile
add(new StorageConfig("secretKey", "SecretKey"));
add(new StorageConfig("endPoint", "服务地址(EndPoint)"));
add(new StorageConfig("bucketName", "存储空间名称"));
add(new StorageConfig("region", "地域"));
add(new StorageConfig("basePath", "基路径"));
add(new StorageConfig("domain", "加速域名"));
add(new StorageConfig("pathStyle", "域名风格"));

View File

@@ -0,0 +1,102 @@
package im.zhaojun.zfile.service.impl;
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
import im.zhaojun.zfile.model.entity.StorageConfig;
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import im.zhaojun.zfile.service.StorageConfigService;
import im.zhaojun.zfile.service.base.AbstractSharePointServiceBase;
import im.zhaojun.zfile.service.base.BaseFileService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.Map;
/**
* @author zhaojun
*/
@Service
@Slf4j
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class SharePointChinaServiceImpl extends AbstractSharePointServiceBase implements BaseFileService {
@Resource
private StorageConfigService storageConfigService;
@Value("${zfile.onedrive-china.clientId}")
private String clientId;
@Value("${zfile.onedrive-china.redirectUri}")
private String redirectUri;
@Value("${zfile.onedrive-china.clientSecret}")
private String clientSecret;
@Value("${zfile.onedrive-china.scope}")
private String scope;
@Override
public void init(Integer driveId) {
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String accessToken = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_TOKEN_KEY).getValue();
String refreshToken = stringStorageConfigMap.get(StorageConfigConstant.REFRESH_TOKEN_KEY).getValue();
super.siteId = stringStorageConfigMap.get(StorageConfigConstant.SHAREPOINT_SITE_ID).getValue();
super.basePath = stringStorageConfigMap.get(StorageConfigConstant.BASE_PATH).getValue();
StorageConfig proxyDomainStorageConfig = stringStorageConfigMap.get(StorageConfigConstant.PROXY_DOMAIN);
if (proxyDomainStorageConfig != null) {
super.proxyDomain = proxyDomainStorageConfig.getValue();
}
if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(refreshToken)) {
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
isInitialized = false;
} else {
refreshOneDriveToken();
testConnection();
isInitialized = true;
}
}
@Override
public StorageTypeEnum getStorageTypeEnum() {
return StorageTypeEnum.SHAREPOINT_DRIVE_CHINA;
}
@Override
public String getGraphEndPoint() {
return "microsoftgraph.chinacloudapi.cn";
}
@Override
public String getAuthenticateEndPoint() {
return "login.partner.microsoftonline.cn";
}
@Override
public String getClientId() {
return clientId;
}
@Override
public String getRedirectUri() {
return redirectUri;
}
@Override
public String getClientSecret() {
return clientSecret;
}
@Override
public String getScope() {
return scope;
}
}

View File

@@ -0,0 +1,102 @@
package im.zhaojun.zfile.service.impl;
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
import im.zhaojun.zfile.model.entity.StorageConfig;
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import im.zhaojun.zfile.service.StorageConfigService;
import im.zhaojun.zfile.service.base.AbstractSharePointServiceBase;
import im.zhaojun.zfile.service.base.BaseFileService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.Map;
/**
* @author zhaojun
*/
@Service
@Slf4j
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class SharePointServiceImpl extends AbstractSharePointServiceBase implements BaseFileService {
@Resource
private StorageConfigService storageConfigService;
@Value("${zfile.onedrive.clientId}")
protected String clientId;
@Value("${zfile.onedrive.redirectUri}")
protected String redirectUri;
@Value("${zfile.onedrive.clientSecret}")
protected String clientSecret;
@Value("${zfile.onedrive.scope}")
protected String scope;
@Override
public void init(Integer driveId) {
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String accessToken = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_TOKEN_KEY).getValue();
String refreshToken = stringStorageConfigMap.get(StorageConfigConstant.REFRESH_TOKEN_KEY).getValue();
super.siteId = stringStorageConfigMap.get(StorageConfigConstant.SHAREPOINT_SITE_ID).getValue();
super.basePath = stringStorageConfigMap.get(StorageConfigConstant.BASE_PATH).getValue();
StorageConfig proxyDomainStorageConfig = stringStorageConfigMap.get(StorageConfigConstant.PROXY_DOMAIN);
if (proxyDomainStorageConfig != null) {
super.proxyDomain = proxyDomainStorageConfig.getValue();
}
if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(refreshToken)) {
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
isInitialized = false;
} else {
refreshOneDriveToken();
testConnection();
isInitialized = true;
}
}
@Override
public StorageTypeEnum getStorageTypeEnum() {
return StorageTypeEnum.SHAREPOINT_DRIVE;
}
@Override
public String getGraphEndPoint() {
return "graph.microsoft.com";
}
@Override
public String getAuthenticateEndPoint() {
return "login.microsoftonline.com";
}
@Override
public String getClientId() {
return clientId;
}
@Override
public String getRedirectUri() {
return redirectUri;
}
@Override
public String getClientSecret() {
return clientSecret;
}
@Override
public String getScope() {
return scope;
}
}

View File

@@ -35,6 +35,7 @@ public class TencentServiceImpl extends AbstractS3BaseFileService implements Bas
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String secretId = stringStorageConfigMap.get(StorageConfigConstant.SECRET_ID_KEY).getValue();
String secretKey = stringStorageConfigMap.get(StorageConfigConstant.SECRET_KEY).getValue();
String endPoint = stringStorageConfigMap.get(StorageConfigConstant.ENDPOINT_KEY).getValue();

View File

@@ -22,6 +22,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@@ -53,6 +54,7 @@ public class UpYunServiceImpl extends AbstractBaseFileService implements BaseFil
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String bucketName = stringStorageConfigMap.get(StorageConfigConstant.BUCKET_NAME_KEY).getValue();
String username = stringStorageConfigMap.get(StorageConfigConstant.USERNAME_KEY).getValue();
String password = stringStorageConfigMap.get(StorageConfigConstant.PASSWORD_KEY).getValue();
@@ -120,7 +122,7 @@ public class UpYunServiceImpl extends AbstractBaseFileService implements BaseFil
int lastDelimiterIndex = path.lastIndexOf("/");
String name = path.substring(lastDelimiterIndex + 1);
Map<String, String> fileInfo = upYun.getFileInfo(StringUtils.removeDuplicateSeparator(basePath + ZFileConstant.PATH_SEPARATOR + path));
Map<String, String> fileInfo = upYun.getFileInfo(URLUtil.encode(StringUtils.removeDuplicateSeparator(basePath + ZFileConstant.PATH_SEPARATOR + path), StandardCharsets.UTF_8));
if (fileInfo == null) {
throw new NotExistFileException();

View File

@@ -0,0 +1,133 @@
package im.zhaojun.zfile.service.impl;
import cn.hutool.core.util.StrUtil;
import com.github.sardine.DavResource;
import com.github.sardine.Sardine;
import com.github.sardine.SardineFactory;
import im.zhaojun.zfile.exception.NotExistFileException;
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.entity.StorageConfig;
import im.zhaojun.zfile.model.enums.FileTypeEnum;
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import im.zhaojun.zfile.service.StorageConfigService;
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
import im.zhaojun.zfile.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @author zhaojun
*/
@Service
@Slf4j
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class WebdavServiceImpl extends AbstractBaseFileService {
private Sardine sardine;
private String url;
@Resource
private StorageConfigService storageConfigService;
@Override
public void init(Integer driveId) {
this.driveId = driveId;
Map<String, StorageConfig> stringStorageConfigMap =
storageConfigService.selectStorageConfigMapByDriveId(driveId);
this.mergeStrategyConfig(stringStorageConfigMap);
String username = stringStorageConfigMap.get(StorageConfigConstant.WEBDAV_USERNAME).getValue();
String password = stringStorageConfigMap.get(StorageConfigConstant.WEBDAV_PASSWORD).getValue();
url = stringStorageConfigMap.get(StorageConfigConstant.WEBDAV_URL).getValue();
if (Objects.isNull(url)) {
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
isInitialized = false;
} else {
// 如果用户名和密码为空,则使用默认用户名和密码
if (StrUtil.isNotEmpty(username) && StrUtil.isNotEmpty(password)) {
sardine = SardineFactory.begin(username, password);
} else {
sardine = SardineFactory.begin();
}
testConnection();
isInitialized = true;
}
}
@Override
public List<FileItemDTO> fileList(String path) throws Exception {
List<FileItemDTO> resultList = new ArrayList<>();
String requestPath = StringUtils.removeDuplicateSeparator(url + "/" + path);
List<DavResource> resources = sardine.list(requestPath);
Integer index = 0;
for (DavResource res : resources) {
// 如果不是根目录, 则跳过第一个, 因为第一个是当前目录
if (!StrUtil.equals(path, "/") && index++ == 0) {
continue;
}
FileItemDTO fileItemResult = new FileItemDTO();
fileItemResult.setName(res.getName());
fileItemResult.setTime(res.getModified());
fileItemResult.setSize(res.getContentLength());
fileItemResult.setType(res.isDirectory() ? FileTypeEnum.FOLDER : FileTypeEnum.FILE);
fileItemResult.setPath(path);
fileItemResult.setUrl(getDownloadUrl(path + res.getName()));
resultList.add(fileItemResult);
}
return resultList;
}
@Override
public String getDownloadUrl(String path) {
return StringUtils.concatPath(url, path);
}
@Override
public StorageTypeEnum getStorageTypeEnum() {
return StorageTypeEnum.WebDAV;
}
@Override
public List<StorageConfig> storageStrategyConfigList() {
return new ArrayList<StorageConfig>() {{
add(new StorageConfig("webdavUsername", "用户名"));
add(new StorageConfig("webdavPassword", "密码"));
add(new StorageConfig("webdavUrl", "WebDav 链接"));
}};
}
@Override
public FileItemDTO getFileItem(String path) {
List<FileItemDTO> list;
try {
int end = path.lastIndexOf("/");
list = fileList(path.substring(0, end + 1));
} catch (Exception e) {
throw new NotExistFileException();
}
for (FileItemDTO fileItemDTO : list) {
String fullPath = StringUtils.concatUrl(fileItemDTO.getPath(), fileItemDTO.getName());
if (Objects.equals(fullPath, path)) {
return fileItemDTO;
}
}
throw new NotExistFileException();
}
}

View File

@@ -1,16 +0,0 @@
package im.zhaojun.zfile.service.support;
import im.zhaojun.zfile.model.support.SystemMonitorInfo;
import org.springframework.stereotype.Service;
/**
* @author zhaojun
*/
@Service
public class SystemMonitorService {
public SystemMonitorInfo systemMonitorInfo() {
return new SystemMonitorInfo();
}
}

View File

@@ -1,10 +1,16 @@
package im.zhaojun.zfile.util;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.mpatric.mp3agic.ID3v1;
import com.mpatric.mp3agic.ID3v2;
import com.mpatric.mp3agic.InvalidDataException;
@@ -29,27 +35,36 @@ public class AudioUtil {
private static final Logger log = LoggerFactory.getLogger(AudioUtil.class);
public static AudioInfoDTO getAudioInfo(String url) throws Exception {
String query = new URL(URLUtil.decode(url)).getQuery();
AudioInfoDTO audioInfoDTO;
try {
String query = new URL(URLUtil.decode(url)).getQuery();
if (query != null) {
url = url.replace(query, URLUtil.encode(query));
if (query != null) {
url = url.replace(query, URLUtil.encode(query));
}
// 如果音乐文件大小超出 5M, 则不解析此音乐
if (im.zhaojun.zfile.util.HttpUtil.getRemoteFileSize(url)
> (1024 * 1024 * ZFileConstant.AUDIO_MAX_FILE_SIZE_MB)) {
return AudioInfoDTO.buildDefaultAudioInfoDTO();
}
String fullFilePath = StringUtils.removeDuplicateSeparator(ZFileConstant.TMP_FILE_PATH + ZFileConstant.PATH_SEPARATOR + UUID.fastUUID());
File file = new File(fullFilePath);
FileUtil.mkParentDirs(file);
final HttpResponse response = HttpRequest.get(url).setFollowRedirects(true).timeout(-1).executeAsync();
response.writeBody(file);
audioInfoDTO = parseAudioInfo(file);
audioInfoDTO.setSrc(url);
file.deleteOnExit();
return audioInfoDTO;
} catch (Exception e) {
log.error("获取音频文件信息失败.", e);
}
// 如果音乐文件大小超出 5M, 则不解析此音乐
if (im.zhaojun.zfile.util.HttpUtil.getRemoteFileSize(url)
> (1024 * 1024 * ZFileConstant.AUDIO_MAX_FILE_SIZE_MB)) {
return AudioInfoDTO.buildDefaultAudioInfoDTO();
}
String fullFilePath = StringUtils.removeDuplicateSeparator(ZFileConstant.TMP_FILE_PATH + ZFileConstant.PATH_SEPARATOR + UUID.fastUUID());
File file = new File(fullFilePath);
FileUtil.mkParentDirs(file);
HttpUtil.downloadFile(url, file);
AudioInfoDTO audioInfoDTO = parseAudioInfo(file);
audioInfoDTO.setSrc(url);
file.deleteOnExit();
return audioInfoDTO;
return AudioInfoDTO.buildDefaultAudioInfoDTO();
}
private static AudioInfoDTO parseAudioInfo(File file) throws IOException, UnsupportedTagException {
@@ -86,4 +101,4 @@ public class AudioUtil {
return audioInfoDTO;
}
}
}

View File

@@ -1,5 +1,6 @@
package im.zhaojun.zfile.util;
import cn.hutool.core.comparator.CompareUtil;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.enums.FileTypeEnum;
@@ -43,8 +44,8 @@ public class FileComparator implements Comparator<FileItemDTO> {
if (o1Type.equals(o2Type)) {
int result;
switch (sortBy) {
case "time": result = o1.getTime().compareTo(o2.getTime()); break;
case "size": result = o1.getSize().compareTo(o2.getSize()); break;
case "time": result = CompareUtil.compare(o1.getTime(), o2.getTime()); break;
case "size": result = CompareUtil.compare(o1.getSize(), o2.getSize()); break;
default: result = naturalOrderComparator.compare(o1.getName(), o2.getName()); break;
}
return "asc".equals(order) ? result : -result;

View File

@@ -1,21 +1,38 @@
package im.zhaojun.zfile.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import im.zhaojun.zfile.model.constant.LocalFileResponseTypeConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Date;
import java.util.Objects;
/**
* @author zhaojun
*/
@Slf4j
public class FileUtil {
public static ResponseEntity<Object> export(File file, String fileName) {
/**
* 文件下载,单线程,直接传
* @param file 文件对象
* @param fileName 要保存为的文件名
* @return 文件下载对象
*/
public static ResponseEntity<Object> exportSingleThread(File file, String fileName) {
if (!file.exists()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("404 FILE NOT FOUND");
}
@@ -23,7 +40,7 @@ public class FileUtil {
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
HttpHeaders headers = new HttpHeaders();
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
if (StringUtils.isNullOrEmpty(fileName)) {
fileName = file.getName();
@@ -31,10 +48,10 @@ public class FileUtil {
headers.setContentDispositionFormData("attachment", URLUtil.encode(fileName));
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");
headers.add("Last-Modified", new Date().toString());
headers.add("ETag", String.valueOf(System.currentTimeMillis()));
headers.add(HttpHeaders.PRAGMA, "no-cache");
headers.add(HttpHeaders.EXPIRES, "0");
headers.add(HttpHeaders.LAST_MODIFIED, new Date().toString());
headers.add(HttpHeaders.ETAG, String.valueOf(System.currentTimeMillis()));
return ResponseEntity
.ok()
.headers(headers)
@@ -43,26 +60,139 @@ public class FileUtil {
.body(new FileSystemResource(file));
}
public static ResponseEntity<Object> export(File file) {
/**
* 返回文件给 response支持断点续传和多线程下载
* @param request 请求对象
* @param response 响应对象
* @param file 下载的文件
*/
public static void export(HttpServletRequest request, HttpServletResponse response, File file, String type) {
export(request, response, file, file.getName(), type);
}
/**
* 返回文件给 response支持断点续传和多线程下载 (动态变化的文件不支持)
* @param request 请求对象
* @param response 响应对象
* @param file 下载的文件
* @param fileName 下载的文件名,为空则默认读取文件名称
*/
public static void export(HttpServletRequest request, HttpServletResponse response, File file, String fileName, String type) {
if (!file.exists()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("404 FILE NOT FOUND");
try {
response.getWriter().write("404 FILE NOT FOUND");
} catch (IOException e) {
e.printStackTrace();
}
}
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
if (StringUtils.isNullOrEmpty(fileName)) {
//文件名
fileName = file.getName();
}
HttpHeaders headers = new HttpHeaders();
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.setContentDispositionFormData("attachment", URLUtil.encode(file.getName()));
String range = request.getHeader(HttpHeaders.RANGE);
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");
headers.add("Last-Modified", new Date().toString());
headers.add("ETag", String.valueOf(System.currentTimeMillis()));
return ResponseEntity
.ok()
.headers(headers)
.contentLength(file.length())
.contentType(mediaType)
.body(new FileSystemResource(file));
String rangeSeparator = "-";
// 开始下载位置
long startByte = 0;
// 结束下载位置
long endByte = file.length() - 1;
// 如果是断点续传
if (range != null && range.contains("bytes=") && range.contains(rangeSeparator)) {
// 设置响应状态码为 206
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
range = range.substring(range.lastIndexOf("=") + 1).trim();
String[] ranges = range.split(rangeSeparator);
try {
// 判断 range 的类型
if (ranges.length == 1) {
// 类型一bytes=-2343
if (range.startsWith(rangeSeparator)) {
endByte = Long.parseLong(ranges[0]);
}
// 类型二bytes=2343-
else if (range.endsWith(rangeSeparator)) {
startByte = Long.parseLong(ranges[0]);
}
}
// 类型三bytes=22-2343
else if (ranges.length == 2) {
startByte = Long.parseLong(ranges[0]);
endByte = Long.parseLong(ranges[1]);
}
} catch (NumberFormatException e) {
// 传参不规范,则直接返回所有内容
startByte = 0;
endByte = file.length() - 1;
}
} else {
// 没有 ranges 即全部一次性传输,需要用 200 状态码,这一行应该可以省掉,因为默认返回是 200 状态码
response.setStatus(HttpServletResponse.SC_OK);
}
//要下载的长度endByte 为总长度 -1这时候要加回去
long contentLength = endByte - startByte + 1;
//文件类型
String contentType = request.getServletContext().getMimeType(fileName);
if (StrUtil.isEmpty(contentType)) {
contentType = "application/octet-stream";
}
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
response.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
// 这里文件名换你想要的inline 表示浏览器可以直接使用
// 参考资料https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
if (Objects.equals(type, LocalFileResponseTypeConstant.DOWNLOAD) || StrUtil.isEmpty(contentType)) {
String contentDisposition = "attachment;filename=" + URLUtil.encode(fileName);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
}
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
// [要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + startByte + rangeSeparator + endByte + "/" + file.length());
BufferedOutputStream outputStream;
RandomAccessFile randomAccessFile = null;
//已传送数据大小
long transmitted = 0;
try {
randomAccessFile = new RandomAccessFile(file, "r");
outputStream = new BufferedOutputStream(response.getOutputStream());
byte[] buff = new byte[4096];
int len = 0;
randomAccessFile.seek(startByte);
while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) {
outputStream.write(buff, 0, len);
transmitted += len;
// 本地测试, 防止下载速度过快
// Thread.sleep(1);
}
// 处理不足 buff.length 部分
if (transmitted < contentLength) {
len = randomAccessFile.read(buff, 0, (int) (contentLength - transmitted));
outputStream.write(buff, 0, len);
transmitted += len;
}
outputStream.flush();
response.flushBuffer();
randomAccessFile.close();
// log.trace("下载完毕: {}-{}, 已传输 {}", startByte, endByte, transmitted);
} catch (ClientAbortException e) {
// ignore 用户停止下载
// log.trace("用户停止下载: {}-{}, 已传输 {}", startByte, endByte, transmitted);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (randomAccessFile != null) {
randomAccessFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

View File

@@ -1,6 +1,8 @@
package im.zhaojun.zfile.util;
import cn.hutool.core.io.FileUtil;
import im.zhaojun.zfile.exception.PreviewException;
import im.zhaojun.zfile.exception.TextParseException;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.client.RestTemplate;
@@ -16,16 +18,28 @@ import java.net.URLConnection;
public class HttpUtil {
/**
* 最大支持文件预览大小: 1M
* 获取 URL 对应的文件内容
*
* @param url
* 文件 URL
* @return 文件 URL
*/
public static String getTextContent(String url) {
RestTemplate restTemplate = SpringContextHolder.getBean("restTemplate");
if (getRemoteFileSize(url) > (1024 * ZFileConstant.TEXT_MAX_FILE_SIZE_KB)) {
throw new PreviewException("存储源跨域请求失败, 服务器中转状态, 预览文件超出大小, 最大支持 1M");
long maxFileSize = 1024 * ZFileConstant.TEXT_MAX_FILE_SIZE_KB;
if (getRemoteFileSize(url) > maxFileSize) {
throw new PreviewException("预览文件超出大小, 最大支持 " + FileUtil.readableFileSize(maxFileSize));
}
String result;
try {
result = restTemplate.getForObject(url, String.class);
} catch (Exception e) {
throw new TextParseException("文件解析异常, 请求 url = " + url + ", 异常信息为 = " + e.getMessage());
}
String result = restTemplate.getForObject(url, String.class);
return result == null ? "" : result;
}

View File

@@ -0,0 +1,61 @@
package im.zhaojun.zfile.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 正则匹配工具类
*
* @author me
* @date 2022/4/9
*/
public class RegexMatchUtils {
/**
* 正则匹配分组序号值
*
* @param regex 正则表达式
* @param str 待匹配字符串
* @param index 分组序号,从1开始
* @return {@link String} 不存在/匹配失败返回null
*/
public static String matchByIndex(String regex, String str, Integer index) {
Matcher matcher = match(regex, str);
if (matcher == null) {
return null;
}
return getIndexResult(matcher, index);
}
/**
* 匹配字符串
*
* @param regex 正则表达式
* @param str 待匹配字符串
* @return {@link Matcher}
*/
public static Matcher match(String regex, String str) {
if (str == null || "".equals(str)) {
return null;
}
Matcher matcher = Pattern.compile(regex).matcher(str);
if (!matcher.lookingAt()) {
return null;
}
return matcher;
}
/**
* 获取指定分组序号的匹配结果
*
* @param matcher {@link Matcher}
* @param index 分组序号,从1开始
* @return {@link String} 不存在/匹配失败返回null
*/
public static String getIndexResult(Matcher matcher, Integer index) {
if (matcher == null || index == null || index < 0 || index > matcher.groupCount()) {
return null;
}
return matcher.group(index);
}
}

Some files were not shown because too many files have changed in this diff Show More