Compare commits

...

164 Commits
3.2.3 ... 4.1.4

Author SHA1 Message Date
zhaojun
e082043f99 🔖 发布 4.1.4 版本 2023-03-05 17:03:38 +08:00
zhaojun
a55e8ae2ad 提交静态页面 2023-03-05 17:02:43 +08:00
zhaojun
188431b64d Merge remote-tracking branch 'origin/main' 2023-03-05 16:50:04 +08:00
赵俊
7f23dcb7c4 合并拉取请求 #507
fix-bug:本地存储上传文件没有关闭流
2023-03-05 16:49:35 +08:00
zhaojun
8dcb64a60d ✏️ 修改 issue 模板 2023-03-05 15:42:35 +08:00
zhaojun
d9c64ff369 ⬆️ 升级依赖版本 2023-03-05 15:38:51 +08:00
zhaojun
6963b1d593 ✏️ 修改文档,增加谷歌云和多吉云支持 2023-03-05 15:37:12 +08:00
zhaojun
0a61e1047d 捕获 ClientAbortException 异常,不进行异常输出 2023-03-05 15:34:34 +08:00
zhaojun
3f02cd9832 🐛 修复存储源别名修改后再修改回去提示占用的 BUG 2023-03-05 15:34:00 +08:00
zhaojun
3aa42c00fa 🐛 修复七牛对私有空间使用自定义域名后无法正常下载的 bug 2023-03-05 15:33:38 +08:00
zhaojun
77b2253ff6 优化本地存储检测安全性的代码 2023-03-05 15:33:29 +08:00
zhaojun
71a4fdfbaf 🐛 修复 GoogleDrive 快捷方式文件夹无法正常获取内容的 BUG 2023-03-05 15:33:14 +08:00
zhaojun
2ecd69dc51 🐛 修复自动设置 CORS 时,某些 S3 兼容性不同导致的 BUG(BackBlaze 不支持 * 和实际域名写到一起,不支持空值) 2023-03-05 15:32:45 +08:00
zhaojun
ebae9ba5c8 增加动态忽略参数不显示到前端功能 2023-03-05 15:31:59 +08:00
zhaojun
b2fd722443 为多吉云增加存储源参数 2023-03-05 15:31:46 +08:00
zhaojun
e2ce404e87 为了安全性,去除从服务器加载文本文件的功能 2023-03-05 15:31:18 +08:00
zhaojun
6cfbe7689e 更显眼的提示用户使用 Google Drive 时需要自建 API 2023-03-05 15:30:38 +08:00
zhaojun
b95c1d890b 更显眼的提示用户腾讯云使用 CDN 回源鉴权后需要关闭 ZFile 中私有空间开关。 2023-03-05 15:29:48 +08:00
zhaojun
461c77012a 增加动态忽略参数不显示到前端功能 2023-03-05 15:29:36 +08:00
zhaojun
9328e0ea9d 根据查询条件批量删除直链 2023-03-05 15:28:14 +08:00
zhaojun
aefa928a19 🐛 修复短链对应的存储源关闭后,存储源仍然可以访问的 bug 2023-03-05 15:27:31 +08:00
zhaojun
89c6c515f1 🐛 避免未控制并发生成短链导致生成多条的 BUG 2023-03-05 15:26:22 +08:00
zhaojun
dcadffa265 增加是否缓存数据库的开关 2023-03-05 15:24:27 +08:00
zhaojun
300e58e92c 限制单 IP 直链单位时间内下载次数 2023-03-05 15:23:47 +08:00
zhaojun
96b71e4f8d 增加站点首页 Logo 自定义功能,Logo 支持点击跳转第三方链接。
增加默认排序字段功能,增加分页加载更多功能。
2023-03-05 15:16:55 +08:00
zhaojun
456aabb893 新增多吉云支持 2023-03-05 15:12:26 +08:00
wlplove007
b4d2ca238f fix-bug:本地存储上传文件没有关闭流 2023-03-02 20:45:53 +08:00
zhaojun
2afb841fd9 🔖 发布 4.1.3 版本 2022-11-26 20:02:31 +08:00
zhaojun
2b09812153 💄 更新前端页面 2022-11-26 20:02:18 +08:00
zhaojun
972099a598 优化 logback 日志输出格式及存储方式 2022-11-26 18:14:56 +08:00
zhaojun
9335d78d60 兼容 3.x 版本获取令牌功能 2022-11-26 18:11:50 +08:00
zhaojun
f332cb929b 🐛 修复本地存储不支持上传大小为 0 的文件. 2022-11-26 18:11:26 +08:00
zhaojun
a190a2ec6e 🐛 校验本地存储路径合法性,防止恶意获取上级目录。 2022-11-26 18:11:18 +08:00
zhaojun
5bfa7037cb ✏️ 增加 SharePoint 网站提示文本 2022-11-26 18:10:26 +08:00
zhaojun
4a94c879b8 🐛 修复 referer 防盗链允许为空时,仍然去黑/白名单校验的 bug 2022-11-26 18:10:04 +08:00
zhaojun
38161f96e1 优化微软 OneDrive、SharePoint 相关存储的代码, 更好的输出日志和重构代码。 2022-11-26 18:00:16 +08:00
zhaojun
33c751ab33 🐛 修复密码文件夹不正确的情况下,也显示出了文件夹 readme 的 bug 2022-11-26 17:59:52 +08:00
zhaojun
d12cbd2383 🐛 修复本地存储不支持上传大小为 0 的文件. 2022-11-26 17:59:14 +08:00
zhaojun
5f84becf08 优化存储源初始化方式,增加 name 的注入,以便更全面的日志输出。 2022-11-26 17:58:50 +08:00
zhaojun
432fd89c0f 优化存储源初始化方式,增加 name 的注入,以便更全面的日志输出。 2022-11-26 17:57:26 +08:00
zhaojun
84c8adc9d2 优化存储源初始化方式,增加 name 的注入,以便更全面的日志输出。 2022-11-26 17:56:54 +08:00
zhaojun
ba2523ac8a 重构代码,优化方法名 2022-11-26 17:56:12 +08:00
zhaojun
dcdd25c01f 🐛 修复 referer 防盗链允许为空时,仍然去黑/白名单校验的 bug 2022-11-26 17:55:15 +08:00
zhaojun
278e320550 🐛 修复 Referer 防盗链不生效的 bug 2022-11-26 17:54:43 +08:00
zhaojun
1bfa66cc49 更新 onedrive 使用的 restTemplate 客户端,增加日志记录功能,并移除请求头中写 storageId 来获取 AccessToken 的功能。 2022-11-26 17:54:07 +08:00
zhaojun
20fb2b3baa 🐛 修复本地存储删除失败的 bug 2022-09-24 20:11:03 +08:00
zhaojun
e9a85a4e88 🔖 发布 4.1.2 版本 2022-09-20 18:30:40 +08:00
zhaojun
63edbc11d3 🐛 修复在 Linux 下开启后台登陆图片验证码时,出现异常提示的 BUG 2022-09-20 18:30:22 +08:00
zhaojun
c98c6af0f9 🐛 修复事务中读取到旧缓存的 bug 2022-09-20 18:30:03 +08:00
zhaojun
47f6066733 🔨 重构代码,将捐赠版已验证的功能合并 2022-09-20 18:12:01 +08:00
zhaojun
f4fb471da3 💄 更新前端页面 2022-09-20 18:08:19 +08:00
zhaojun
301b6bdf70 🐛 修复没有 origin 情况下,跨域失效的问题。 2022-09-09 14:36:22 +08:00
zhaojun
a98c4f4c48 📝 文档更新,增加 3d 文件预览图片 2022-08-29 15:32:44 +08:00
zhaojun
f4a625dac6 🔖 发布 4.1.1 版本 2022-08-29 14:56:39 +08:00
zhaojun
1c2f3de55c 更新文档 2022-08-29 14:56:20 +08:00
zhaojun
ed375768f5 Merge remote-tracking branch 'origin/main' 2022-08-29 14:55:07 +08:00
zhaojun
b0c4ed1fad 💄 更新前端页面 2022-08-29 14:54:59 +08:00
zhaojun
8d30ca7eee 🐛 修复通过 nginx 反向代理后, 未登录后台时请求, 重定向到反代前地址的 bug 2022-08-29 14:44:12 +08:00
zhaojun
43aba8e20e 修改系统设置 value 值字段类型为 text, 防止过长 2022-08-29 14:39:08 +08:00
zhaojun
a2fa8b3eeb 🐛 修复没有自动创建数据库目录的 bug 2022-08-29 14:38:47 +08:00
赵俊
0ef77ee11b Update README.md
增加在线预览 Office 文档截图
2022-08-27 09:09:03 +08:00
zhaojun
6608c0b456 Merge remote-tracking branch 'origin/main' 2022-08-26 18:19:04 +08:00
zhaojun
3ceb5c8c1b 🔖 发布 4.1.0 版本 2022-08-26 18:18:44 +08:00
zhaojun
7399c89a8e 批量删除文件和批量删除直链功能 2022-08-26 18:18:19 +08:00
zhaojun
477c9dbdd2 优化代码结构 2022-08-26 18:17:58 +08:00
zhaojun
e8117c7d3b 增加 OnlyOffice 集成 2022-08-26 18:16:53 +08:00
zhaojun
b29ff1e646 增加 Google Drive 支持 2022-08-26 18:16:00 +08:00
zhaojun
774a8e184a 优化 OneDrive/SharePoint 获取令牌相关代码 2022-08-26 18:14:25 +08:00
zhaojun
7da1405798 💄 更新前端页面 2022-08-26 18:12:07 +08:00
赵俊
564770ba3c Update README.md 2022-08-16 18:45:11 +08:00
zhaojun
ac07de4e73 Merge remote-tracking branch 'origin/main' 2022-08-15 17:11:18 +08:00
zhaojun
3933eba99a 🔖 发布 4.0.10版本 2022-08-15 17:10:03 +08:00
zhaojun
27e8f7961e 🔖 发布 4.0.9 版本 2022-08-15 17:08:57 +08:00
zhaojun
55843cbef6 💄 更新前端页面 2022-08-15 16:35:17 +08:00
zhaojun
ebbb33409f 优化 OneDrive/SharePoint 获取 token 体验,增加信息显示,并优化页面效果。 2022-08-15 13:29:35 +08:00
zhaojun
b39360791f 🐛 修复因缓存未清理导致兼容 readme.md 功能修改无效的 bug 2022-08-15 13:28:35 +08:00
zhaojun
9833378e25 🐛 修复 SharePoint 世纪互联自定义 api 失败的 bug 2022-08-15 13:28:10 +08:00
zhaojun
31df2d16e2 🔖 发布 4.0.9 版本 2022-08-11 21:09:06 +08:00
zhaojun
f8f07912a1 🔨 规范代码 2022-08-11 21:06:40 +08:00
zhaojun
edf43954c6 兼容捐赠版数据文件功能 2022-08-11 21:06:16 +08:00
zhaojun
5a816b1dfb 增加 readme.md 兼容模式 2022-08-11 21:05:25 +08:00
zhaojun
910406c33a 增加最大同时上传数限制 2022-08-11 21:05:18 +08:00
zhaojun
543f76ad1d 增加 readme.md 兼容模式 2022-08-11 21:04:45 +08:00
zhaojun
1cc2d874a1 完善 OneDrive SharePoint 自定义 api 体验 2022-08-11 21:03:11 +08:00
zhaojun
18de372bf9 🐛 修复批量删除直链或直链下载日志数量过多时, 无法正常删除的问题。 2022-08-11 21:02:07 +08:00
zhaojun
5748644814 🐛 修复批量删除直链或直链下载日志数量过多时, 无法正常删除的问题。 2022-08-11 21:02:02 +08:00
zhaojun
fb8060d316 💄 更新前端页面 2022-08-11 21:01:41 +08:00
zhaojun
a4005defe2 更新 github issue 模板 2022-08-05 16:02:34 +08:00
zhaojun
4c90d5bdda 🔖 发布 4.0.8 版本 2022-08-05 15:30:13 +08:00
zhaojun
fa46850bb4 💄 更新前端页面 2022-08-05 15:29:08 +08:00
zhaojun
a7551fce53 完善代码结构 2022-08-05 15:28:52 +08:00
zhaojun
0591669ec9 增加获取单个文件接口请求参数 2022-08-05 15:28:33 +08:00
zhaojun
6286a9e9aa ✏️ 优化提示信息 2022-08-05 15:28:24 +08:00
zhaojun
c67ee1e89d 增加获取单个文件接口 2022-08-05 15:27:58 +08:00
zhaojun
c75695c63a 🐛 修复因类调用自身方法导致事务启用失败的 bug 2022-08-05 15:27:38 +08:00
zhaojun
8f771c652d ✏️ 优化 minio 提示信息 2022-08-05 15:25:52 +08:00
zhaojun
d6e13d6115 🐛 修复获取 m3u8 直链后系统报错的 bug. 2022-08-05 15:25:09 +08:00
zhaojun
8460d17b07 🔨 移动无用代码 2022-08-05 15:24:46 +08:00
zhaojun
49806221b4 💡 增加代码注释 2022-08-05 15:23:36 +08:00
zhaojun
409af8409d 🔨 重构代码,修改包名 2022-08-05 15:22:36 +08:00
zhaojun
6647efeb03 📝 完善 OneDrive SharePoint 反代域名参数的描述信息 2022-08-02 10:25:22 +08:00
zhaojun
c8bd52e26a 统一 sftp、ftp、webdav 下载文件 contentType 为 application/octet-stream, 避免浏览器自动进行默认预览动作 2022-08-01 15:20:06 +08:00
zhaojun
27db6ed14a 🐛 修复本地存储下载时,错误的 contentType 类型导致无法正常下载的问题 2022-08-01 15:17:29 +08:00
zhaojun
b079d03753 🔖 发布 4.0.7 版本 2022-07-31 15:07:11 +08:00
zhaojun
edf8e114ad 💄 更新前端页面 2022-07-31 15:06:55 +08:00
zhaojun
67a84edd4d 优化 S3 协议自动配置跨域逻辑,改为不覆盖原有配置。且增加 GET 跨域,对于在线预览文本、视频场景提供跨域支持. 2022-07-30 20:01:41 +08:00
zhaojun
d798750ee6 优化 S3 协议自动配置跨域逻辑,改为不覆盖原有配置,修改描述. 2022-07-30 20:00:18 +08:00
zhaojun
fea1da86fc 🐛 修复 minio 设置跨域上传报错的 bug 2022-07-30 18:45:19 +08:00
zhaojun
96a0c90600 ⬆️ 更新 pom 依赖,去除无用依赖,升级有漏洞的依赖,划分相关的依赖 2022-07-29 16:50:08 +08:00
赵俊
a054e82740 Merge pull request #404 from wswm152/main
解除对过期内容的引用
2022-07-29 16:19:08 +08:00
无始无名
dc0e84e1e3 解除对过期内容的引用
部分内容在jdk8以上版本中已被去除
2022-07-29 15:05:34 +08:00
zhaojun
ae31005959 🔖 发布 4.0.6 版本 2022-07-28 21:27:18 +08:00
zhaojun
0f167a304d 独立直链和短链功能 2022-07-28 21:24:38 +08:00
zhaojun
43d8143c74 💄 更新前端页面 2022-07-28 21:23:42 +08:00
zhaojun
b2fb1af99b 📝 修复本地存储文件路径描述,对于 docker 环境更容易理解。 2022-07-27 20:49:39 +08:00
zhaojun
a4f4d654a3 ✏️ 修改、完善判断隐藏文件夹方法的代码注释 2022-07-16 17:42:14 +08:00
zhaojun
04d9c24b43 📝 修改文档 docker-compose 错误部分 2022-07-15 22:52:13 +08:00
zhaojun
91a7092190 🔖 发布 4.0.5 版本 2022-07-15 20:50:35 +08:00
zhaojun
f82155f157 💄 更新前端页面 2022-07-15 20:45:29 +08:00
zhaojun
a99218dfa5 🐛 修复 4.0.4 版本更新导致的新增存储源错误的 bug, 其他版本不影响 2022-07-15 15:08:23 +08:00
zhaojun
3e9fe27890 🐛 修复本地存储,文件重命名,前后名称一致时,会无限创建子目录的 bug 2022-07-15 15:07:53 +08:00
zhaojun
7524f58928 💄 更新前端页面 2022-07-15 08:47:25 +08:00
zhaojun
6499f2d220 💄 更新前端页面 2022-07-15 08:47:11 +08:00
zhaojun
40b1f1bc2d 🔖 发布 4.0.4 版本 2022-07-14 20:28:58 +08:00
zhaojun
3bd859d705 ✏️ 删除无用代码 2022-07-14 15:03:59 +08:00
zhaojun
6156d85ba3 🐛 修复启动匿名操作失败的 BUG 2022-07-14 15:03:03 +08:00
zhaojun
e1df2f886b 🐛 修复 url 路径中带 . 访问 404 的 bug 2022-07-13 20:18:48 +08:00
zhaojun
cea42a57bb 🔖 发布 4.0.3 版本 2022-07-13 20:14:24 +08:00
zhaojun
c123d2a671 💄 更新前端页面 2022-07-13 20:13:12 +08:00
zhaojun
02e00b765b 💄 更新前端页面 2022-07-13 20:12:05 +08:00
zhaojun
1992c1f52d 📝 增加异常情况的提示信息 2022-07-13 20:11:50 +08:00
zhaojun
4eb9a721fc 🐛 修复本地存储、FTP、SFTP、WebDav 未启用私有空间,仍然校验私有签名的 bug 2022-07-13 20:10:08 +08:00
zhaojun
f1bac40af4 ✏️ 删除无用代码 2022-07-13 11:19:42 +08:00
zhaojun
0a7fb41e81 🐛 修复 readme 文档仅在首页显示的 bug. 2022-07-13 11:08:30 +08:00
zhaojun
4fd0b8d442 🐛 修复 s3 存储类型不开启私有空间时,文件路径或文件名包含中文无法直链下载的 bug 2022-07-13 11:08:05 +08:00
zhaojun
28150b0c1a 🐛 修复直链下载日志表中 ip 字段长度不足矣存储 ipv6 的 问题 2022-07-13 11:06:29 +08:00
zhaojun
a7813ff7bc 🔖 发布 4.0.2 版本 2022-07-12 19:52:56 +08:00
zhaojun
ae500ef9dc ⬇️ 移除无用依赖,减小项目体积 2022-07-12 19:52:40 +08:00
zhaojun
29d5b84df2 📝 更新文档, 更换图片预览的示例图 2022-07-12 19:49:11 +08:00
zhaojun
28882b198c 💄 更新前端页面 2022-07-12 19:48:42 +08:00
zhaojun
d624c886fe war 打包方式 ipv6 支持 2022-07-12 19:36:45 +08:00
zhaojun
e08654a59e Merge remote-tracking branch 'origin/main' 2022-07-12 19:34:53 +08:00
zhaojun
8fa6a5c9b8 💄 更新前端页面 2022-07-12 19:34:20 +08:00
zhaojun
7620a578fb 服务器代理上传支持特殊字符的文件名,如 # 2022-07-12 19:33:49 +08:00
赵俊
9c9bf93f00 Update README.md 2022-07-11 19:17:54 +08:00
赵俊
912eb694cb Update README.md 2022-07-11 19:11:08 +08:00
zhaojun
78d5ba90cf 💄 更新前端页面 2022-07-11 17:33:53 +08:00
zhaojun
056f9d4449 💄 更新前端页面 2022-07-11 17:33:37 +08:00
zhaojun
9a6020dcc8 增加文件点击习惯设置, 支持设置为单击还是双击 2022-07-11 17:29:15 +08:00
zhaojun
6b3025f379 fix(直链): 统一下载日志 storage_key 字段长度, 防止存储源长度过长后无法正常记录下载日志. 2022-07-11 14:57:30 +08:00
zhaojun
9dffaaee25 fix(异常处理): 存储源不存在时进行提示,修复后提示存储源不存在 2022-07-11 14:56:27 +08:00
zhaojun
bda654a012 fix: 修复管理员修改密码无效的 bug 2022-07-11 14:54:26 +08:00
zhaojun
52271b5117 fix: 修复目录文档某些情况无法匹配成功的 bug 2022-07-11 14:53:44 +08:00
zhaojun
223a6c1970 去除无用的存储类型枚举 2022-07-11 14:53:04 +08:00
zhaojun
60c272b714 修复 SharePoint 无法直链下载的 bug 2022-07-11 14:52:45 +08:00
zhaojun
3b6ed1c78d fix: 修改数据库和日志等文件默认路径为新版路径 2022-07-10 21:56:43 +08:00
zhaojun
5d888bb68f 🔖 发布 4.0.0 版本 2022-07-10 21:44:14 +08:00
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
891 changed files with 25067 additions and 9213 deletions

View File

@@ -6,43 +6,20 @@ labels: 'bug'
assignees: ''
---
<!--
你好!感谢你正在考虑为 ZFile 提交一个 bug。请花一点点时间尽量详细地回答以下基础问题。
为了帮助我们更好的解决您的问题,请填写以下选项(不填写完整可能会被直接关闭 issue
谢谢!
-->
<!--
请确认你已经做了下面这些事情,若 bug 还是未解决,那么请尽可详细地描述你的问题。
- 我已经安装了最新版的 ZFile
- 我已经阅读了 ZFile 的文档https://docs.zfile.vip
- 我已经搜索了已有的 Issues 列表中有关的信息
- 我已经清理过浏览器缓存并重试
-->
## 我的环境
<!--
请登录到管理后台,点击左侧系统监控, 复制或截图此页内容.
-->
## 错误日志
<!--
请登录到管理后台,点击左侧系统监控, 点击右上角诊断日志下载, 然后上传到此 Issue 中.
-->
## 期望行为
<!--
你期望会发生什么?
-->
## 当前行为
<!--
描述 bug 细节,确认出现此问题的复现步骤,例如点击了哪里,发生了什么情况?
你可以粘贴截图或附件。
-->
- 是否已搜索其他 issue没有人提过这个问题
- 是否已查阅、搜索 [ZFile 文档](https://docs.zfile.vip),尤其是常见问题页,仍然未解决?:
- 当前 ZFile 版本:
- 是否尝试最新版是否已解决此问题:
- 是否尝试重启 ZFile且问题依旧存在
- 是否已尝试清空浏览器缓存,且问题依旧存在?:
- 操作系统(如 Windows、Mac、iOS、安卓
- 浏览器(如 Chrome、Firefox、SafariX 浏览器):
- 做什么操作提示的错误?:
- 期望行为(应该是什么样的结果):
- 当前行为(当前是什么样的结果):
- 错误日志(可选):
- 复现步骤(可选):
- 您的额外信息(可选):

View File

@@ -7,28 +7,11 @@ assignees: ''
---
<!--
你好!感谢你愿意考虑希望 ZFile 增加某个新功能。请花一点点时间尽量详细地回答以下基础问题。
谢谢!
-->
为了帮助我们更好的解决您的问题,请填写以下选项(不填写完整可能会被直接关闭 issue
## 概述
<!--
对这个新功能的一段描述
-->
## 动机
<!--
为什么你希望在 ZFile 中使用这个功能?
-->
## 详细解释
<!--
详细描述这个新功能。
如果这是一个小功能,你可以忽略这部分。
-->
- 是否已搜索其他 issue没有人提过这个功能
- 是否已尝试使用最新版本,且仍然没有此功能?:
- 功能概述:
- 功能动机:
- 详细解释(可选):

View File

@@ -1,14 +0,0 @@
---
name: Question
about: 对 ZFile 有任何问题吗?
title: ''
labels: 'question'
assignees: ''
---
<!--
如果你有任何问题也可以通过此渠道来向我们反馈。
谢谢!
-->

15
.gitignore vendored
View File

@@ -1,11 +1,8 @@
HELP.md
target/
.mvn/wrapper/**
!**/src/main/**
**/src/test/**
mvnw
mvnw.cmd
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
@@ -29,6 +26,12 @@ mvnw.cmd
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
/.mvn/wrapper/
/mvnw
/mvnw.cmd
/.script/

181
API.md
View File

@@ -1,181 +0,0 @@
## API 标准
所有 API 均返回 `msg`, `code`, `data` 三个属性.
| code | 描述 |
| :---: | :------------: |
| 0 | 请求成功 |
| -1 | 请求失败 |
| -2 | 文件夹需要密码 |
`code == 0` 时, `data` 中为请求所需数据.
`code != 0` 时, 应当将 `msg` 中的内容作为参考值.
## 驱动器列表
### 请求 URL
`/api/drive/list` `GET`
### 响应
```json
{
"msg": "操作成功",
"code": 0,
"data": [
{
"id": 3, --- ID 是驱动器 ID, 用来唯一区分驱动器
"name": "演示 A 盘", --- 驱动器名称
"enableCache": true, --- 是否开启了缓存
"autoRefreshCache": false, --- 是否开启了缓存自动刷新
"type": { --- 存储源类型
"key": "upyun",
"description": "又拍云 USS"
},
"searchEnable": false, --- 是否开启搜索
"searchIgnoreCase": false, --- 搜索是否忽略大小写
"searchContainEncryptedFile": false --- 搜索是否包含加密文件夹
}
]
}
```
## 获取文件列表
### 请求 URL
`/api/list/{driveId}` `GET`
### URL 参数
| 参数名 | 描述 | 是否必填 | 参考值 |
| :-----: | :-------------------: | :------: | :------------------------------------: |
| driveId | 驱动器 ID | 是 | 参考 `获取驱动器列表` 接口返回的 id 值 |
### 请求参数
| 参数名 | 描述 | 是否必填 | 参考值 |
| :------: | :--------: | :------: | :--------------------------: |
| path | 路径 | 是 | `/`, `/文件夹名称` |
| password | 文件夹密码 | 否 | 当文件夹需要密码时, |
| page | 页数 | 否 | 默认取第一页, 每页固定 30 条 |
### 响应
```json
{
"msg": "操作成功",
"code": 0,
"data": [
{
"name": "密码文件夹",
"time": "2020-01-28 13:17",
"size": 4096,
"type": "FOLDER",
"path": "/",
"url": null
},
{
"name": "新建 文本文档.txt",
"time": "2020-01-28 13:16",
"size": 3,
"type": "FILE",
"path": "/",
"url": "http://127.0.0.1:8080/file/新建 文本文档.txt"
}
]
}
```
## 获取单个文件信息
### 请求 URL
`/api/directlink/{driveId}` `GET`
### URL 参数
| 参数名 | 描述 | 是否必填 | 参考值 |
| :-----: | :-------------------: | :------: | :------------------------------------: |
| driveId | 驱动器 ID | 是 | 参考 `获取驱动器列表` 接口返回的 id 值 |
### 参数
| 参数名 | 描述 | 是否必填 | 参考值 |
| :----: | :--------: | :------: | :------------------: |
| path | 文件全路径 | 是 | `/新建 文本文档.txt` |
### 响应
```json
{
"msg": "操作成功",
"code": 0,
"data": {
"name": "新建 文本文档.txt",
"time": "2020-01-28 13:16",
"size": 3,
"type": "FILE",
"path": "d:/test",
"url": "http://127.0.0.1:8080/file/新建 文本文档.txt"
}
}
```
## 获取系统配置
### 请求 URL
`/api/config/{driveId}` `GET`
### URL 参数
| 参数名 | 描述 | 是否必填 | 参考值 |
| :-----: | :-------------------: | :------: | :------------------------------------: |
| driveId | 驱动器 ID | 是 | 参考 `获取驱动器列表` 接口返回的 id 值 |
### 参数
| 参数名 | 描述 | 是否必填 | 参考值 |
| :----: | :--------: | :------: | :-----------: |
| path | 文件夹名称 | 是 | `/文件夹名称` |
### 响应
```json
{
"msg": "操作成功",
"code": 0,
"data": {
"siteName": "ZFile 演示站",
"searchEnable": false,
"username": "zhao",
"domain": "https://zfile.jun6.net",
"customJs": "",
"customCss": "",
"tableSize": "small",
"showOperator": true,
"showDocument": true,
"announcement": "本站是 ZFile 演示站,交流反馈群 180605017",
"showAnnouncement": true,
"layout": "full",
"readme": null
}
}
```

172
README.md
View File

@@ -1,126 +1,66 @@
<p align = "center">
<img alt="ZFile" src="https://cdn.jun6.net/2021/04/21/69a89344e2a84.png" height="150px">
<p align="center">
![zfile](https://cdn.jun6.net/uPic/2022/09/04/zfile-header.png)
基于 Java 的在线网盘程序,支持对接 S3、OneDrive、SharePoint、Google Drive、多吉云、又拍云、本地存储、FTP、SFTP 等存储源支持在线浏览图片、播放音视频文本文件、Office、obj3d等文件类型。
<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">
<img src="https://img.shields.io/badge/license-MIT-blue.svg?longCache=true&style=flat-square" alt="license">
<img src="https://api.codacy.com/project/badge/Grade/70b793267f7941d58cbd93f50c9a8e0a" alt="codady">
<img src="https://img.shields.io/github/last-commit/zhaojun1998/zfile.svg?style=flat-square" alt="last commit">
<img src="https://img.shields.io/github/downloads/zhaojun1998/zfile/total?style=flat-square" alt="downloads">
<img src="https://img.shields.io/github/v/release/zhaojun1998/zfile?style=flat-square" alt="release">
<img src="https://img.shields.io/github/commit-activity/y/zhaojun1998/zfile?style=flat-square" alt="commit activity">
<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">
<img src="https://img.shields.io/github/issues/zhaojun1998/zfile?style=flat-square" alt="issues">
<img src="https://img.shields.io/github/issues-closed-raw/zhaojun1998/zfile?style=flat-square" alt="closed issues">
<img src="https://img.shields.io/github/forks/zhaojun1998/zfile?style=flat-square" alt="forks">
<img src="https://img.shields.io/github/stars/zhaojun1998/zfile?style=flat-square" alt="stars">
<img src="https://img.shields.io/github/watchers/zhaojun1998/zfile?style=flat-square" alt="watchers">
</p>
## 相关地址
预览地址: [https://zfile.vip](https://zfile.vip)
文档地址: [https://docs.zfile.vip](https://docs.zfile.vip)
社区地址: [https://bbs.zfile.vip](https://bbs.zfile.vip)
项目源码: [https://github.com/zhaojun1998/zfile](https://github.com/zhaojun1998/zfile)
前端源码: [https://github.com/zhaojun1998/zfile-vue](https://github.com/zhaojun1998/zfile-vue)
## 系统特色
* 文件夹密码
* 目录 README 说明
* 文件直链(短链,永久直链,二维码)
* 支持在线浏览文本文件, 视频, 图片, 音乐. (支持 FLV 和 HLS)
* 图片模式
* Docker 支持
* 隐藏指定文件夹(通配符支持)
* 自定义 JS, CSS
* 自定义目录 README 说明文件和密码文件名称
* 同时挂载多个存储策略
* 缓存动态开启, ~~缓存自动刷新 (v2.2 及以前版本支持)~~
* ~~全局搜索 (v2.2 及以前版本支持)~~
* 支持 S3 协议, 阿里云 OSS, FTP, 华为云 OBS, 本地存储, MINIO, OneDrive 国际/家庭/个人版/世纪互联版/SharePoint, , 七牛云 KODO, 腾讯云 COS, 又拍云 USS.
## 快速开始
安装依赖环境:
请参考部署文档: [https://docs.zfile.vip](https://docs.zfile.vip)
```bash
# CentOS系统
yum install -y java-1.8.0-openjdk unzip
```
## 在线体验
```bash
# Debian 9 / Ubuntu 14+
apt update
apt install -y openjdk-8-jre-headless unzip
```
[https://demo.zfile.vip](https://demo.zfile.vip)
```bash
# Debian 10 (Buster) 系统
apt update && apt install -y apt-transport-https software-properties-common ca-certificates dirmngr gnupg
wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add -
add-apt-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/
apt update && apt install -y adoptopenjdk-8-hotspot-jre
```
## 功能预览
### 文件列表
![文件列表](https://cdn.jun6.net/uPic/2022/08/13/0urMn8.png)
### 画廊模式
![图片预览](https://cdn.jun6.net/uPic/2022/08/13/d2J9aE.png)
### 视频预览
![视频预览](https://cdn.jun6.net/uPic/2022/08/13/tBX00R.png)
### 文本预览
![文本预览](https://cdn.jun6.net/uPic/2022/08/13/7dDy4G.png)
### 音频预览
![音频预览](https://cdn.jun6.net/uPic/2022/08/13/N5bU1R.png)
### PDF 预览
![PDF 预览](https://cdn.jun6.net/uPic/2022/08/13/H327bV.png)
### Office 预览
![Office 预览](https://cdn.jun6.net/uPic/2022/08/27/RxeiqI.png)
### 3d 文件预览
![3d 文件预览](https://cdn.jun6.net/uPic/2022/08/29/8iszyh.png)
### 生成直链
![生成直链](https://cdn.jun6.net/uPic/2022/08/13/zCX3xT.jpg)
### 页面设置
![页面设置](https://cdn.jun6.net/uPic/2022/08/13/54nYv2.png)
### 后台设置-登录
![后台设置-登录](https://cdn.jun6.net/uPic/2022/08/13/J8P2Zf.png)
### 后台设置-存储源列表
![后台设置-存储源列表](https://cdn.jun6.net/uPic/2022/08/13/jymieO.png)
### 后台设置-存储源权限控制
![后台设置-存储源权限控制](https://cdn.jun6.net/uPic/2022/08/13/JgiwkH.jpg)
### 后台设置-添加存储源(本地存储)
![后台设置-添加存储源(本地存储)](https://cdn.jun6.net/uPic/2022/08/13/add-storage.png)
### 后台设置-添加存储源(世纪互联)
![后台设置-添加存储源(世纪互联)](https://cdn.jun6.net/uPic/2022/08/13/add-storage2.png)
### 后台设置-显示设置
![后台设置-显示设置](https://cdn.jun6.net/uPic/2022/08/13/view-setting.png)
> 如为更新程序, 则请先执行 `~/zfile/bin/stop.sh && rm -rf ~/zfile` 清理旧程序. 首次安装请忽略此选项.
下载项目:
```bash
export ZFILE_INSTALL_PATH=~/zfile
mkdir -p $ZFILE_INSTALL_PATH && cd $ZFILE_INSTALL_PATH
wget https://c.jun6.net/ZFILE/zfile-release.war
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`。
程序的目录结构为:
```
├── zfile
├── META-INF
├── WEB-INF
└── bin
├── start.sh # 启动脚本
└── stop.sh # 停止脚本
├── restart.sh # 重启脚本
```
启动项目:
```bash
~/zfile/bin/start.sh
```
篇幅有限, 更详细的安装教程及介绍请参考: [ZFile 文档](https://docs.zfile.vip)
访问地址:
用户前台: http://127.0.0.1:8080/main
初始安装: http://127.0.0.1:8080/install
管理后台: http://127.0.0.1:8080/admin
## 预览
![前台首页](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)
## 支持作者
@@ -128,10 +68,10 @@ chmod +x $ZFILE_INSTALL_PATH/bin/*.sh
<img src="https://cdn.jun6.net/2021/03/27/152704e91f13d.png" width="400" alt="赞助我">
## Stargazers over time
## Status
[![starcharts stargazers over time](https://starchart.cc/zhaojun1998/zfile.svg)](https://starchart.cc/zhaojun1998/zfile.svg)
![Alt](https://repobeats.axiom.co/api/embed/580333f83b91087e713f15497e6433c50e1da090.svg "Repobeats analytics image")
## 开发工具赞助
## Star History
<a href="https://www.jetbrains.com/?from=zfile"><img src="https://cdn.jun6.net/2021/04/21/26e410d60b0b0.png?1=1" width="100px"></a>
[![Star History Chart](https://api.star-history.com/svg?repos=zfile-dev/zfile&type=Date)](https://star-history.com/#zfile-dev/zfile&Date)

227
pom.xml
View File

@@ -1,29 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
<relativePath/>
<version>2.6.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>im.zhaojun</groupId>
<artifactId>zfile</artifactId>
<version>3.2.3</version>
<version>4.1.4</version>
<name>zfile</name>
<packaging>war</packaging>
<description>一个在线的文件浏览系统</description>
<properties>
<java.version>1.8</java.version>
<log4j2.version>2.17.1</log4j2.version>
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
</properties>
<dependencies>
<!-- spring boot 官方相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -33,10 +31,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
@@ -46,43 +40,94 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 数据库驱动-->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<version>1.4.197</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
<scope>runtime</scope>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>7.15.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.3</version>
</dependency>
<!-- 存储策略相关 API, 对象存储、FTP、 Rest API-->
<dependency>
<groupId>com.upyun</groupId>
<artifactId>java-sdk</artifactId>
<version>4.1.3</version>
<version>4.2.3</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.11.699</version>
<version>1.12.406</version>
</dependency>
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.12.0</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<dependency>
<groupId>com.github.lookfirst</groupId>
<artifactId>sardine</artifactId>
<version>5.10</version>
</dependency>
<!-- 登陆/权限相关 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.33.0</version>
</dependency>
<!-- 文档相关 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
@@ -91,53 +136,61 @@
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
<version>3.9.0</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>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<!-- 其他工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mpatric</groupId>
<artifactId>mp3agic</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.69</version>
<version>1.2.83_noneautotype</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>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>commons-chain</groupId>
<artifactId>commons-chain</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
<version>1.82</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20200518</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
</dependencies>
<build>
@@ -145,6 +198,44 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.uyoqu.framework</groupId>
@@ -162,10 +253,14 @@
<jvms>
<jvm>-Djava.security.egd=file:/dev/./urandom</jvm>
<jvm>-Dfile.encoding=utf-8</jvm>
<jvm>-Djava.net.preferIPv4Stack=false</jvm>
<jvm>-Djava.net.preferIPv4Addresses=true</jvm>
<jvm>-Djava.awt.headless=true</jvm>
</jvms>
</configuration>
</plugin>
</plugins>
</build>
</build>
</project>

View File

@@ -0,0 +1,173 @@
/*
* Copyright (c) 2011-2022, baomidou (jobob@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baomidou.mybatisplus.core.handlers;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.invoker.Invoker;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定义枚举属性转换器
*
* @author hubin
* @since 2017-10-11
*/
public class MybatisEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
private static final Map<String, String> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
private final Class<E> enumClassType;
private final Class<?> propertyType;
private final Invoker getInvoker;
public MybatisEnumTypeHandler(Class<E> enumClassType) {
if (enumClassType == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.enumClassType = enumClassType;
MetaClass metaClass = MetaClass.forClass(enumClassType, REFLECTOR_FACTORY);
String name = "value";
if (!IEnum.class.isAssignableFrom(enumClassType)) {
name = findEnumValueFieldName(this.enumClassType).orElseThrow(() -> new IllegalArgumentException(String.format("Could not find @EnumValue in Class: %s.", this.enumClassType.getName())));
}
this.propertyType = ReflectionKit.resolvePrimitiveIfNecessary(metaClass.getGetterType(name));
this.getInvoker = metaClass.getGetInvoker(name);
}
/**
* 查找标记标记EnumValue字段
*
* @param clazz class
* @return EnumValue字段
* @since 3.3.1
*/
public static Optional<String> findEnumValueFieldName(Class<?> clazz) {
if (clazz != null && clazz.isEnum()) {
String className = clazz.getName();
return Optional.ofNullable(CollectionUtils.computeIfAbsent(TABLE_METHOD_OF_ENUM_TYPES, className, key -> {
Optional<Field> fieldOptional = findEnumValueAnnotationField(clazz);
return fieldOptional.map(Field::getName).orElse(null);
}));
}
return Optional.empty();
}
private static Optional<Field> findEnumValueAnnotationField(Class<?> clazz) {
return Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EnumValue.class)).findFirst();
}
/**
* 判断是否为MP枚举处理
*
* @param clazz class
* @return 是否为MP枚举处理
* @since 3.3.1
*/
public static boolean isMpEnums(Class<?> clazz) {
return clazz != null && clazz.isEnum() && (IEnum.class.isAssignableFrom(clazz) || findEnumValueFieldName(clazz).isPresent());
}
@SuppressWarnings("Duplicates")
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType)
throws SQLException {
if (jdbcType == null) {
ps.setObject(i, this.getValue(parameter));
} else {
// see r3589
ps.setObject(i, this.getValue(parameter), jdbcType.TYPE_CODE);
}
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
Object value = rs.getObject(columnName);
if (null == value && rs.wasNull()) {
return null;
}
return this.valueOf(value);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Object value = rs.getObject(columnIndex, this.propertyType);
if (null == value && rs.wasNull()) {
return null;
}
return this.valueOf(value);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Object value = cs.getObject(columnIndex, this.propertyType);
if (null == value && cs.wasNull()) {
return null;
}
return this.valueOf(value);
}
private E valueOf(Object value) {
E[] es = this.enumClassType.getEnumConstants();
return Arrays.stream(es).filter((e) -> equalsValue(value, getValue(e))).findAny().orElse(null);
}
/**
* 值比较
*
* @param sourceValue 数据库字段值
* @param targetValue 当前枚举属性值
* @return 是否匹配
* @since 3.3.0
*/
protected boolean equalsValue(Object sourceValue, Object targetValue) {
String sValue = StringUtils.toStringTrim(sourceValue);
String tValue = StringUtils.toStringTrim(targetValue);
if (sourceValue instanceof Number && targetValue instanceof Number
&& new BigDecimal(sValue).compareTo(new BigDecimal(tValue)) == 0) {
return true;
}
return Objects.equals(sValue, tValue);
}
private Object getValue(Object object) {
try {
return this.getInvoker.invoke(object, new Object[0]);
} catch (ReflectiveOperationException e) {
throw ExceptionUtils.mpe(e);
}
}
}

View File

@@ -2,21 +2,22 @@ package im.zhaojun.zfile;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* @author zhaojun
*/
@EnableAsync
@SpringBootApplication
@EnableJpaRepositories("im.zhaojun.zfile.repository")
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
@ServletComponentScan(basePackages = {"im.zhaojun.zfile.core.filter", "im.zhaojun.zfile.module.storage.filter"})
@ComponentScan(basePackages = "im.zhaojun.zfile.*")
public class ZfileApplication {
public static void main(String[] args) {
SpringApplication.run(ZfileApplication.class, args);
}
}
}

View File

@@ -1,66 +0,0 @@
package im.zhaojun.zfile.aspect;
import im.zhaojun.zfile.cache.ZFileCache;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.entity.DriveConfig;
import im.zhaojun.zfile.service.DriveConfigService;
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
/**
* @author zhaojun
* 缓存切面类, 用于访问文件夹时, 缓存文件列表内容.
*/
@Aspect
@Component
public class FileListCacheAspect {
@Resource
private ZFileCache zFileCache;
@Resource
private DriveConfigService driveConfigService;
/**
* 缓存切面, 如果此驱动器开启了缓存, 则从缓存中取数据, 没有开启, 则直接调用方法.
*/
@Around(value = "execution(public * im.zhaojun.zfile.service.base.AbstractBaseFileService.fileList(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
List<FileItemDTO> result;
// 获取请求路径
Object[] args = point.getArgs();
String path = String.valueOf(args[0]);
// 获取当前驱动器
AbstractBaseFileService fileService = ((AbstractBaseFileService) point.getTarget());
Integer driveId = fileService.driveId;
// 判断驱动器是否开启了缓存
DriveConfig driveConfig = driveConfigService.findById(driveId);
boolean enableCache = driveConfig.getEnableCache();
if (enableCache) {
List<FileItemDTO> cacheFileList = zFileCache.get(driveId, path);
if (cacheFileList == null) {
result = Collections.unmodifiableList((List<FileItemDTO>) point.proceed());
zFileCache.put(driveId, path, result);
} else {
result = cacheFileList;
}
} else {
result = (List<FileItemDTO>) point.proceed();
}
return result;
}
}

View File

@@ -1,19 +0,0 @@
package im.zhaojun.zfile.cache;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author zhaojun
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DriveCacheKey {
private Integer driveId;
private String key;
}

View File

@@ -1,55 +0,0 @@
package im.zhaojun.zfile.cache;
import cn.hutool.cache.impl.CacheObj;
import cn.hutool.cache.impl.TimedCache;
import im.zhaojun.zfile.context.DriveContext;
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
import im.zhaojun.zfile.util.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* @author zhaojun
*/
@Slf4j
public class MyTimedCache<K, V> extends TimedCache<K, V> {
private DriveContext driveContext;
public MyTimedCache(long timeout) {
super(timeout);
}
public MyTimedCache(long timeout, Map<K, CacheObj<K, V>> map) {
super(timeout, map);
}
@Override
protected void onRemove(K key, V cachedObject) {
if (driveContext == null) {
driveContext = SpringContextHolder.getBean(DriveContext.class);
}
DriveCacheKey cacheKey = (DriveCacheKey) key;
AbstractBaseFileService baseFileService = driveContext.get(cacheKey.getDriveId());
if (log.isDebugEnabled()) {
log.debug("尝试刷新缓存: {}", cacheKey);
}
if (baseFileService == null) {
log.error("尝试刷新缓存: {}, 时出现异常, 驱动器已不存在", cacheKey);
return;
}
try {
baseFileService.fileList(cacheKey.getKey());
} catch (Exception e) {
log.error("尝试刷新缓存 {} 失败", cacheKey);
e.printStackTrace();
}
}
}

View File

@@ -1,331 +0,0 @@
package im.zhaojun.zfile.cache;
import cn.hutool.cache.impl.CacheObj;
import cn.hutool.core.util.StrUtil;
import im.zhaojun.zfile.model.constant.ZFileConstant;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.model.entity.DriveConfig;
import im.zhaojun.zfile.repository.DriverConfigRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* ZFile 缓存类
*
* @author zhaojun
*/
@Component
@Slf4j
public class ZFileCache {
@Resource
private DriverConfigRepository driverConfigRepository;
/**
* 缓存过期时间
*/
@Value("${zfile.cache.timeout}")
private long timeout;
/**
* 缓存自动刷新间隔
*/
@Value("${zfile.cache.auto-refresh.interval}")
private long autoRefreshInterval;
/**
* 文件/文件对象缓存.
*
* ConcurrentMap<Integer, ConcurrentHashMap<String, List<FileItemDTO>>>
* ConcurrentMap<driveId, ConcurrentHashMap<key, value>>
*
* driveId: 驱动器 ID
* key: 文件夹路径
* value: 文件夹中内容
*/
private ConcurrentMap<Integer, MyTimedCache<DriveCacheKey, List<FileItemDTO>>> drivesCache = new ConcurrentHashMap<>();
/**
* 系统设置缓存
*/
private SystemConfigDTO systemConfigCache;
/**
* 写入缓存
*
* @param driveId
* 驱动器 ID
*
* @param key
* 文件夹路径
*
* @param value
* 文件夹中列表
*/
public synchronized void put(Integer driveId, String key, List<FileItemDTO> value) {
getCacheByDriveId(driveId).put(new DriveCacheKey(driveId, key), value);
}
/**
* 获取指定驱动器, 某个文件夹的名称
*
* @param driveId
* 驱动器 ID
*
* @param key
* 文件夹路径
*
* @return 驱动器中文件夹的内容
*/
public List<FileItemDTO> get(Integer driveId, String key) {
return getCacheByDriveId(driveId).get(new DriveCacheKey(driveId, key), false);
}
/**
* 清空指定驱动器的缓存.
*
* @param driveId
* 驱动器 ID
*/
public void clear(Integer driveId) {
if (log.isDebugEnabled()) {
log.debug("清空驱动器所有缓存, driveId: {}", driveId);
}
getCacheByDriveId(driveId).clear();
}
/**
* 获取指定驱动器中已缓存文件夹数量
*
* @param driveId
* 驱动器 ID
*
* @return 已缓存文件夹数量
*/
public int cacheCount(Integer driveId) {
return getCacheByDriveId(driveId).size();
}
/**
* 指定驱动器, 根据文件及文件名查找相关的文件
*
* @param driveId
* 驱动器 ID
*
* @param key
* 搜索键, 可匹配文件夹名称和文件名称.
*
* @return 搜索结果, 包含文件夹和文件.
*/
public List<FileItemDTO> find(Integer driveId, String key) {
List<FileItemDTO> result = new ArrayList<>();
DriveConfig driveConfig = driverConfigRepository.getOne(driveId);
boolean searchContainEncryptedFile = driveConfig.getSearchContainEncryptedFile();
boolean ignoreCase = driveConfig.getSearchIgnoreCase();
for (List<FileItemDTO> fileItemList : getCacheByDriveId(driveId)) {
// 过滤加密文件
if (!searchContainEncryptedFile && isEncryptedFolder(fileItemList)) {
continue;
}
for (FileItemDTO fileItemDTO : fileItemList) {
boolean testResult;
// 根据是否需要忽略大小写来匹配文件(夹)名
if (ignoreCase) {
testResult = StrUtil.containsIgnoreCase(fileItemDTO.getName(), key);
} else {
testResult = fileItemDTO.getName().contains(key);
}
if (testResult) {
result.add(fileItemDTO);
}
}
}
return result;
}
/**
* 获取所有缓存 key (文件夹名称)
*
* @return 所有缓存 key
*/
public Set<String> keySet(Integer driveId) {
Iterator<CacheObj<DriveCacheKey, List<FileItemDTO>>> cacheObjIterator = getCacheByDriveId(driveId).cacheObjIterator();
Set<String> keys = new HashSet<>();
while (cacheObjIterator.hasNext()) {
keys.add(cacheObjIterator.next().getKey().getKey());
}
return keys;
}
/**
* 从缓存中删除指定驱动器的某个路径的缓存
*
* @param driveId
* 驱动器 ID
*
* @param key
* 文件夹路径
*/
public void remove(Integer driveId, String key) {
getCacheByDriveId(driveId).remove(new DriveCacheKey(driveId, key));
}
/**
* 更新缓存中的系统设置
*
* @param systemConfigCache
* 系统设置
*/
public void updateConfig(SystemConfigDTO systemConfigCache) {
this.systemConfigCache = systemConfigCache;
}
/**
* 从缓存中获取系统设置
*
* @return 系统设置
*/
public SystemConfigDTO getConfig() {
return this.systemConfigCache;
}
/**
* 清空系统设置缓存
*/
public void removeConfig() {
this.systemConfigCache = null;
}
/**
* 判断此文件夹是否为加密文件夹 (包含)
*
* @param list
* 文件夹中的内容
*
* @return 返回此文件夹是否是加密的 ().
*/
private boolean isEncryptedFolder(List<FileItemDTO> list) {
// 遍历文件判断是否包含
for (FileItemDTO fileItemDTO : list) {
if (Objects.equals(ZFileConstant.PASSWORD_FILE_NAME, fileItemDTO.getName())) {
return true;
}
}
return false;
}
/**
* 获取指定驱动器对应的缓存
*
* @param driveId
* 驱动器 ID
*
* @return 驱动器对应的缓存
*/
private synchronized MyTimedCache<DriveCacheKey, List<FileItemDTO>> getCacheByDriveId(Integer driveId) {
MyTimedCache<DriveCacheKey, List<FileItemDTO>> driveCache = drivesCache.get(driveId);
if (driveCache == null) {
driveCache = new MyTimedCache<>(timeout * 1000);
drivesCache.put(driveId, driveCache);
startAutoCacheRefresh(driveId);
}
return driveCache;
}
/**
* 获取指定驱动器的缓存命中数
*
* @param driveId
* 驱动器 ID
*
* @return 缓存命中数
*/
public int getHitCount(Integer driveId) {
return getCacheByDriveId(driveId).getHitCount();
}
/**
* 获取指定驱动器的缓存未命中数
*
* @param driveId
* 驱动器 ID
*
* @return 缓存未命中数
*/
public int getMissCount(Integer driveId) {
return getCacheByDriveId(driveId).getMissCount();
}
/**
* 开启缓存自动刷新
*
* @param driveId
* 驱动器 ID
*/
public void startAutoCacheRefresh(Integer driveId) {
if (log.isDebugEnabled()) {
log.debug("开启缓存自动刷新 driveId: {}", driveId);
}
DriveConfig driveConfig = driverConfigRepository.findById(driveId).get();
Boolean autoRefreshCache = driveConfig.getAutoRefreshCache();
if (autoRefreshCache != null && autoRefreshCache) {
MyTimedCache<DriveCacheKey, List<FileItemDTO>> driveCache = drivesCache.get(driveId);
if (driveCache == null) {
driveCache = new MyTimedCache<>(timeout * 1000);
drivesCache.put(driveId, driveCache);
}
driveCache.schedulePrune(autoRefreshInterval * 1000);
}
}
/**
* 停止缓存自动刷新
*
* @param driveId
* 驱动器 ID
*/
public void stopAutoCacheRefresh(Integer driveId) {
if (log.isDebugEnabled()) {
log.debug("停止缓存自动刷新 driveId: {}", driveId);
}
MyTimedCache<DriveCacheKey, List<FileItemDTO>> driveCache = drivesCache.get(driveId);
if (driveCache != null) {
driveCache.cancelPruneSchedule();
}
}
}

View File

@@ -1,36 +0,0 @@
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

@@ -1,47 +0,0 @@
package im.zhaojun.zfile.config;
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
import im.zhaojun.zfile.model.entity.StorageConfig;
import im.zhaojun.zfile.service.StorageConfigService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
/**
* @author zhaojun
*/
@Configuration
public class OneDriveConfig {
@Resource
private StorageConfigService storageConfigService;
/**
* OneDrive 请求 RestTemplate, 会在请求头中添加 Bearer: Authorization {token} 信息, 用于 API 认证.
*/
@Bean
public RestTemplate oneDriveRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
ClientHttpRequestInterceptor interceptor = (httpRequest, bytes, clientHttpRequestExecution) -> {
HttpHeaders headers = httpRequest.getHeaders();
Integer driveId = Integer.valueOf(((List)headers.get("driveId")).get(0).toString());
StorageConfig accessTokenConfig =
storageConfigService.findByDriveIdAndKey(driveId, StorageConfigConstant.ACCESS_TOKEN_KEY);
String tokenValue = String.format("%s %s", "Bearer", accessTokenConfig.getValue());
httpRequest.getHeaders().add("Authorization", tokenValue);
return clientHttpRequestExecution.execute(httpRequest, bytes);
};
restTemplate.setInterceptors(Collections.singletonList(interceptor));
return restTemplate;
}
}

View File

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

View File

@@ -1,42 +0,0 @@
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;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
/**
* @author zhaojun
*/
@Configuration
public class ZFileConfiguration {
@Bean
public RestTemplate 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) -> {
ClientHttpResponse response = execution.execute(request, body);
HttpHeaders headers = response.getHeaders();
headers.put("Content-Type", Collections.singletonList("application/text"));
return response;
}));
return restTemplate;
}
}

View File

@@ -1,60 +0,0 @@
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

@@ -1,33 +0,0 @@
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

@@ -1,41 +0,0 @@
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

@@ -1,130 +0,0 @@
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

@@ -1,62 +0,0 @@
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

@@ -1,147 +0,0 @@
package im.zhaojun.zfile.context;
import com.alibaba.fastjson.JSON;
import im.zhaojun.zfile.exception.InvalidDriveException;
import im.zhaojun.zfile.model.entity.DriveConfig;
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import im.zhaojun.zfile.service.DriveConfigService;
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
import im.zhaojun.zfile.util.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* 每个驱动器对应一个 Service, 其中初始化好了与对象存储的连接信息.
* 此驱动器上下文环境用户缓存每个 Service, 避免重复创建连接.
* @author zhaojun
*/
@Component
@DependsOn("springContextHolder")
@Slf4j
public class DriveContext implements ApplicationContextAware {
/**
* Map<Integer, AbstractBaseFileService>
* Map<驱动器 ID, 驱动器连接 Service>
*/
private static Map<Integer, AbstractBaseFileService> drivesServiceMap = new ConcurrentHashMap<>();
@Resource
private DriveConfigService driveConfigService;
/**
* 项目启动时, 自动调用数据库已存储的所有驱动器进行初始化.
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
List<DriveConfig> list = driveConfigService.list();
for (DriveConfig driveConfig : list) {
try {
init(driveConfig.getId());
log.info("启动时初始化驱动器成功, 驱动器信息: {}", JSON.toJSONString(driveConfig));
} catch (Exception e) {
log.error("启动时初始化驱动器失败, 驱动器信息: {}", JSON.toJSONString(driveConfig), e);
}
}
}
/**
* 初始化指定驱动器的 Service, 添加到上下文环境中.
*
* @param driveId
* 驱动器 ID.
*/
public void init(Integer driveId) {
AbstractBaseFileService baseFileService = getBeanByDriveId(driveId);
if (baseFileService != null) {
if (log.isDebugEnabled()) {
log.debug("尝试初始化驱动器, driveId: {}", driveId);
}
baseFileService.init(driveId);
if (log.isDebugEnabled()) {
log.debug("初始化驱动器成功, driveId: {}", driveId);
}
drivesServiceMap.put(driveId, baseFileService);
}
}
/**
* 获取指定驱动器的 Service.
*
* @param driveId
* 驱动器 ID
*
* @return 驱动器对应的 Service
*/
public AbstractBaseFileService get(Integer driveId) {
AbstractBaseFileService abstractBaseFileService = drivesServiceMap.get(driveId);
if (abstractBaseFileService == null) {
throw new InvalidDriveException("此驱动器不存在或初始化失败, 请检查后台参数配置");
}
return abstractBaseFileService;
}
/**
* 销毁指定驱动器的 Service.
*
* @param driveId
* 驱动器 ID
*/
public void destroy(Integer driveId) {
if (log.isDebugEnabled()) {
log.debug("清理驱动器上下文对象, driveId: {}", driveId);
}
drivesServiceMap.remove(driveId);
}
/**
* 获取指定驱动器对应的 Service, 状态为未初始化
*
* @param driveId
* 驱动器 ID
*
* @return 驱动器对应未初始化的 Service
*/
private AbstractBaseFileService getBeanByDriveId(Integer driveId) {
StorageTypeEnum storageTypeEnum = driveConfigService.findStorageTypeById(driveId);
Map<String, AbstractBaseFileService> beansOfType = SpringContextHolder.getBeansOfType(AbstractBaseFileService.class);
for (AbstractBaseFileService value : beansOfType.values()) {
if (Objects.equals(value.getStorageTypeEnum(), storageTypeEnum)) {
return SpringContextHolder.getBean(value.getClass());
}
}
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

@@ -1,56 +0,0 @@
package im.zhaojun.zfile.context;
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 存储类型工厂类
* @author zhaojun
*/
@Component
public class StorageTypeContext implements ApplicationContextAware {
private static Map<String, AbstractBaseFileService> storageTypeEnumFileServiceMap;
private static ApplicationContext applicationContext;
/**
* 项目启动时执行
*/
@Override
public void setApplicationContext(ApplicationContext act) throws BeansException {
applicationContext = act;
// 获取 Spring 容器中所有 FileService 类型的类
storageTypeEnumFileServiceMap = act.getBeansOfType(AbstractBaseFileService.class);
}
/**
* 获取指定存储类型 Service
*/
public static AbstractBaseFileService getStorageTypeService(StorageTypeEnum type) {
AbstractBaseFileService result = null;
for (AbstractBaseFileService fileService : storageTypeEnumFileServiceMap.values()) {
if (fileService.getStorageTypeEnum() == type) {
result = fileService;
break;
}
}
return result;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
}

View File

@@ -1,54 +0,0 @@
package im.zhaojun.zfile.controller.admin;
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.service.SystemConfigService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 管理后台接口
* @author zhaojun
*/
@RestController
@RequestMapping("/admin")
public class AdminController {
@Resource
private SystemConfigService systemConfigService;
/**
* 获取系统配置
*/
@GetMapping("/config")
public ResultBean getConfig() {
SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
return ResultBean.success(systemConfigDTO);
}
/**
* 更新系统配置
*/
@PostMapping("/config")
public ResultBean updateConfig(SystemConfigDTO systemConfigDTO) {
systemConfigDTO.setId(1);
systemConfigService.updateSystemConfig(systemConfigDTO);
return ResultBean.success();
}
/**
* 修改管理员登陆密码
*/
@PostMapping("/update-pwd")
public ResultBean updatePwd(String username, String password) {
systemConfigService.updateUsernameAndPwd(username, password);
return ResultBean.success();
}
}

View File

@@ -1,72 +0,0 @@
package im.zhaojun.zfile.controller.admin;
import im.zhaojun.zfile.model.dto.CacheInfoDTO;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.service.DriveConfigService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 缓存 Controller
*
* @author zhaojun
*/
@RestController
@RequestMapping("/admin/cache")
public class CacheController {
@Resource
private DriveConfigService driveConfigService;
@PostMapping("/{driveId}/enable")
public ResultBean enableCache(@PathVariable("driveId") Integer driveId) {
driveConfigService.updateCacheStatus(driveId, true);
return ResultBean.success();
}
@PostMapping("/{driveId}/disable")
public ResultBean disableCache(@PathVariable("driveId") Integer driveId) {
driveConfigService.updateCacheStatus(driveId, false);
return ResultBean.success();
}
@GetMapping("/{driveId}/info")
public ResultBean cacheInfo(@PathVariable("driveId") Integer driveId) {
CacheInfoDTO cacheInfo = driveConfigService.findCacheInfo(driveId);
return ResultBean.success(cacheInfo);
}
@PostMapping("/{driveId}/refresh")
public ResultBean refreshCache(@PathVariable("driveId") Integer driveId, String key) throws Exception {
driveConfigService.refreshCache(driveId, key);
return ResultBean.success();
}
@PostMapping("/{driveId}/auto-refresh/start")
public ResultBean enableAutoRefresh(@PathVariable("driveId") Integer driveId) {
driveConfigService.startAutoCacheRefresh(driveId);
return ResultBean.success();
}
@PostMapping("/{driveId}/auto-refresh/stop")
public ResultBean disableAutoRefresh(@PathVariable("driveId") Integer driveId) {
driveConfigService.stopAutoCacheRefresh(driveId);
return ResultBean.success();
}
@PostMapping("/{driveId}/clear")
public ResultBean clearCache(@PathVariable("driveId") Integer driveId) {
driveConfigService.clearCache(driveId);
return ResultBean.success();
}
}

View File

@@ -1,33 +0,0 @@
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

@@ -1,173 +0,0 @@
package im.zhaojun.zfile.controller.admin;
import com.alibaba.fastjson.JSONObject;
import im.zhaojun.zfile.model.dto.DriveConfigDTO;
import im.zhaojun.zfile.model.entity.DriveConfig;
import im.zhaojun.zfile.model.entity.FilterConfig;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.service.DriveConfigService;
import im.zhaojun.zfile.service.FilterConfigService;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* 驱动器相关操作 Controller
* @author zhaojun
*/
@RestController
@RequestMapping("/admin")
public class DriveController {
@Resource
private DriveConfigService driveConfigService;
@Resource
private FilterConfigService filterConfigService;
/**
* 获取所有驱动器列表
*
* @return 驱动器列表
*/
@GetMapping("/drives")
public ResultBean driveList() {
List<DriveConfig> list = driveConfigService.list();
return ResultBean.success(list);
}
/**
* 获取指定驱动器基本信息及其参数
*
* @param driveId
* 驱动器 ID
*
* @return 驱动器基本信息
*/
@GetMapping("/drive/{driveId}")
public ResultBean driveItem(@PathVariable Integer driveId) {
DriveConfigDTO driveConfig = driveConfigService.findDriveConfigDTOById(driveId);
return ResultBean.success(driveConfig);
}
/**
* 保存驱动器设置
*/
@PostMapping("/drive")
public ResultBean saveDriveItem(@RequestBody DriveConfigDTO driveConfigDTO) {
driveConfigService.saveDriveConfigDTO(driveConfigDTO);
return ResultBean.success();
}
/**
* 删除驱动器设置
*
* @param driveId
* 驱动器 ID
*/
@DeleteMapping("/drive/{driveId}")
public ResultBean deleteDriveItem(@PathVariable Integer driveId) {
driveConfigService.deleteById(driveId);
return ResultBean.success();
}
/**
* 启用驱动器
*
* @param driveId
* 驱动器 ID
*/
@PostMapping("/drive/{driveId}/enable")
public ResultBean enable(@PathVariable Integer driveId) {
DriveConfig driveConfig = driveConfigService.findById(driveId);
driveConfig.setEnable(true);
driveConfigService.updateDriveConfig(driveConfig);
return ResultBean.success();
}
/**
* 停止驱动器
*
* @param driveId
* 驱动器 ID
*/
@PostMapping("/drive/{driveId}/disable")
public ResultBean disable(@PathVariable Integer driveId) {
DriveConfig driveConfig = driveConfigService.findById(driveId);
driveConfig.setEnable(false);
driveConfigService.updateDriveConfig(driveConfig);
return ResultBean.success();
}
/**
* 根据驱动器 ID 获取过滤文件列表
*
* @param driveId
* 驱动器 ID
*/
@GetMapping("/drive/{driveId}/filters")
public ResultBean getFilters(@PathVariable Integer driveId) {
return ResultBean.success(filterConfigService.findByDriveId(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

@@ -1,38 +0,0 @@
package im.zhaojun.zfile.controller.admin;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ZipUtil;
import im.zhaojun.zfile.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.util.Date;
/**
* 日志相关 Controller
* @author zhaojun
*/
@RestController
@RequestMapping("/admin")
@Slf4j
public class LogController {
/**
* 系统日志下载
*/
@GetMapping("/log")
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.exportSingleThread(fileZip, "ZFile 诊断日志 - " + currentDate + ".zip");
}
}

View File

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

View File

@@ -1,47 +0,0 @@
package im.zhaojun.zfile.controller.admin;
import im.zhaojun.zfile.context.StorageTypeContext;
import im.zhaojun.zfile.model.entity.StorageConfig;
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 系统元数据 Controller
* @author zhaojun
*/
@RestController
@RequestMapping("/admin")
public class MateDataController {
/**
* 返回支持的存储引擎.
*/
@GetMapping("/support-strategy")
public ResultBean supportStrategy() {
StorageTypeEnum[] values = StorageTypeEnum.values();
return ResultBean.successData(values);
}
/**
* 获取指定存储策略的表单域
*
* @param storageType
* 存储策略
*
* @return 所有表单域
*/
@GetMapping("/strategy-form")
public ResultBean getFormByStorageType(StorageTypeEnum storageType) {
AbstractBaseFileService storageTypeService = StorageTypeContext.getStorageTypeService(storageType);
List<StorageConfig> storageConfigList = storageTypeService.storageStrategyConfigList();
return ResultBean.success(storageConfigList);
}
}

View File

@@ -1,45 +0,0 @@
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,102 +0,0 @@
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;
import org.springframework.web.bind.annotation.PathVariable;
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
public class DirectLinkController {
@Resource
private DriveContext driveContext;
@Resource
private DriveConfigService driveConfigService;
/**
* 获取指定驱动器, 某个文件的直链, 然后重定向过去.
* @param driveId
* 驱动器 ID
*
* @return 重定向至文件直链
*/
@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);
AntPathMatcher apm = new AntPathMatcher();
String filePath = apm.extractPathWithinPattern(bestMatchPattern, path);
if (filePath.length() > 0 && filePath.charAt(0) != ZFileConstant.PATH_SEPARATOR_CHAR) {
filePath = "/" + filePath;
}
AbstractBaseFileService fileService = driveContext.get(driveId);
FileItemDTO fileItem = fileService.getFileItem(filePath);
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) {
String origin = url.substring(0, queryIndex);
String queryString = url.substring(queryIndex + 1);
url = URLUtil.encode(origin) + "?" + URLUtil.encode(queryString);
} else {
url = URLUtil.encode(url);
}
if (Objects.equals(fileItem.getType(), FileTypeEnum.FOLDER)) {
return "redirect:" + fileItem.getUrl();
} else {
return "redirect:" + url;
}
}
}

View File

@@ -1,243 +0,0 @@
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.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.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;
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
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;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpClientErrorException;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* 前台文件管理
* @author zhaojun
*/
@Slf4j
@RequestMapping("/api")
@RestController
public class FileController {
@Value("${zfile.debug}")
private Boolean debug;
@Resource
private SystemConfigService systemConfigService;
@Resource
private DriveContext driveContext;
@Resource
private DriveConfigService driveConfigService;
@Resource
private FilterConfigService filterConfigService;
/**
* 获取所有已启用的驱动器
*
* @return 所有已启用驱动器
*/
@GetMapping("/drive/list")
public ResultBean drives() {
List<DriveConfig> driveList = driveConfigService.listOnlyEnable();
boolean isInstall = systemConfigService.getIsInstall();
DriveListDTO driveListDTO = new DriveListDTO(driveList, isInstall);
return ResultBean.success(driveListDTO);
}
/**
* 获取某个驱动器下, 指定路径的数据
*
* @param driveId
* 驱动器 ID
*
* @param path
* 路径
*
* @param password
* 文件夹密码, 某些文件夹需要密码才能访问, 当不需要密码时, 此参数可以为空
*
* @return 当前路径下所有文件及文件夹
*/
@GetMapping("/list/{driveId}")
public ResultBean list(@PathVariable(name = "driveId") Integer driveId,
@RequestParam(defaultValue = "/") String path,
@RequestParam(required = false) String password,
@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);
for (FileItemDTO fileItemDTO : fileItemList) {
if (ZFileConstant.PASSWORD_FILE_NAME.equals(fileItemDTO.getName())) {
String expectedPasswordContent;
try {
expectedPasswordContent = HttpUtil.getTextContent(fileItemDTO.getUrl());
} catch (HttpClientErrorException httpClientErrorException) {
log.trace("尝试重新获取密码文件缓存中链接后仍失败, driveId: {}, path: {}, inputPassword: {}, passwordFile:{} ",
driveId, path, inputPassword, JSON.toJSONString(fileItemDTO), httpClientErrorException);
try {
String pwdFileFullPath = StringUtils.removeDuplicateSeparator(fileItemDTO.getPath() + ZFileConstant.PATH_SEPARATOR + fileItemDTO.getName());
FileItemDTO pwdFileItem = fileService.getFileItem(pwdFileFullPath);
expectedPasswordContent = HttpUtil.getTextContent(pwdFileItem.getUrl());
} catch (Exception e) {
throw new PasswordVerifyException("此文件夹为加密文件夹, 但密码检查异常, 请联系管理员检查密码设置", e);
}
} catch (Exception e) {
throw new PasswordVerifyException("此文件夹为加密文件夹, 但密码检查异常, 请联系管理员检查密码设置", e);
}
if (matchPassword(expectedPasswordContent, inputPassword)) {
break;
}
if (StrUtil.isEmpty(inputPassword)) {
return VerifyResult.fail("此文件夹需要密码.", ResultBean.REQUIRED_PASSWORD);
}
return VerifyResult.fail("密码错误.", ResultBean.INVALID_PASSWORD);
}
}
return VerifyResult.success();
}
/**
* 校验两个密码是否相同, 忽略空白字符
*
* @param expectedPasswordContent
* 预期密码
*
* @param password
* 实际输入密码
*
* @return 是否匹配
*/
private boolean matchPassword(String expectedPasswordContent, String password) {
if (Objects.equals(expectedPasswordContent, password)) {
return true;
}
if (expectedPasswordContent == null) {
return false;
}
if (password == null) {
return false;
}
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, Integer driveId) {
if (fileItemList == null) {
return;
}
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

@@ -1,45 +0,0 @@
package im.zhaojun.zfile.controller.home;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.util.AudioUtil;
import im.zhaojun.zfile.util.HttpUtil;
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("/common")
public class FileParseController {
/**
* 获取文件内容, 仅限用于 txt, md, ini 等普通文本文件.
*
* @param url
* 文件路径
*
* @return 文件内容
*/
@GetMapping("/content")
public ResultBean getContent(String url) {
return ResultBean.successData(HttpUtil.getTextContent(url));
}
/**
* 获取音频文件信息
*
* @param url
* 文件 URL
*
* @return 音频信息, 标题封面等信息
*/
@GetMapping("/audio-info")
public ResultBean getAudioInfo(String url) throws Exception {
return ResultBean.success(AudioUtil.getAudioInfo(url));
}
}

View File

@@ -1,52 +0,0 @@
package im.zhaojun.zfile.controller.home;
import im.zhaojun.zfile.context.DriveContext;
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.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.HandlerMapping;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
/**
* 本地存储 Controller
* @author zhaojun
*/
@Controller
public class LocalController {
@Resource
private DriveContext driveContext;
/**
* 本地存储下载指定文件
*
* @param driveId
* 驱动器 ID
* @param type
* 附件预览类型:
* download:下载
* default: 浏览器默认行为
*/
@GetMapping("/file/{driveId}/**")
@ResponseBody
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);
File file = new File(StringUtils.removeDuplicateSeparator(localService.getFilePath() + ZFileConstant.PATH_SEPARATOR + filePath));
FileUtil.export(request, response, file, type);
}
}

View File

@@ -1,107 +0,0 @@
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

@@ -1,159 +0,0 @@
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

@@ -1,45 +0,0 @@
package im.zhaojun.zfile.controller.install;
import cn.hutool.crypto.SecureUtil;
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.model.support.ResultBean;
import im.zhaojun.zfile.service.SystemConfigService;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 系统安装初始化
* @author zhaojun
*/
@RestController
public class InstallController {
@Resource
private SystemConfigService systemConfigService;
@GetMapping("/is-installed")
public ResultBean isInstall() {
if (!StringUtils.isEmpty(systemConfigService.getAdminUsername())) {
return ResultBean.error("请勿重复初始化");
}
return ResultBean.success();
}
@PostMapping("/doInstall")
public ResultBean install(SystemConfigDTO systemConfigDTO) {
if (!StringUtils.isEmpty(systemConfigService.getAdminUsername())) {
return ResultBean.error("请勿重复初始化.");
}
systemConfigDTO.setPassword(SecureUtil.md5(systemConfigDTO.getPassword()));
systemConfigService.updateSystemConfig(systemConfigDTO);
return ResultBean.success();
}
}

View File

@@ -1,57 +0,0 @@
package im.zhaojun.zfile.controller.onedrive;
import im.zhaojun.zfile.model.support.OneDriveToken;
import im.zhaojun.zfile.service.impl.OneDriveChinaServiceImpl;
import im.zhaojun.zfile.service.impl.OneDriveServiceImpl;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.Resource;
/**
* @author zhaojun
*/
@Controller
@RequestMapping(value = {"/onedrive", "/onedirve"})
public class OneDriveCallbackController {
@Resource
private OneDriveServiceImpl oneDriveServiceImpl;
@Resource
private OneDriveChinaServiceImpl oneDriveChinaServiceImpl;
@GetMapping("/callback")
public String oneDriveCallback(String code, Model model) {
OneDriveToken oneDriveToken = oneDriveServiceImpl.getToken(code);
model.addAttribute("accessToken", oneDriveToken.getAccessToken());
model.addAttribute("refreshToken", oneDriveToken.getRefreshToken());
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) {
OneDriveToken oneDriveToken = oneDriveChinaServiceImpl.getToken(code);
model.addAttribute("accessToken", oneDriveToken.getAccessToken());
model.addAttribute("refreshToken", oneDriveToken.getRefreshToken());
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

@@ -1,134 +0,0 @@
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

@@ -0,0 +1,67 @@
package im.zhaojun.zfile.core;
import im.zhaojun.zfile.core.util.AjaxJson;
import org.slf4j.MDC;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* @author zhaojun
*/
@ControllerAdvice
public class CommonResultControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
@NonNull Class<? extends HttpMessageConverter<?>> converterType) {
return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
}
@Override
@NonNull
public final Object beforeBodyWrite(@Nullable Object body,
@NonNull MethodParameter returnType,
@NonNull MediaType contentType,
@NonNull Class<? extends HttpMessageConverter<?>> converterType,
@NonNull ServerHttpRequest request,
@NonNull ServerHttpResponse response) {
MappingJacksonValue container = getOrCreateContainer(body);
// The contain body will never be null
beforeBodyWriteInternal(container, contentType, returnType, request, response);
return container;
}
/**
* Wrap the body in a {@link MappingJacksonValue} value container (for providing
* additional serialization instructions) or simply cast it if already wrapped.
*/
private MappingJacksonValue getOrCreateContainer(Object body) {
return body instanceof MappingJacksonValue ? (MappingJacksonValue) body :
new MappingJacksonValue(body);
}
private void beforeBodyWriteInternal(MappingJacksonValue bodyContainer,
MediaType contentType,
MethodParameter returnType,
ServerHttpRequest request,
ServerHttpResponse response) {
// Get return body
Object returnBody = bodyContainer.getValue();
if (returnBody instanceof AjaxJson) {
// If the return body is instance of BaseResponse, then just do nothing
AjaxJson<?> baseResponse = (AjaxJson<?>) returnBody;
baseResponse.setTraceId(MDC.get("traceId"));
}
}
}

View File

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

View File

@@ -0,0 +1,83 @@
package im.zhaojun.zfile.core.config;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* Jackson 枚举反序列化器
*
* @author zhaojun
*/
@Slf4j
@Setter
@JsonComponent
public class JacksonEnumDeserializer extends JsonDeserializer<Enum<?>> implements ContextualDeserializer {
private Class<?> clazz;
/**
* 反序列化操作
*
* @param jsonParser
* json 解析器
*
* @param ctx
* 反序列化上下文
*
* @return 反序列化后的枚举值
* @throws IOException 反序列化异常
*/
@Override
public Enum<?> deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
Class<?> enumType = clazz;
if (Objects.isNull(enumType) || !enumType.isEnum()) {
return null;
}
String text = jsonParser.getText();
Method method = StringToEnumConverterFactory.getMethod(clazz);
Enum<?>[] enumConstants = (Enum<?>[]) enumType.getEnumConstants();
// 将值与枚举对象对应并缓存
for (Enum<?> e : enumConstants) {
try {
if (Objects.equals(method.invoke(e).toString(), text)) {
return e;
}
} catch (IllegalAccessException | InvocationTargetException ex) {
log.error("获取枚举值错误!!! ", ex);
}
}
return null;
}
/**
* 为不同的枚举获取合适的解析器
*
* @param ctx
* 反序列化上下文
*
* @param property
* property
*/
@Override
public JsonDeserializer<Enum<?>> createContextual(DeserializationContext ctx, BeanProperty property) {
Class<?> rawCls = ctx.getContextualType().getRawClass();
JacksonEnumDeserializer converter = new JacksonEnumDeserializer();
converter.setClazz(rawCls);
return converter;
}
}

View File

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

View File

@@ -0,0 +1,60 @@
package im.zhaojun.zfile.core.config;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* mybatis-plus 配置类
*
* @author zhaojun
*/
@Configuration
public class MyBatisPlusConfig {
@Resource
private DataSource dataSource;
@Value("${spring.datasource.driver-class-name}")
private String datasourceDriveClassName;
@Value("${spring.datasource.url}")
private String datasourceUrl;
/**
* 如果是 sqlite 数据库,自动创建数据库文件所在目录
*/
@PostConstruct
public void init() {
if (StrUtil.equals(datasourceDriveClassName, "org.sqlite.JDBC")) {
String path = datasourceUrl.replace("jdbc:sqlite:", "");
String folderPath = FileUtil.getParent(path, 1);
if (!FileUtil.exist(folderPath)) {
FileUtil.mkdir(folderPath);
}
}
}
/**
* mybatis plus 分页插件配置
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() throws SQLException {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
String databaseProductName = dataSource.getConnection().getMetaData().getDatabaseProductName();
DbType dbType = DbType.getDbType(databaseProductName);
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(dbType));
return interceptor;
}
}

View File

@@ -0,0 +1,27 @@
package im.zhaojun.zfile.core.config;
import im.zhaojun.zfile.core.httpclient.ZFileOkHttp3ClientHttpRequestFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* restTemplate 相关配置
*
* @author zhaojun
*/
@Configuration
public class RestTemplateConfig {
/**
* OneDrive 请求 RestTemplate.
* 获取 header 中的 storageId 来判断到底是哪个存储源 ID, 在请求头中添加 Bearer: Authorization {token} 信息, 用于 API 认证.
*/
@Bean
public RestTemplate oneDriveRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new ZFileOkHttp3ClientHttpRequestFactory());
return restTemplate;
}
}

View File

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

View File

@@ -0,0 +1,33 @@
package im.zhaojun.zfile.core.config;
import cn.hutool.core.util.BooleanUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.support.NoOpCacheManager;
import org.springframework.cache.transaction.TransactionAwareCacheManagerProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Spring Cache 相关配置
*
* @author zhaojun
*/
@Configuration
@EnableCaching
public class SpringCacheConfig {
@Value("${zfile.dbCache.enable:true}")
private Boolean dbCacheEnable;
/**
* 使用 TransactionAwareCacheManagerProxy 装饰 ConcurrentMapCacheManager使其支持事务 (将 put、evict、clear 操作延迟到事务成功提交再执行.
*/
@Bean
public CacheManager cacheManager() {
return BooleanUtil.isFalse(dbCacheEnable) ? new NoOpCacheManager() : new TransactionAwareCacheManagerProxy(new ConcurrentMapCacheManager());
}
}

View File

@@ -0,0 +1,116 @@
package im.zhaojun.zfile.core.config;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import javax.validation.constraints.NotNull;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* String 转枚举通用转换器工厂
*
* @author zhaojun
*/
@Slf4j
public class StringToEnumConverterFactory implements ConverterFactory<String, Enum<?>> {
/**
* 存储枚举类型的缓存
*/
private static final Map<Class<?>, Converter<String, ? extends Enum<?>>> CONVERTER_MAP = new ConcurrentHashMap<>();
/**
* 枚举类的获取枚举值方法缓存
*/
private static final Map<Class<?>, Method> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
@Override
@SuppressWarnings("unchecked cast")
public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
// 缓存转换器
Converter<String, T> converter = (Converter<String, T>) CONVERTER_MAP.get(targetType);
if (converter == null) {
converter = new StringToEnumConverter<>(targetType);
CONVERTER_MAP.put(targetType, converter);
}
return converter;
}
static class StringToEnumConverter<T extends Enum<?>> implements Converter<String, T> {
private final Map<String, T> enumMap = new ConcurrentHashMap<>();
StringToEnumConverter(Class<T> enumType) {
Method method = getMethod(enumType);
T[] enums = enumType.getEnumConstants();
// 将值与枚举对象对应并缓存
for (T e : enums) {
try {
enumMap.put(method.invoke(e).toString(), e);
} catch (IllegalAccessException | InvocationTargetException ex) {
log.error("获取枚举值错误!!! ", ex);
}
}
}
@Override
public T convert(@NotNull String source) {
// 获取
T t = enumMap.get(source);
if (t == null) {
throw new IllegalArgumentException("该字符串找不到对应的枚举对象 字符串:" + source);
}
return t;
}
}
public static <T> Method getMethod(Class<T> enumType) {
Method method;
// 找到取值的方法
if (IEnum.class.isAssignableFrom(enumType)) {
try {
method = enumType.getMethod("getValue");
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(String.format("类:%s 找不到 getValue方法",
enumType.getName()));
}
} else {
method = TABLE_METHOD_OF_ENUM_TYPES.computeIfAbsent(enumType, k -> {
Field field =
dealEnumType(enumType).orElseThrow(() -> new IllegalArgumentException(String.format(
"类:%s 找不到 EnumValue注解", enumType.getName())));
Class<?> fieldType = field.getType();
String fieldName = field.getName();
String methodName = StringUtils.concatCapitalize(boolean.class.equals(fieldType) ? "is" : "get", fieldName);
try {
return enumType.getDeclaredMethod(methodName);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return null;
});
}
return method;
}
private static Optional<Field> dealEnumType(Class<?> clazz) {
return clazz.isEnum() ?
Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EnumValue.class)).findFirst() : Optional.empty();
}
}

View File

@@ -1,6 +1,6 @@
package im.zhaojun.zfile.config;
package im.zhaojun.zfile.core.config;
import im.zhaojun.zfile.model.enums.StorageTypeEnumDeSerializerConvert;
import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
@@ -16,28 +16,40 @@ import java.util.HashSet;
import java.util.Set;
/**
* ZFile Web 相关配置.
*
* @author zhaojun
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 添加自定义枚举格式化器.
* @see StorageTypeEnum
*/
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StorageTypeEnumDeSerializerConvert());
registry.addConverterFactory(new StringToEnumConverterFactory());
}
/**
* 支持 url 中传入 <>[\]^`{|} 这些特殊字符.
*/
@Bean
public ServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory();
// 添加对 URL 中特殊符号的支持.
webServerFactory.addConnectorCustomizers(connector -> {
connector.setAttribute("relaxedPathChars", "<>[\\]^`{|}");
connector.setAttribute("relaxedQueryChars", "<>[\\]^`{|}");
connector.setProperty("relaxedPathChars", "<>[\\]^`{|}%[]");
connector.setProperty("relaxedQueryChars", "<>[\\]^`{|}%[]");
});
return webServerFactory;
}
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
return factory -> {
@@ -49,5 +61,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
factory.setErrorPages(errorPages);
};
}
}

View File

@@ -0,0 +1,19 @@
package im.zhaojun.zfile.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author zhaojun
*/
@Data
@EnableConfigurationProperties
@Component
@ConfigurationProperties(prefix = "zfile")
public class ZFileProperties {
private boolean debug;
}

View File

@@ -0,0 +1,16 @@
package im.zhaojun.zfile.core.constant;
/**
* Slf4j mdc 常量
*
* @author zhaojun
*/
public class MdcConstant {
public static final String TRACE_ID = "traceId";
public static final String IP = "ip";
public static final String USER = "user";
}

View File

@@ -0,0 +1,29 @@
package im.zhaojun.zfile.core.constant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
/**
* ZFile 常量
*
* @author zhaojun
*/
@Configuration
public class ZFileConstant {
public static final Character PATH_SEPARATOR_CHAR = '/';
public static final String PATH_SEPARATOR = "/";
/**
* 最大支持文本文件大小为 ? KB 的文件内容.
*/
public static Long TEXT_MAX_FILE_SIZE_KB = 100L;
@Autowired(required = false)
public void setTextMaxFileSizeMb(@Value("${zfile.preview.text.maxFileSizeKb}") Long maxFileSizeKb) {
ZFileConstant.TEXT_MAX_FILE_SIZE_KB = maxFileSizeKb;
}
}

View File

@@ -0,0 +1,25 @@
package im.zhaojun.zfile.core.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 处理前端首页 Controller
*
* @author zhaojun
*/
@Controller
public class FrontIndexController {
/**
* 所有未找到的页面都跳转到首页, 用户解决 vue history 直接访问 404 的问题
*
* @return 转发到 /index.html
*/
@RequestMapping(value = "/**/{[path:[^\\.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
}

View File

@@ -0,0 +1,47 @@
package im.zhaojun.zfile.core.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ZipUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import im.zhaojun.zfile.core.util.FileResponseUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.util.Date;
/**
* 获取系统日志接口
*
* @author zhaojun
*/
@Api(tags = "日志")
@ApiSort(8)
@Slf4j
@RestController
@RequestMapping("/admin")
public class LogController {
@Value("${zfile.log.path}")
private String zfileLogPath;
@GetMapping("/log/download")
@ApiOperation(value = "下载系统日志")
public ResponseEntity<Resource> downloadLog() {
if (log.isDebugEnabled()) {
log.debug("下载诊断日志");
}
File fileZip = ZipUtil.zip(zfileLogPath);
String currentDate = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss");
return FileResponseUtil.exportSingleThread(fileZip, "ZFile 诊断日志 - " + currentDate + ".zip");
}
}

View File

@@ -0,0 +1,14 @@
package im.zhaojun.zfile.core.exception;
/**
* 非法使用下载链接异常.
*
* @author zhaojun
*/
public class IllegalDownloadLinkException extends ZFileRuntimeException {
public IllegalDownloadLinkException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,14 @@
package im.zhaojun.zfile.core.exception;
/**
* 系统初始化异常
*
* @author zhaojun
*/
public class InstallSystemException extends ZFileRuntimeException {
public InstallSystemException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,14 @@
package im.zhaojun.zfile.core.exception;
/**
* 无效的直链异常
*
* @author zhaojun
*/
public class InvalidShortLinkException extends ZFileRuntimeException {
public InvalidShortLinkException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,14 @@
package im.zhaojun.zfile.core.exception;
/**
* 登陆验证码验证异常
*
* @author zhaojun
*/
public class LoginVerifyException extends ZFileRuntimeException {
public LoginVerifyException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,22 @@
package im.zhaojun.zfile.core.exception;
/**
* 密码校验失败异常
*
* @author zhaojun
*/
public class PasswordVerifyException extends RuntimeException {
private final Integer code;
public PasswordVerifyException(Integer code, String message) {
super(message);
this.code = code;
}
public Integer getCode() {
return code;
}
}

View File

@@ -0,0 +1,14 @@
package im.zhaojun.zfile.core.exception;
/**
* 文件预览异常类
*
* @author zhaojun
*/
public class PreviewException extends ZFileRuntimeException {
public PreviewException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,42 @@
package im.zhaojun.zfile.core.exception;
import im.zhaojun.zfile.core.util.CodeMsg;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Service 层异常
* 所有 message 均为系统日志打印输出, CodeMsg 中的消息才是返回给客户端的消息.
*
* @author zhaojun
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ServiceException extends RuntimeException {
private CodeMsg codeMsg;
public ServiceException(CodeMsg codeMsg) {
this.codeMsg = codeMsg;
}
public ServiceException(String message, CodeMsg codeMsg) {
super(message);
this.codeMsg = codeMsg;
}
public ServiceException(String message, Throwable cause, CodeMsg codeMsg) {
super(message, cause);
this.codeMsg = codeMsg;
}
public ServiceException(Throwable cause, CodeMsg codeMsg) {
super(cause);
this.codeMsg = codeMsg;
}
public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, CodeMsg codeMsg) {
super(message, cause, enableSuppression, writableStackTrace);
this.codeMsg = codeMsg;
}
}

View File

@@ -0,0 +1,21 @@
package im.zhaojun.zfile.core.exception;
import im.zhaojun.zfile.module.storage.model.param.IStorageParam;
import lombok.Getter;
/**
* 存储源自动设置 cors 异常
*
* @author zhaojun
*/
@Getter
public class StorageSourceAutoConfigCorsException extends RuntimeException {
private final IStorageParam iStorageParam;
public StorageSourceAutoConfigCorsException(String message, Throwable cause, IStorageParam iStorageParam) {
super(message, cause);
this.iStorageParam = iStorageParam;
}
}

View File

@@ -0,0 +1,60 @@
package im.zhaojun.zfile.core.exception;
import im.zhaojun.zfile.core.util.CodeMsg;
import lombok.EqualsAndHashCode;
import lombok.Getter;
/**
* 存储源异常
*
* @author zhaojun
*/
@EqualsAndHashCode(callSuper = true)
@Getter
public class StorageSourceException extends ServiceException {
/**
* 是否使用异常消息进行接口返回,如果是则取异常的 message, 否则取 CodeMsg 中的 message
*/
private boolean responseExceptionMessage;
/**
* 存储源 ID
*/
private final Integer storageId;
public StorageSourceException(CodeMsg codeMsg, Integer storageId, String message) {
super(message, codeMsg);
this.storageId = storageId;
}
public StorageSourceException(CodeMsg codeMsg, Integer storageId, String message, Throwable cause) {
super(message, cause, codeMsg);
this.storageId = storageId;
}
/**
* 根据 responseExceptionMessage 判断使用异常消息进行接口返回,如果是则取异常的 message, 否则取 CodeMsg 中的 message
*
* @return 异常消息
*/
public String getResultMessage() {
return responseExceptionMessage ? super.getMessage() : super.getCodeMsg().getMsg();
}
/**
* 设置值是否使用异常消息进行接口返回
*
* @param responseExceptionMessage
* 是否使用异常消息进行接口返回
*
* @return 当前对象
*/
public StorageSourceException setResponseExceptionMessage(boolean responseExceptionMessage) {
this.responseExceptionMessage = responseExceptionMessage;
return this;
}
}

View File

@@ -0,0 +1,14 @@
package im.zhaojun.zfile.core.exception;
/**
* 存储源不支持代理上传异常
*
* @author zhaojun
*/
public class StorageSourceNotSupportProxyUploadException extends ZFileRuntimeException {
public StorageSourceNotSupportProxyUploadException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,22 @@
package im.zhaojun.zfile.core.exception;
import lombok.Getter;
/**
* @author zhaojun
*/
@Getter
public class StorageSourceRefreshTokenException extends RuntimeException {
private final Integer storageId;
public StorageSourceRefreshTokenException(String message, Integer storageId) {
super(message);
this.storageId = storageId;
}
public StorageSourceRefreshTokenException(String message, Throwable cause, Integer storageId) {
super(message, cause);
this.storageId = storageId;
}
}

View File

@@ -0,0 +1,17 @@
package im.zhaojun.zfile.core.exception;
/**
* 文件解析异常
*
* @author zhaojun
*/
public class TextParseException extends ZFileRuntimeException {
public TextParseException(String message) {
super(message);
}
public TextParseException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,26 @@
package im.zhaojun.zfile.core.exception;
/**
* @author zhaojun
*/
public class ZFileRetryException extends RuntimeException {
public ZFileRetryException() {
}
public ZFileRetryException(String message) {
super(message);
}
public ZFileRetryException(String message, Throwable cause) {
super(message, cause);
}
public ZFileRetryException(Throwable cause) {
super(cause);
}
public ZFileRetryException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -0,0 +1,15 @@
package im.zhaojun.zfile.core.exception;
/**
* @author zhaojun
*/
public class ZFileRuntimeException extends RuntimeException {
public ZFileRuntimeException(String message) {
super(message);
}
public ZFileRuntimeException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,25 @@
package im.zhaojun.zfile.core.exception.file;
import im.zhaojun.zfile.core.exception.StorageSourceException;
import im.zhaojun.zfile.core.util.CodeMsg;
/**
* 无效的存储源异常
*
* @author zhaojun
*/
public class InvalidStorageSourceException extends StorageSourceException {
public InvalidStorageSourceException(String message) {
super(CodeMsg.STORAGE_SOURCE_NOT_FOUND, null, message);
}
public InvalidStorageSourceException(Integer storageId) {
super(CodeMsg.STORAGE_SOURCE_NOT_FOUND, storageId, CodeMsg.STORAGE_SOURCE_NOT_FOUND.getMsg());
}
public InvalidStorageSourceException(Integer storageId, String message) {
super(CodeMsg.STORAGE_SOURCE_NOT_FOUND, storageId, message);
}
}

View File

@@ -0,0 +1,21 @@
package im.zhaojun.zfile.core.exception.file.init;
import im.zhaojun.zfile.core.exception.StorageSourceException;
import im.zhaojun.zfile.core.util.CodeMsg;
/**
* 存储源初始化异常
*
* @author zhaojun
*/
public class InitializeStorageSourceException extends StorageSourceException {
public InitializeStorageSourceException(CodeMsg codeMsg, Integer storageId, String message) {
super(codeMsg, storageId, message);
}
public InitializeStorageSourceException(CodeMsg codeMsg, Integer storageId, String message, Throwable cause) {
super(codeMsg, storageId, message, cause);
}
}

View File

@@ -0,0 +1,24 @@
package im.zhaojun.zfile.core.exception.file.operator;
import im.zhaojun.zfile.core.exception.StorageSourceException;
import im.zhaojun.zfile.core.util.CodeMsg;
/**
* 禁止服务器代理下载异常
*
* @author zhaojun
*/
public class DisableProxyDownloadException extends StorageSourceException {
public DisableProxyDownloadException(CodeMsg codeMsg, Integer storageId) {
super(codeMsg, storageId, null);
}
public DisableProxyDownloadException(CodeMsg codeMsg, Integer storageId, String message) {
super(codeMsg, storageId, message);
}
public DisableProxyDownloadException(CodeMsg codeMsg, Integer storageId, String message, Throwable cause) {
super(codeMsg, storageId, message, cause);
}
}

View File

@@ -0,0 +1,18 @@
package im.zhaojun.zfile.core.exception.file.operator;
import im.zhaojun.zfile.core.exception.StorageSourceException;
import im.zhaojun.zfile.core.util.CodeMsg;
import lombok.Getter;
/**
* 存储源文件操作异常
* @author zhaojun
*/
@Getter
public class StorageSourceFileOperatorException extends StorageSourceException {
public StorageSourceFileOperatorException(CodeMsg codeMsg, Integer storageId, String message, Throwable cause) {
super(codeMsg, storageId, message, cause);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
package im.zhaojun.zfile.core.exception.http;
/**
* Http 请求状态码异常 (返回状态码为 5xx 抛出此异常)
* @author zhaojun
*/
public class HttpResponseStatusErrorException extends RuntimeException {
public HttpResponseStatusErrorException(String message) {
super(message);
}
}

View File

@@ -1,11 +1,10 @@
package im.zhaojun.zfile.filter;
package im.zhaojun.zfile.core.filter;
import org.springframework.core.annotation.Order;
import cn.hutool.core.util.ObjectUtil;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
@@ -17,27 +16,27 @@ import java.io.IOException;
/**
* 开启跨域支持. 一般用于开发环境, 或前后端分离部署时开启.
*
* @author zhaojun
*/
@Order(1)
@WebFilter(value = "/*")
@Component
public class CorsFilter extends GenericFilterBean {
@WebFilter(urlPatterns = "/*")
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpServletRequest.getHeader(HttpHeaders.ORIGIN));
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Origin, X-Requested-With, Content-Type, Accept");
String header = httpServletRequest.getHeader(HttpHeaders.ORIGIN);
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, ObjectUtil.defaultIfNull(header, "*"));
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Origin, X-Requested-With, Content-Type, Accept, zfile-token, axios-request");
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "false");
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "600");
if (!CorsUtils.isPreFlightRequest(httpServletRequest)) {
chain.doFilter(httpServletRequest, httpServletResponse);
}
}
}
}

View File

@@ -0,0 +1,41 @@
package im.zhaojun.zfile.core.filter;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.extra.servlet.ServletUtil;
import im.zhaojun.zfile.core.constant.MdcConstant;
import org.slf4j.MDC;
import javax.servlet.Filter;
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;
/**
* @author zhaojun
*/
@WebFilter(urlPatterns = "/*")
public class MDCFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
MDC.put(MdcConstant.TRACE_ID, IdUtil.fastUUID());
MDC.put(MdcConstant.IP, ServletUtil.getClientIP(httpServletRequest));
MDC.put(MdcConstant.USER, StpUtil.isLogin() ? StpUtil.getLoginIdAsString() : "anonymous");
try {
filterChain.doFilter(httpServletRequest, httpServletResponse);
} finally {
MDC.clear();
}
}
}

View File

@@ -0,0 +1,22 @@
package im.zhaojun.zfile.core.httpclient;
import im.zhaojun.zfile.core.httpclient.logging.HttpLoggingInterceptor;
import okhttp3.OkHttpClient;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
/**
* 自建 OkHTTP3 客户端工厂类, 增加日志输出拦截器.
* @author zhaojun
*/
public class ZFileOkHttp3ClientHttpRequestFactory extends OkHttp3ClientHttpRequestFactory {
public ZFileOkHttp3ClientHttpRequestFactory() {
// 使用 OkHttp3 作为底层请求库, 并设置重试机制和日志拦截器
super(new OkHttpClient()
.newBuilder()
.addNetworkInterceptor(new HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEBUG)
.setLevel(HttpLoggingInterceptor.Level.HEADERS))
.build());
}
}

View File

@@ -0,0 +1,340 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.zhaojun.zfile.core.httpclient.logging;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Connection;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.http.HttpHeaders;
import okhttp3.internal.platform.Platform;
import okio.Buffer;
import okio.BufferedSource;
import okio.GzipSource;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import static okhttp3.internal.platform.Platform.INFO;
/**
* 此类代码来源于 logging-interceptor.
* <br>
* An OkHttp interceptor which logs request and response information. Can be applied as an
* {@linkplain OkHttpClient#interceptors() application interceptor} or as a {@linkplain
* OkHttpClient#networkInterceptors() network interceptor}. <p> The format of the logs created by
* this class should not be considered stable and may change slightly between releases. If you need
* a stable logging format, use your own interceptor.
* <br>
* @author zhaojun
*/
@Slf4j
public final class HttpLoggingInterceptor implements Interceptor {
private static final Charset UTF8 = StandardCharsets.UTF_8;
public enum Level {
/** No logs. */
NONE,
/**
* Logs request and response lines.
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1 (3-byte body)
*
* <-- 200 OK (22ms, 6-byte body)
* }</pre>
*/
BASIC,
/**
* Logs request and response lines and their respective headers.
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
* <-- END HTTP
* }</pre>
*/
HEADERS,
/**
* Logs request and response lines and their respective headers and bodies (if present).
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
*
* Hi?
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
*
* Hello!
* <-- END HTTP
* }</pre>
*/
BODY
}
public interface Logger {
void log(String message);
/** A {@link Logger} defaults output appropriate for the current platform. */
Logger DEFAULT = message -> Platform.get().log(INFO, message, null);
Logger DEBUG = log::debug;
Logger TRACE = log::trace;
}
public HttpLoggingInterceptor() {
this(Logger.DEFAULT);
}
public HttpLoggingInterceptor(Logger logger) {
this.logger = logger;
}
private final Logger logger;
private volatile Set<String> headersToRedact = Collections.emptySet();
public void redactHeader(String name) {
Set<String> newHeadersToRedact = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
newHeadersToRedact.addAll(headersToRedact);
newHeadersToRedact.add(name);
headersToRedact = newHeadersToRedact;
}
private volatile Level level = Level.NONE;
/** Change the level at which this interceptor logs. */
public HttpLoggingInterceptor setLevel(Level level) {
if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
this.level = level;
return this;
}
public Level getLevel() {
return level;
}
@Override public Response intercept(Chain chain) throws IOException {
Level level = this.level;
Request request = chain.request();
if (level == Level.NONE) {
return chain.proceed(request);
}
boolean logBody = level == Level.BODY;
boolean logHeaders = logBody || level == Level.HEADERS;
RequestBody requestBody = request.body();
boolean hasRequestBody = requestBody != null;
Connection connection = chain.connection();
String requestStartMessage = "--> "
+ request.method()
+ ' ' + request.url()
+ (connection != null ? " " + connection.protocol() : "");
if (!logHeaders && hasRequestBody) {
requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
}
logger.log(requestStartMessage);
if (logHeaders) {
if (hasRequestBody) {
// Request body headers are only present when installed as a network interceptor. Force
// them to be included (when available) so there values are known.
if (requestBody.contentType() != null) {
logger.log("Content-Type: " + requestBody.contentType());
}
if (requestBody.contentLength() != -1) {
logger.log("Content-Length: " + requestBody.contentLength());
}
}
Headers headers = request.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
String name = headers.name(i);
// Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
logHeader(headers, i);
}
}
if (!logBody || !hasRequestBody) {
logger.log("--> END " + request.method());
} else if (bodyHasUnknownEncoding(request.headers())) {
logger.log("--> END " + request.method() + " (encoded body omitted)");
} else if (requestBody.isDuplex()) {
logger.log("--> END " + request.method() + " (duplex request body omitted)");
} else {
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
logger.log("");
if (isPlaintext(buffer)) {
logger.log(buffer.readString(charset));
logger.log("--> END " + request.method()
+ " (" + requestBody.contentLength() + "-byte body)");
} else {
logger.log("--> END " + request.method() + " (binary "
+ requestBody.contentLength() + "-byte body omitted)");
}
}
}
long startNs = System.nanoTime();
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
logger.log("<-- HTTP FAILED: " + e);
throw e;
}
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
ResponseBody responseBody = response.body();
long contentLength = responseBody.contentLength();
String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
logger.log("<-- "
+ response.code()
+ (response.message().isEmpty() ? "" : ' ' + response.message())
+ ' ' + response.request().url()
+ " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')');
if (logHeaders) {
Headers headers = response.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
logHeader(headers, i);
}
if (!logBody || !HttpHeaders.hasBody(response)) {
logger.log("<-- END HTTP");
} else if (bodyHasUnknownEncoding(response.headers())) {
logger.log("<-- END HTTP (encoded body omitted)");
} else {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.getBuffer();
Long gzippedLength = null;
if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) {
gzippedLength = buffer.size();
try (GzipSource gzippedResponseBody = new GzipSource(buffer.clone())) {
buffer = new Buffer();
buffer.writeAll(gzippedResponseBody);
}
}
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
if (!isPlaintext(buffer)) {
logger.log("");
logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
return response;
}
if (contentLength != 0) {
logger.log("");
logger.log(buffer.clone().readString(charset));
}
if (gzippedLength != null) {
logger.log("<-- END HTTP (" + buffer.size() + "-byte, "
+ gzippedLength + "-gzipped-byte body)");
} else {
logger.log("<-- END HTTP (" + buffer.size() + "-byte body)");
}
}
}
return response;
}
private void logHeader(Headers headers, int i) {
String value = headersToRedact.contains(headers.name(i)) ? "██" : headers.value(i);
logger.log(headers.name(i) + ": " + value);
}
/**
* Returns true if the body in question probably contains human readable text. Uses a small sample
* of code points to detect unicode control characters commonly used in binary file signatures.
*/
static boolean isPlaintext(Buffer buffer) {
try {
Buffer prefix = new Buffer();
long byteCount = buffer.size() < 64 ? buffer.size() : 64;
buffer.copyTo(prefix, 0, byteCount);
for (int i = 0; i < 16; i++) {
if (prefix.exhausted()) {
break;
}
int codePoint = prefix.readUtf8CodePoint();
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false;
}
}
return true;
} catch (EOFException e) {
return false; // Truncated UTF-8 sequence.
}
}
private static boolean bodyHasUnknownEncoding(Headers headers) {
String contentEncoding = headers.get("Content-Encoding");
return contentEncoding != null
&& !contentEncoding.equalsIgnoreCase("identity")
&& !contentEncoding.equalsIgnoreCase("gzip");
}
}

View File

@@ -0,0 +1,24 @@
package im.zhaojun.zfile.core.model.request;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author zhaojun
*/
@Data
public class PageQueryRequest {
@ApiModelProperty(value="分页页数")
private Integer page = 1;
@ApiModelProperty(value="每页条数")
private Integer limit = 10;
@ApiModelProperty(value="排序字段")
private String orderBy = "create_date";
@ApiModelProperty(value="排序顺序")
private String orderDirection = "desc";
}

View File

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

View File

@@ -0,0 +1,27 @@
package im.zhaojun.zfile.core.util;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
/**
* Class & 反射相关工具类
*
* @author zhaojun
*/
public class ClassUtils {
/**
* 获取指定类的泛型类型, 只获取第一个泛型类型
*
* @param clazz
* 泛型类
*
* @return 泛型类型
*/
public static Class<?> getClassFirstGenericsParam(Class<?> clazz) {
Type genericSuperclass = clazz.getGenericSuperclass();
Type actualTypeArgument = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
return (Class<?>) actualTypeArgument;
}
}

View File

@@ -0,0 +1,76 @@
package im.zhaojun.zfile.core.util;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author zhaojun
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class CodeMsg {
/**
* 错误码
* <p>
* 均为 5 位数, 如 00000, 10100, 20105 等.
* <br>
* 第一位表示错误类型, 4 为用户请求输入错误, 5 为服务端处理错误, 6 为警告信息
* <br>
* 第二位到第三位为二级类型
* <br>
* 第四位到第五位为具体错误代码, 根据业务场景自行定义
* <p>
* 以上三种类型均不允许重复, 且都需保持递增.
*/
private String code;
/**
* 错误消息
*/
private String msg;
// 通用返回值
public static CodeMsg SUCCESS = new CodeMsg("00000", "success");
public static CodeMsg BAD_REQUEST = new CodeMsg("40000", "非法请求");
public static CodeMsg ERROR = new CodeMsg("50000", "服务端异常");
// -------------- 用户输入级错误 --------------
public static CodeMsg REQUIRED_PASSWORD = new CodeMsg("40100", "请输入密码");
public static CodeMsg PASSWORD_FAULT = new CodeMsg("40101", "密码输入错误");
public static CodeMsg STORAGE_SOURCE_NOT_FOUND = new CodeMsg("40102", "无效的或初始化失败的存储源");
public static CodeMsg STORAGE_SOURCE_FORBIDDEN = new CodeMsg("40103", "无权访问存储源");
public static CodeMsg STORAGE_SOURCE_FILE_FORBIDDEN = new CodeMsg("40104", "无权访问该目录");
public static CodeMsg STORAGE_SOURCE_ILLEGAL_OPERATION = new CodeMsg("40105", "非法操作");
// -------------- 服务端处理错误 --------------
// 初始化相关错误
public static CodeMsg STORAGE_SOURCE_INIT_FAIL = new CodeMsg("50100", "初始化存储源失败");
public static CodeMsg STORAGE_SOURCE_INIT_STORAGE_CONFIG_FAIL = new CodeMsg("50101", "初始化存储源参数失败");
public static CodeMsg STORAGE_SOURCE_INIT_STORAGE_PARAM_FIELD_FAIL = new CodeMsg("50102", "填充存储源字段失败");
// 文件操作相关错误
public static CodeMsg STORAGE_SOURCE_FILE_NEW_FOLDER_FAIL = new CodeMsg("50201", "新建文件夹失败");
public static CodeMsg STORAGE_SOURCE_FILE_DELETE_FAIL = new CodeMsg("50202", "删除失败");
public static CodeMsg STORAGE_SOURCE_FILE_RENAME_FAIL = new CodeMsg("50203", "重命名失败");
public static CodeMsg STORAGE_SOURCE_FILE_GET_UPLOAD_FAIL = new CodeMsg("50204", "获取上传链接失败");
public static CodeMsg STORAGE_SOURCE_FILE_PROXY_UPLOAD_FAIL = new CodeMsg("50205", "文件上传失败");
public static CodeMsg STORAGE_SOURCE_FILE_PROXY_DOWNLOAD_FAIL = new CodeMsg("50206", "文件下载失败");
public static CodeMsg STORAGE_SOURCE_FILE_GET_ITEM_FAIL = new CodeMsg("50207", "文件不存在或请求异常");
public static CodeMsg STORAGE_SOURCE_FILE_DISABLE_PROXY_DOWNLOAD = new CodeMsg("50208", "非法操作, 当前文件不支持此类下载方式");
}

View File

@@ -0,0 +1,82 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import java.lang.reflect.Field;
/**
* 枚举转换工具类
*
* @author zhaojun
*/
public class EnumConvertUtils {
/**
* 根据枚举 class 和值获取对应的枚举对象
*
* @param clazz
* 枚举类 Class
*
* @param value
* 枚举值
*
* @return 枚举对象
*/
public static Enum<?> convertStrToEnum(Class<?> clazz, Object value) {
if (!ClassUtil.isEnum(clazz)) {
return null;
}
Field[] fields = ReflectUtil.getFields(clazz);
for (Field field : fields) {
boolean jsonValuePresent = field.isAnnotationPresent(JsonValue.class);
boolean enumValuePresent = field.isAnnotationPresent(EnumValue.class);
if (jsonValuePresent || enumValuePresent) {
Object[] enumConstants = clazz.getEnumConstants();
for (Object enumObj : enumConstants) {
if (ObjectUtil.equal(value, ReflectUtil.getFieldValue(enumObj, field))) {
return (Enum<?>) enumObj;
}
}
}
}
return null;
}
/**
* 转换枚举对象为字符串, 如果枚举对象没有定义 JsonValue 注解, 则使用 EnumValue 注解的值
*
* @param enumObj
* 枚举对象
*
* @return 字符串
*/
public static String convertEnumToStr(Object enumObj) {
Class<?> clazz = enumObj.getClass();
if (!ClassUtil.isEnum(clazz)) {
return null;
}
Field[] fields = ReflectUtil.getFields(clazz);
for (Field field : fields) {
boolean jsonValuePresent = field.isAnnotationPresent(JsonValue.class);
boolean enumValuePresent = field.isAnnotationPresent(EnumValue.class);
if (jsonValuePresent || enumValuePresent) {
return Convert.toStr(ReflectUtil.getFieldValue(enumObj, field));
}
}
return null;
}
}

View File

@@ -1,8 +1,8 @@
package im.zhaojun.zfile.util;
package im.zhaojun.zfile.core.util;
import cn.hutool.core.comparator.CompareUtil;
import im.zhaojun.zfile.model.dto.FileItemDTO;
import im.zhaojun.zfile.model.enums.FileTypeEnum;
import im.zhaojun.zfile.module.storage.model.result.FileItemResult;
import im.zhaojun.zfile.module.storage.model.enums.FileTypeEnum;
import java.util.Comparator;
@@ -16,21 +16,31 @@ import java.util.Comparator;
*
* @author zhaojun
*/
public class FileComparator implements Comparator<FileItemDTO> {
public class FileComparator implements Comparator<FileItemResult> {
private String sortBy;
private String order;
public FileComparator() {
}
private String order;
public FileComparator(String sortBy, String order) {
this.sortBy = sortBy;
this.order = order;
}
/**
* 比较两个文件的大小
*
* @param o1
* 第一个文件
*
* @param o2
* 第二个文件
*
* @return 比较结果
*/
@Override
public int compare(FileItemDTO o1, FileItemDTO o2) {
public int compare(FileItemResult o1, FileItemResult o2) {
if (sortBy == null) {
sortBy = "name";
}
@@ -57,4 +67,5 @@ public class FileComparator implements Comparator<FileItemDTO> {
return 1;
}
}
}

View File

@@ -0,0 +1,63 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.io.File;
import java.nio.charset.StandardCharsets;
/**
* 将文件输出对象
*
* @author zhaojun
*/
@Slf4j
public class FileResponseUtil {
/**
* 文件下载,单线程,不支持断点续传
*
* @param file
* 文件对象
*
* @param fileName
* 要保存为的文件名
*
* @return 文件下载对象
*/
public static ResponseEntity<Resource> exportSingleThread(File file, String fileName) {
if (!file.exists()) {
ByteArrayResource byteArrayResource = new ByteArrayResource("文件不存在或异常,请联系管理员.".getBytes(StandardCharsets.UTF_8));
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(byteArrayResource);
}
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
HttpHeaders headers = new HttpHeaders();
if (StrUtil.isEmpty(fileName)) {
fileName = file.getName();
}
headers.setContentDispositionFormData("attachment", StringUtils.encodeAllIgnoreSlashes(fileName));
return ResponseEntity
.ok()
.headers(headers)
.contentLength(file.length())
.contentType(mediaType)
.body(new InputStreamResource(FileUtil.getInputStream(file)));
}
}

View File

@@ -1,32 +1,34 @@
package im.zhaojun.zfile.util;
package im.zhaojun.zfile.core.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 cn.hutool.core.util.StrUtil;
import im.zhaojun.zfile.core.constant.ZFileConstant;
import im.zhaojun.zfile.core.exception.PreviewException;
import im.zhaojun.zfile.core.exception.TextParseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
/**
* 网络相关工具
*
* @author zhaojun
*/
@Slf4j
public class HttpUtil {
/**
* 获取 URL 对应的文件内容
*
* @param url
* 文件 URL
* @return 文件 URL
*
* @return 文件内容
*/
public static String getTextContent(String url) {
RestTemplate restTemplate = SpringContextHolder.getBean("restTemplate");
long maxFileSize = 1024 * ZFileConstant.TEXT_MAX_FILE_SIZE_KB;
if (getRemoteFileSize(url) > maxFileSize) {
@@ -35,16 +37,22 @@ public class HttpUtil {
String result;
try {
result = restTemplate.getForObject(url, String.class);
result = cn.hutool.http.HttpUtil.get(url);
} catch (Exception e) {
throw new TextParseException("文件解析异常, 请求 url = " + url + ", 异常信息为 = " + e.getMessage());
throw new TextParseException(StrUtil.format("获取文件内容失败, URL: {}", url), e);
}
return result == null ? "" : result;
}
/**
* 获取远程文件大小
*
* @param url
* 文件 URL
*
* @return 文件大小
*/
public static Long getRemoteFileSize(String url) {
long size = 0;
@@ -60,4 +68,4 @@ public class HttpUtil {
return size;
}
}
}

View File

@@ -1,4 +1,4 @@
package im.zhaojun.zfile.util;
package im.zhaojun.zfile.core.util;
/*
NaturalOrderComparator.java -- Perform 'natural order' comparisons of strings in Java.
Copyright (C) 2003 by Pierre-Luc Paour <natorder@paour.com>
@@ -26,6 +26,8 @@ package im.zhaojun.zfile.util;
import java.util.Comparator;
/**
* windows 文件排序算法
*
* @author zhaojun
*/
public class NaturalOrderComparator implements Comparator<String> {
@@ -147,4 +149,4 @@ public class NaturalOrderComparator implements Comparator<String> {
return a.length() - b.length();
}
}
}

View File

@@ -0,0 +1,72 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.util.StrUtil;
import im.zhaojun.zfile.core.constant.ZFileConstant;
import java.nio.file.FileSystems;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* 规则表达式工具类
*
* @author zhaojun
*/
public class PatternMatcherUtils {
private static final Map<String, PathMatcher> PATH_MATCHER_MAP = new HashMap<>();
/**
* 兼容模式的 glob 表达式匹配.
* 默认的 glob 表达式是不支持以下情况的:<br>
* <ul>
* <li>pattern: /a/**</li>
* <li>test1: /a</li>
* <li>test2: /a/</li>
* <ul>
* <p>test1 和 test 2 均无法匹配这种情况, 此方法兼容了这种情况, 即对 test 内容后拼接 "/xx", 使其可以匹配上 pattern.
* <p><strong>注意:</strong>但此方法对包含文件名的情况无效, 仅支持 test 为 路径的情况.
*
* @param pattern
* glob 规则表达式
*
* @param test
* 匹配内容
*
* @return 是否匹配.
*/
public static boolean testCompatibilityGlobPattern(String pattern, String test) {
// 如果规则表达式最开始没有 /, 则兼容在最前方加上 /.
if (!StrUtil.startWith(pattern, ZFileConstant.PATH_SEPARATOR)) {
pattern = ZFileConstant.PATH_SEPARATOR + pattern;
}
// 兼容性处理.
test = StringUtils.concat(test, ZFileConstant.PATH_SEPARATOR);
if (StrUtil.endWith(pattern, "/**")) {
test += "xxx";
}
return testGlobPattern(pattern, test);
}
/**
* 测试密码规则表达式和文件路径是否匹配
*
* @param pattern
* glob 规则表达式
*
* @param test
* 测试字符串
*/
private static boolean testGlobPattern(String pattern, String test) {
// 从缓存取出 PathMatcher, 防止重复初始化
PathMatcher pathMatcher = PATH_MATCHER_MAP.getOrDefault(pattern, FileSystems.getDefault().getPathMatcher("glob:" + pattern));
PATH_MATCHER_MAP.put(pattern, pathMatcher);
return pathMatcher.matches(Paths.get(test)) || StrUtil.equals(pattern, test);
}
}

View File

@@ -0,0 +1,146 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 配置文件或模板中的占位符替换工具类
*
* @author zhaojun
*/
@Slf4j
public class PlaceholderUtils {
/**
* Prefix for system property placeholders: "${"
*/
public static final String PLACEHOLDER_PREFIX = "${";
/**
* Suffix for system property placeholders: "}"
*/
public static final String PLACEHOLDER_SUFFIX = "}";
/**
* 解析占位符, 将指定的占位符替换为指定的值. 变量值从 Spring 环境中获取, 如没取到, 则默认为空.
*
* 必须在 Spring 环境下使用, 否则会抛出异常.
*
*
* @param formatStr
* 模板字符串
*
* @return 替换后的字符串
*/
public static String resolvePlaceholdersBySpringProperties(String formatStr) {
String placeholderName = getFirstPlaceholderName(formatStr);
if (StrUtil.isEmpty(placeholderName)) {
return formatStr;
}
String propertyValue = SpringUtil.getProperty(placeholderName);
Map<String, String> map = new HashMap<>();
map.put(placeholderName, propertyValue);
return resolvePlaceholders(formatStr, map);
}
/**
* 解析占位符, 将指定的占位符替换为指定的值.
*
* @param formatStr
* 模板字符串
*
* @param parameter
* 参数列表
*
* @return 替换后的字符串
*/
public static String resolvePlaceholders(String formatStr, Map<String, String> parameter) {
if (parameter == null || parameter.isEmpty()) {
return formatStr;
}
StringBuffer buf = new StringBuffer(formatStr);
int startIndex = buf.indexOf(PLACEHOLDER_PREFIX);
while (startIndex != -1) {
int endIndex = buf.indexOf(PLACEHOLDER_SUFFIX, startIndex + PLACEHOLDER_PREFIX.length());
if (endIndex != -1) {
String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
int nextIndex = endIndex + PLACEHOLDER_SUFFIX.length();
try {
String propVal = parameter.get(placeholder);
if (propVal != null) {
buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal);
nextIndex = startIndex + propVal.length();
} else {
log.warn("Could not resolve placeholder '{}' in [{}] ", placeholder, formatStr);
}
} catch (Exception ex) {
log.error("Could not resolve placeholder '{}' in [{}]: ", placeholder, formatStr, ex);
}
startIndex = buf.indexOf(PLACEHOLDER_PREFIX, nextIndex);
} else {
startIndex = -1;
}
}
return buf.toString();
}
/**
* 获取模板字符串第一个占位符的名称, 如 "我的名字是: ${name}, 我的年龄是: ${age}", 返回 "name".
*
* @param formatStr
* 模板字符串
*
* @return 占位符名称
*/
public static String getFirstPlaceholderName(String formatStr) {
List<String> list = getPlaceholderNames(formatStr);
if (CollUtil.isNotEmpty(list)) {
return list.get(0);
}
return null;
}
/**
* 获取模板字符串第一个占位符的名称, 如 "我的名字是: ${name}, 我的年龄是: ${age}", 返回 ["name", "age].
*
* @param formatStr
* 模板字符串
*
* @return 占位符名称
*/
public static List<String> getPlaceholderNames(String formatStr) {
if (StrUtil.isEmpty(formatStr)) {
return Collections.emptyList();
}
List<String> placeholderNameList = new ArrayList<>();
StringBuffer buf = new StringBuffer(formatStr);
int startIndex = buf.indexOf(PLACEHOLDER_PREFIX);
while (startIndex != -1) {
int endIndex = buf.indexOf(PLACEHOLDER_SUFFIX, startIndex + PLACEHOLDER_PREFIX.length());
if (endIndex != -1) {
String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
int nextIndex = endIndex + PLACEHOLDER_SUFFIX.length();
startIndex = buf.indexOf(PLACEHOLDER_PREFIX, nextIndex);
placeholderNameList.add(placeholder);
} else {
startIndex = -1;
}
}
return placeholderNameList;
}
}

View File

@@ -0,0 +1,96 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import cn.hutool.extra.spring.SpringUtil;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 代理下载链接工具类
*
* @author zhaojun
*/
@Slf4j
public class ProxyDownloadUrlUtils {
private static SystemConfigService systemConfigService;
private static final String PROXY_DOWNLOAD_LINK_DELIMITER= ":";
/**
* 服务器代理下载 URL 有效期 (分钟).
*/
public static final Integer PROXY_DOWNLOAD_LINK_EFFECTIVE_SECOND = 1800;
public static String generatorSignature(Integer storageId, String pathAndName, Integer effectiveSecond) {
if (systemConfigService == null) {
systemConfigService = SpringUtil.getBean(SystemConfigService.class);
}
// 如果有效时间为空, 则设置 30 分钟过期
if (effectiveSecond == null || effectiveSecond < 1) {
effectiveSecond = PROXY_DOWNLOAD_LINK_EFFECTIVE_SECOND;
}
// 过期时间的秒数
long second = DateUtil.offsetSecond(DateUtil.date(), effectiveSecond).getTime();
String content = storageId + PROXY_DOWNLOAD_LINK_DELIMITER + pathAndName + PROXY_DOWNLOAD_LINK_DELIMITER + second;
String rsaHexKey = systemConfigService.getRsaHexKeyOrGenerate();
byte[] key = HexUtil.decodeHex(rsaHexKey);
//构建
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key);
//加密
return aes.encryptHex(content);
}
public static boolean validSignatureExpired(Integer expectedStorageId, String expectedPathAndName, String signature) {
if (systemConfigService == null) {
systemConfigService = SpringUtil.getBean(SystemConfigService.class);
}
String rsaHexKey = systemConfigService.getRsaHexKeyOrGenerate();
byte[] key = HexUtil.decodeHex(rsaHexKey);
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key);
long currentTimeMillis = System.currentTimeMillis();
String storageId = null;
String pathAndName = null;
String expiredSecond = null;
try {
//解密
String decryptStr = aes.decryptStr(signature);
List<String> split = StrUtil.split(decryptStr, PROXY_DOWNLOAD_LINK_DELIMITER);
storageId = split.get(0);
pathAndName = split.get(1);
expiredSecond = split.get(2);
// 校验存储源 ID 和文件路径及是否过期.
if (StrUtil.equals(storageId, Convert.toStr(expectedStorageId))
&& StrUtil.equals(StringUtils.concat(pathAndName), StringUtils.concat(expectedPathAndName))
&& currentTimeMillis < Convert.toLong(expiredSecond)) {
return true;
}
log.warn("校验链接已过期或不匹配, signature: {}, storageId={}, pathAndName={}, expiredSecond={}, now:={}", signature, storageId, pathAndName, expiredSecond, currentTimeMillis);
} catch (Exception e) {
log.error("校验签名链接异常, signature: {}, storageId={}, pathAndName={}, expiredSecond={}, now:={}", signature, storageId, pathAndName, expiredSecond, currentTimeMillis);
return false;
}
return false;
}
}

View File

@@ -0,0 +1,72 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
import java.util.function.Function;
/**
* 获取 Request 工具类
*
* @author zhaojun
*/
public class RequestHolder {
/**
* 获取 HttpServletRequest
*
* @return HttpServletRequest
*/
public static HttpServletRequest getRequest(){
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
/**
* 获取 HttpServletResponse
*
* @return HttpServletResponse
*/
public static HttpServletResponse getResponse(){
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
}
/**
* 向 response 写入文件流.
*
* @param function
* 文件输入流获取函数
*
* @param path
* 文件路径
*/
public static void writeFile(Function<String, InputStream> function, String path){
try (InputStream inputStream = function.apply(path)) {
HttpServletResponse response = RequestHolder.getResponse();
String fileName = FileUtil.getName(path);
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + StringUtils.encodeAllIgnoreSlashes(fileName));
response.setContentType(MediaType.APPLICATION_OCTET_STREAM.getType());
OutputStream outputStream = response.getOutputStream();
IoUtil.copy(inputStream, outputStream);
response.flushBuffer();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,51 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.util.NumberUtil;
/**
* 文件大小或带宽大小转可读单位
*
* @author zhaojun
*/
public class SizeToStrUtils {
/**
* 将文件大小转换为可读单位
*
* @param bytes
* 字节数
*
* @return 文件大小可读单位
*/
public static String bytesToSize(long bytes) {
if (bytes == 0) {
return "0";
}
double k = 1024;
String[] sizes = new String[]{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
double i = Math.floor(Math.log(bytes) / Math.log(k));
return NumberUtil.round(bytes / Math.pow(k, i), 3) + " " + sizes[(int) i];
}
/**
* 将带宽大小转换为可读单位
*
* @param bps
* 字节数
*
* @return 带宽大小可读单位
*/
public static String bpsToSize(long bps) {
if (bps == 0) {
return "0";
}
double k = 1000;
String[] sizes = new String[]{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
double i = Math.floor(Math.log(bps) / Math.log(k));
return NumberUtil.round(bps / Math.pow(k, i), 3) + " " + sizes[(int) i];
}
}

View File

@@ -0,0 +1,402 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.net.URLEncodeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.spring.SpringUtil;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import im.zhaojun.zfile.core.constant.ZFileConstant;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* 字符串相关工具类
*
* @author zhaojun
*/
public class StringUtils {
public static final char DELIMITER = '/';
public static final String DELIMITER_STR = "/";
public static final String HTTP_PROTOCOL = "http://";
public static final String HTTPS_PROTOCOL = "https://";
/**
* 移除 URL 中的前后的所有 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1/file1/', 返回 'folder1/file1'
* 如 path = '///folder1/file1//', 返回 'folder1/file1'
*/
public static String trimSlashes(String path) {
path = trimStartSlashes(path);
path = trimEndSlashes(path);
return path;
}
/**
* 移除 URL 中的第一个 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1/file1', 返回 'folder1/file1'
* 如 path = '/folder1/file1', 返回 'folder1/file1'
*
*/
public static String trimStartSlashes(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
while (path.startsWith(DELIMITER_STR)) {
path = path.substring(1);
}
return path;
}
/**
* 移除 URL 中的最后一个 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1/file1/', 返回 '/folder1/file1'
* 如 path = '/folder1/file1///', 返回 '/folder1/file1'
*/
public static String trimEndSlashes(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
while (path.endsWith(DELIMITER_STR)) {
path = path.substring(0, path.length() - 1);
}
return path;
}
/**
* 去除路径中所有重复的 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1//file1/', 返回 '/folder1/file1/'
* 如 path = '/folder1////file1///', 返回 '/folder1/file1/'
*/
public static String removeDuplicateSlashes(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
StringBuilder sb = new StringBuilder();
// 是否包含 http 或 https 协议信息
boolean containProtocol = StrUtil.containsAnyIgnoreCase(path, HTTP_PROTOCOL, HTTPS_PROTOCOL);
if (containProtocol) {
path = trimStartSlashes(path);
}
// 是否包含 http 协议信息
boolean startWithHttpProtocol = StrUtil.startWithIgnoreCase(path, HTTP_PROTOCOL);
// 是否包含 https 协议信息
boolean startWithHttpsProtocol = StrUtil.startWithIgnoreCase(path, HTTPS_PROTOCOL);
if (startWithHttpProtocol) {
sb.append(HTTP_PROTOCOL);
} else if (startWithHttpsProtocol) {
sb.append(HTTPS_PROTOCOL);
}
for (int i = sb.length(); i < path.length() - 1; i++) {
char current = path.charAt(i);
char next = path.charAt(i + 1);
if (!(current == DELIMITER && next == DELIMITER)) {
sb.append(current);
}
}
sb.append(path.charAt(path.length() - 1));
return sb.toString();
}
/**
* 去除路径中所有重复的 '/', 并且去除开头的 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1//file1/', 返回 'folder1/file1/'
* 如 path = '///folder1////file1///', 返回 'folder1/file1/'
*/
public static String removeDuplicateSlashesAndTrimStart(String path) {
path = removeDuplicateSlashes(path);
path = trimStartSlashes(path);
return path;
}
/**
* 去除路径中所有重复的 '/', 并且去除结尾的 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1//file1/', 返回 '/folder1/file1'
* 如 path = '///folder1////file1///', 返回 '/folder1/file1'
*/
public static String removeDuplicateSlashesAndTrimEnd(String path) {
path = removeDuplicateSlashes(path);
path = trimEndSlashes(path);
return path;
}
/**
* 拼接 URL并去除重复的分隔符 '/',并去除开头的 '/', 但不会影响 http:// 和 https:// 这种头部.
*
* @param strs
* 拼接的字符数组
*
* @return 拼接结果
*/
public static String concatTrimStartSlashes(String... strs) {
return trimStartSlashes(concat(strs));
}
/**
* 拼接 URL并去除重复的分隔符 '/',并去除结尾的 '/', 但不会影响 http:// 和 https:// 这种头部.
*
* @param strs
* 拼接的字符数组
*
* @return 拼接结果
*/
public static String concatTrimEndSlashes(String... strs) {
return trimEndSlashes(concat(strs));
}
/**
* 拼接 URL并去除重复的分隔符 '/',并去除开头和结尾的 '/', 但不会影响 http:// 和 https:// 这种头部.
*
* @param strs
* 拼接的字符数组
*
* @return 拼接结果
*/
public static String concatTrimSlashes(String... strs) {
return trimSlashes(concat(strs));
}
/**
* 拼接 URL并去除重复的分隔符 '/',但不会影响 http:// 和 https:// 这种头部.
*
* @param strs
* 拼接的字符数组
*
* @return 拼接结果
*/
public static String concat(String... strs) {
StringBuilder sb = new StringBuilder(DELIMITER_STR);
for (int i = 0; i < strs.length; i++) {
String str = strs[i];
if (StrUtil.isEmpty(str)) {
continue;
}
sb.append(str);
if (i != strs.length - 1) {
sb.append(DELIMITER);
}
}
return removeDuplicateSlashes(sb.toString());
}
/**
* 拼接 URL并去除重复的分隔符 '/',但不会影响 http:// 和 https:// 这种头部.
*
* @param encodeAllIgnoreSlashes
* 是否 encode 编码 (忽略 /)
*
* @param strs
* 拼接的字符数组
*
* @return 拼接结果
*/
public static String concat(boolean encodeAllIgnoreSlashes, String... strs) {
StringBuilder sb = new StringBuilder(DELIMITER_STR);
for (int i = 0; i < strs.length; i++) {
String str = strs[i];
if (StrUtil.isEmpty(str)) {
continue;
}
sb.append(str);
if (i != strs.length - 1) {
sb.append(DELIMITER);
}
}
if (encodeAllIgnoreSlashes) {
return encodeAllIgnoreSlashes(removeDuplicateSlashes(sb.toString()));
} else {
return removeDuplicateSlashes(sb.toString());
}
}
/**
* 拼接文件直链生成 URL
*
* @param storageKey
* 存储源 ID
*
* @param fullPath
* 文件全路径
*
* @return 生成结果
*/
public static String generatorPathLink(String storageKey, String fullPath) {
SystemConfigService systemConfigService = SpringUtil.getBean(SystemConfigService.class);
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
String domain = systemConfig.getDomain();
String directLinkPrefix = systemConfig.getDirectLinkPrefix();
return concat(domain, directLinkPrefix, storageKey, encodeAllIgnoreSlashes(fullPath));
}
/**
* 替换 URL 中的 Host 部分,如替换 http://a.com/1.txt 为 https://abc.com/1.txt
*
* @param originUrl
* 原 URL
*
* @param replaceHost
* 替换的 HOST
*
* @return 替换后的 URL
*/
public static String replaceHost(String originUrl, String replaceHost) {
try {
String path = new URL(originUrl).getFile();
return concat(replaceHost, path);
} catch (MalformedURLException e) {
e.printStackTrace();
}
return null;
}
/**
* 编码 URL默认使用 UTF-8 编码
* URL 的 Fragment URLEncoder
* 默认的编码器针对Fragment定义如下
*
* <pre>
* fragment = *( pchar / "/" / "?" )
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* sub-delims = "!" / "$" / "&amp;" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
* </pre>
*
* 具体见https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
*
* @param url
* 被编码内容
*
* @return 编码后的字符
*/
public static String encode(String url) {
return URLEncodeUtil.encodeFragment(url);
}
/**
* 编码全部字符
*
* @param str
* 被编码内容
*
* @return 编码后的字符
*/
public static String encodeAllIgnoreSlashes(String str) {
if (StrUtil.isEmpty(str)) {
return str;
}
StringBuilder sb = new StringBuilder();
int prevIndex = -1;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == ZFileConstant.PATH_SEPARATOR_CHAR) {
if (prevIndex < i) {
String substring = str.substring(prevIndex + 1, i);
sb.append(URLEncodeUtil.encodeAll(substring));
prevIndex = i;
}
sb.append(c);
}
if (i == str.length() - 1 && prevIndex < i) {
String substring = str.substring(prevIndex + 1, i + 1);
sb.append(URLEncodeUtil.encodeAll(substring));
}
}
return sb.toString();
}
/**
* 解码 URL, 默认使用 UTF8 编码. 不会将 + 转为空格.
*
* @param url
* 被解码内容
*
* @return 解码后的内容
*/
public static String decode(String url) {
return URLUtil.decode(url, StandardCharsets.UTF_8, false);
}
/**
* 获取路径的上级目录, 如最后为 /, 则也会认为是一级目录
*
* @param path
* 文件路径
*
* @return 父级目录
*/
public static String getParentPath(String path) {
int toIndex = StrUtil.lastIndexOfIgnoreCase(path, ZFileConstant.PATH_SEPARATOR);
if (toIndex <= 0) {
return "/";
} else {
return StrUtil.sub(path, 0, toIndex);
}
}
public static String removeAllLineBreaksAndTrim(String str) {
String removeResult = StrUtil.removeAllLineBreaks(str);
return StrUtil.trim(removeResult);
}
}

View File

@@ -0,0 +1,34 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.util.StrUtil;
/**
* url 相关工具类
*
* @author zhaojun
*/
public class UrlUtils {
/**
* 给 url 拼接参数
*
* @param url
* 原始 URL
*
* @param name
* 参数名称
*
* @param value
* 参数值
*
* @return 拼接后的 URL
*/
public static String concatQueryParam(String url, String name, String value) {
if (StrUtil.contains(url, "?")) {
return url + "&" + name + "=" + value;
} else {
return url + "?" + name + "=" + value;
}
}
}

View File

@@ -0,0 +1,31 @@
package im.zhaojun.zfile.core.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 字符串列表值校验注解
*
* @author zhaojun
*/
@Documented
@Constraint(validatedBy = { StringListValueConstraintValidator.class })
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface StringListValue {
String message() default "";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
String[] vals() default { };
}

View File

@@ -0,0 +1,53 @@
package im.zhaojun.zfile.core.validation;
import cn.hutool.core.util.StrUtil;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 字符串列表值校验器
*
* @author zhaojun
*/
public class StringListValueConstraintValidator implements ConstraintValidator<StringListValue, String> {
private final Set<String> set = new HashSet<>();
/**
* 初始化方法
*
* @param constraintAnnotation
* 校验注解对象
*/
@Override
public void initialize(StringListValue constraintAnnotation) {
String[] vals = constraintAnnotation.vals();
set.addAll(Arrays.asList(vals));
}
/**
* 判断是否校验成功
*
* @param value
* 需要校验的值
*
* @param context
* 校验上下文
*
* @return 是否校验成功
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StrUtil.isEmpty(value)) {
return true;
}
return set.contains(value);
}
}

View File

@@ -1,135 +0,0 @@
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.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
*/
@ControllerAdvice
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() {
return ResultBean.error("文件不存在");
}
/**
* 捕获 ClientAbortException 异常, 不做任何处理, 防止出现大量堆栈日志输出, 此异常不影响功能.
*/
@ExceptionHandler({HttpMediaTypeNotAcceptableException.class, ClientAbortException.class})
@ResponseBody
@ResponseStatus
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());
}
/**
* 文件预览异常
*/
@ExceptionHandler({PreviewException.class})
@ResponseBody
@ResponseStatus
public ResultBean previewException(PreviewException ex) {
return ResultBean.error(ex.getMessage());
}
/**
* 初始化异常
*/
@ExceptionHandler({InitializeDriveException.class})
@ResponseBody
@ResponseStatus
public ResultBean initializeException(InitializeDriveException ex) {
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
public ResultBean extraExceptionHandler(Exception e) {
log.error(e.getMessage(), e);
if (e.getClass() == Exception.class) {
return ResultBean.error("系统异常, 请联系管理员");
} else {
return ResultBean.error(e.getMessage());
}
}
}

View File

@@ -1,29 +0,0 @@
package im.zhaojun.zfile.exception;
/**
* 对象存储初始化异常
* @author zhaojun
*/
public class InitializeDriveException extends RuntimeException {
private static final long serialVersionUID = -1920550904063819880L;
public InitializeDriveException() {
}
public InitializeDriveException(String message) {
super(message);
}
public InitializeDriveException(String message, Throwable cause) {
super(message, cause);
}
public InitializeDriveException(Throwable cause) {
super(cause);
}
public InitializeDriveException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

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