Compare commits

...
131 Commits
Author SHA1 Message Date
lishuang 8b8265d77f feat: replace front. 2026-02-14 23:56:36 +08:00
seaheart ceecc95c29 upgrade version 2026-02-14 23:18:57 +08:00
lishuang 27c48446ce feat: add auto clear feature. ready to release 4.2.0 2026-02-14 23:04:59 +08:00
seaheart 1d65c0e8d6 Support chunk upload and breakpoint resume feature 2026-02-08 16:31:56 +08:00
lishuang 6b09c1ffac feat: find service back. 2025-09-22 22:37:40 +08:00
lishuang 07d8e7e0b1 feat: show error log. 2025-09-22 22:12:50 +08:00
lishuang e80e41266a feat: show error log. 2025-09-22 22:05:57 +08:00
lishuang 640d543487 feat: show error log. 2025-09-22 21:59:06 +08:00
zicla aef3cee03c chmod x 2025-09-22 21:48:23 +08:00
lishuang a9d843d885 feat: upgrade to 4.1.3 2025-09-22 21:46:25 +08:00
lishuang bcc9d19916 feat: upgrade to 4.1.3 2025-09-21 23:27:38 +08:00
seaheart 23d2d40613 fix: space dir download logic 2025-09-14 23:10:53 +08:00
lishuang f873a4aeb9 feat: upgrade to 4.1.2 2025-02-24 23:01:44 +08:00
eyeblue a49495268f Merge pull request #273 from eyebluecn/fix-batch-download
fix: space files from different space member download batch error
2025-02-24 22:58:15 +08:00
seaheart 389b1bdd2e fix: batch download 2025-02-23 17:14:55 +08:00
eyeblue 09c903ec2d Merge pull request #265 from eyebluecn/fix-space-matter-preview
fix: space matter preview token logic
2024-10-06 21:54:56 +08:00
yinyuchen b4c0455f70 fix: space matter preview token logic 2024-10-06 20:51:06 +08:00
lishuang 6f59fe2b37 feat: update golang version in docker. 2024-07-28 01:17:10 +08:00
lishuang 365853fade feat: upgrade 4.1.0 2024-07-27 23:54:12 +08:00
lishuang 837ce742e0 feat: add changelog 2024-07-27 23:50:43 +08:00
yinyuchen 11bf29fad7 feat: upgrade frontend static 2024-07-27 23:48:19 +08:00
lishuang 350e898bda fix: sort bug. 2024-07-27 22:48:24 +08:00
lishuang 3c29788980 feat: add default sort for matter search. 2024-07-27 21:43:36 +08:00
lishuang 09ff58362b feat: add the api search api. 2024-07-27 17:51:05 +08:00
lishuang 7cb6c148dd fix: soft delete bug. 2024-07-27 17:21:30 +08:00
lishuang e8cca6ea15 feat: update the go mod. 2024-07-25 19:44:30 +08:00
lishuang a067234c64 feat: finish api for search dfs. 2024-07-21 23:15:32 +08:00
lishuang b74463d9e5 feat: search dfs. 2024-07-20 16:09:26 +08:00
lishuang 28f85b4d45 fix: fix issue #232 2024-07-20 14:56:01 +08:00
lishuang e8567524ac fix: fix issue #222 2024-07-20 14:43:14 +08:00
lishuang c0dcee45d4 fix: bug for /api/user/edit 2024-07-20 13:00:54 +08:00
lishuang d51343ba8d feat: refine api for /api/space/member/create 2024-07-20 12:47:30 +08:00
yinyuchen 777a1961d0 update dingding qrcode 2024-06-26 22:54:54 +08:00
lishuang 706cb057bc feat: upgrade to tank-4.0.4 2024-05-05 20:39:43 +08:00
eyeblue 31c0356acc Merge pull request #231 from kuww/master
完善预览视频无法播放的问题
2024-05-05 19:42:31 +08:00
yinyuchen 47de9769d0 fix: share overview problem 2024-04-29 23:00:34 +08:00
lishuang ebb7ad0207 feat: upgrade to 4.0.3 2024-03-26 23:38:59 +08:00
yinyuchen 750c0983f1 chore: upgrade version 2024-03-26 23:28:29 +08:00
yinyuchen f005466293 fix: frontend dir problem 2024-03-26 23:24:07 +08:00
seaheart aacf9e05b9 add static 2023-11-12 15:39:04 +08:00
lishuang 62fba81406 feat: use go 1.18 in docker. 2023-11-10 00:24:36 +08:00
lishuang c644757135 feat: release 4.0.2 2023-11-10 00:06:02 +08:00
lishuang 53a0dcce2f feat: add the privacy false default for dir. 2023-11-09 23:48:41 +08:00
seaheart b1a948dd2a change static file 2023-11-09 23:47:41 +08:00
kuww e19fdb2ad2 完善预览视频无法播放的问题 2023-11-02 16:46:12 +08:00
lishuang b9e2f2cf48 feat: upgrade t tank-4.0.1 2023-09-09 11:56:05 +08:00
lishuang 2de7e6b2d0 feat: fix the scan bug. 2023-09-09 11:47:48 +08:00
eyeblue a2716ed8b8 Merge pull request #224 from gmugu/master
Fix "Scan Disk" abnormality
2023-09-09 11:43:39 +08:00
lishuang 73d60a0907 feat: update the dingding image. 2023-09-07 00:41:25 +08:00
lishuang 15c26a6e1f feat: update the dingding image. 2023-09-07 00:40:55 +08:00
lishuang 09ce9b811e feat: update the dingding image. 2023-09-07 00:12:06 +08:00
MuGu 05737663a2 Fix the user_uuid error in scanning disk local space. 2023-09-02 17:17:26 +08:00
MuGu 9820a1e1f3 1、Fix "Scan Disk" user_uuid error that causes WebDAV unable to display scan list.
2、Fix the problem of abnormality when scanning non-existent folders.
2023-09-01 18:46:20 +08:00
lishuang f4965c8256 feat: add the shared space feature. 2023-08-13 16:14:56 +08:00
lishuang 4892083ee9 feat: upgrade the doc img. 2023-08-13 16:13:47 +08:00
lishuang 8f8295cb68 build: version 4.0.0 2023-08-13 12:31:23 +08:00
lishuang 0be4513349 feat: upgrade frontend res to 4.0.0 2023-08-13 12:26:57 +08:00
lishuang c7219a7516 feat: add the query for space's name. 2023-08-13 01:45:19 +08:00
lishuang f5704f470a feat: fix the create user bug for admin. 2023-08-12 16:56:32 +08:00
lishuang 9b19fd1e9d feat: fix some bugs. 2023-08-12 16:46:36 +08:00
lishuang 3e81d886f6 feat: change the scan. 2023-08-12 16:27:38 +08:00
lishuang f1d31d1c10 feat: fix the bug for size compute. 2023-08-06 22:46:25 +08:00
lishuang 44b811507d feat: fix the matter author bug. 2023-08-06 22:31:42 +08:00
lishuang 3c28140c32 feat: release 4.0.0.beta2 2023-08-06 16:19:32 +08:00
lishuang a12148671a feat: fix the bug for sqlite. 2023-08-05 01:19:50 +08:00
lishuang 3a9199e176 feat: add the change log. 2023-07-22 21:08:49 +08:00
lishuang 3d5c444db0 build: build version 4.0.0.beta0 2023-07-22 20:35:47 +08:00
lishuang 6c385014cc feat: append space. 2023-07-22 20:30:52 +08:00
lishuang f0a1c0ab06 feat: fix the role in space memeber. 2023-07-22 20:05:25 +08:00
lishuang 4ebf15f3c1 fix: fix the auth bug. 2023-07-22 19:41:02 +08:00
lishuang b325a3e9a0 fix: fix the bug of download token. 2023-07-22 19:24:00 +08:00
lishuang c3dd34dac9 fix: make the puuid optional. 2023-07-22 18:13:21 +08:00
lishuang 3eceb3c719 fix: fix the bug for file size. 2023-07-22 18:09:25 +08:00
lishuang f1cbde414f feat: enhance the mine api. 2023-07-22 17:16:55 +08:00
lishuang 9bc02bdfcb build: build the new beta version. 2023-07-22 16:21:31 +08:00
lishuang e34a26c009 fix: fix the status code hint bug. 2023-07-22 15:45:07 +08:00
lishuang 3fc032daa9 fix: fix bug. 2023-07-22 15:40:49 +08:00
lishuang 006554cd0a fix: fix the jsoniter fatal bug. 2023-07-22 15:39:53 +08:00
lishuang 42468e9711 feat: add root dir support. 2023-07-22 15:35:45 +08:00
lishuang da5b04cdb3 feat: add a new api. add puuid for crawl. 2023-07-22 15:11:36 +08:00
lishuang 66486b8810 feat: upgrade the version for jsoniter. 2023-07-22 12:06:48 +08:00
lishuang c9883cafb9 feat: change the zip auth. 2023-07-12 00:30:21 +08:00
lishuang a83b5fa489 feat: the frontend res. 2023-07-08 20:58:32 +08:00
lishuang 49f44a00b2 feat: use version 4.0.0.beta1 2023-07-08 19:18:30 +08:00
lishuang 807a860d07 feat: simplify the index name. 2023-07-08 18:39:15 +08:00
lishuang 3df445e98c feat: upgrade to 4.0.0.beta 2023-07-08 17:54:19 +08:00
lishuang e53abb7c79 feat: add the share matter preview and download. 2023-07-08 17:34:57 +08:00
lishuang 4ca8bc06d8 fix: refine the share's things. 2023-07-08 15:25:39 +08:00
lishuang 76d4752776 fix: fix the share space uuid. 2023-07-08 15:21:05 +08:00
lishuang 95fcaada2b fix: separate the share's matter page. 2023-07-08 14:55:06 +08:00
lishuang 327af9f3fa fix: add space into user info. 2023-07-08 14:11:42 +08:00
lishuang 066ae11ad7 fix: add the space info into user's struct. 2023-07-08 14:09:31 +08:00
lishuang f5c377dab8 fix: fix the bug for moving. 2023-06-30 00:22:06 +08:00
lishuang 963b7fdb82 fix: fix the bugs for space file upload. 2023-06-27 00:03:49 +08:00
lishuang 8f66b5e04b feat: add the space page for private. 2023-06-25 22:29:45 +08:00
lishuang 0b98c6acc0 Merge remote-tracking branch 'origin/master' into v3.2.0 2023-06-25 22:07:57 +08:00
seaheart dfae93bbed change dingding qrcode 2023-06-11 15:48:52 +08:00
lishuang 1a440e1f67 feat: finish the basic space things. 2023-05-27 16:21:35 +08:00
lishuang 270e2253a4 Merge remote-tracking branch 'origin/v3.2.0' into v3.2.0_tmp
# Conflicts:
#	code/rest/space_member_controller.go
2023-05-27 15:45:12 +08:00
lishuang a9a3b524eb feat: finish the basic file operation. 2023-05-27 15:43:38 +08:00
seaheart e4799f0045 change space member create and edit key 2023-05-27 15:28:29 +08:00
lishuang 9e6d2c1043 feat: remove the space props in the user model. 2023-05-27 14:12:35 +08:00
lishuang 691fb2a380 feat: fix the spacemember things. 2023-05-27 12:34:58 +08:00
lishuang f39bf33922 feat: finish the space CRUD. 2023-05-27 12:25:24 +08:00
lishuang 14ff3fea9a feat: ready to refine the space model. 2023-05-13 22:07:33 +08:00
lishuang 246829869a feat: add spaceUuid for matterModel. 2023-05-13 21:19:11 +08:00
lishuang 5f2fa2f033 feat: finish the edit for space and space member. 2023-05-13 19:12:21 +08:00
lishuang 6c52e87396 feat: finish the space delete and space member delete api. 2023-05-13 19:01:20 +08:00
lishuang a4b0bb3e9c feat: finish the permission query for normal user. 2023-05-13 18:29:13 +08:00
lishuang 44b79ab946 feat: upgrade the dependencies. 2023-05-13 18:00:02 +08:00
lishuang 1a39ed726d feat: upgrade the gorm. 2023-05-13 17:33:24 +08:00
lishuang 4c4fa9b2c7 Merge remote-tracking branch 'origin/dependabot/go_modules/golang.org/x/crypto-0.1.0' into v3.2.0
# Conflicts:
#	go.mod
#	go.sum
2023-05-13 17:29:21 +08:00
dependabot[bot] 708110c498 Bump golang.org/x/crypto from 0.0.0-20200709230013-948cd5f35899 to 0.1.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.0.0-20200709230013-948cd5f35899 to 0.1.0.
- [Commits](https://github.com/golang/crypto/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-13 09:28:47 +00:00
lishuang 227efb5471 feat: refine the space member codes. 2023-05-13 17:27:28 +08:00
lishuang 17cbb4b663 Merge remote-tracking branch 'origin/dependabot/go_modules/golang.org/x/text-0.3.8' into v3.2.0 2023-05-13 17:26:41 +08:00
dependabot[bot] 35ecc4c894 Bump golang.org/x/text from 0.3.3 to 0.3.8
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.3 to 0.3.8.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.3...v0.3.8)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-13 09:25:45 +00:00
lishuang d976abdb84 feat: add the member page api. 2023-05-13 17:18:01 +08:00
lishuang bd78b87d6f feat: add some space member api. 2023-05-13 16:46:18 +08:00
lishuang a0869708aa feat: add the spaceMember operations. 2023-05-13 16:12:37 +08:00
lishuang bf53df1dc3 feat: finish the space create and page feature. 2023-05-13 15:45:07 +08:00
lishuang 6f63e1c837 feat: fix the win detect for dev. 2023-05-13 14:49:20 +08:00
lishuang 3e2234c1cd feat: add the space and spaceMember module. 2023-05-13 14:35:33 +08:00
lishuang 42208ac101 feat: prepare work for v3.2.0 2023-05-13 14:07:18 +08:00
lishuang b247364d16 revert: revert the change for v3.2.0 2023-05-13 11:43:59 +08:00
eyeblue d8b86bd302 Update README.md 2023-04-13 02:08:49 +08:00
simba 1ec66adda0 change dingtalk qrcode 2022-06-30 23:28:44 +08:00
simba d3d0ee73a0 stash file 2022-05-12 18:06:08 +08:00
simba e757b471fd stash file 2022-05-12 18:05:57 +08:00
Simba 60c8843734 Merge pull request #175 from eyebluecn/v3.1.6
V3.1.6
2022-05-01 18:01:32 +08:00
Simba 6626afa6b6 Merge pull request #165 from eyebluecn/fix/build.sh
fix: throw build.sh error
2022-05-01 10:31:04 +08:00
simba 6d35063027 fix: throw build.sh error 2022-04-29 16:32:14 +08:00
97 changed files with 4619 additions and 1068 deletions
+2
View File
@@ -8,3 +8,5 @@ tmp
tank
tank.exe
# mac
.DS_Store
+74 -2
View File
@@ -9,10 +9,13 @@
2. build/pack/build.sh
VERSION_NAME=tank-x.x.x
3. core/config.go
3. build/pack/cross-build.sh
VERSION_NAME=tank-x.x.x
4. core/config.go
VERSION = x.x.x
4. readme.md 修改 eyeblue/tank:x.x.x
5. readme.md 修改 eyeblue/tank:x.x.x
eyeblue/tank:x.x.x
@@ -51,6 +54,75 @@ c. 在github上发布新版本。
go get xxx/xxx
go mod tidy
tank-4.1.1 2024-10-6
1. 修复已知问题
tank-4.1.0 2024-7-27
1. 删除用户后,空间一并删除。 #222
2. 增加批量绑定空间成员功能。 #227
3. 共享空间增加回收站逻辑。 #232
4. 修复用户修改空间单位问题。 #233 #253
5. 增强搜索功能,会进行dfs深度优先搜索。 #239
1. When delete user, it's space will be deleted #222
2. Batch creating space member #227
3. Add feature of recycle bin for space #232
4. Fix issue of changing unit #233 #253
5. Enhance the search ability, using dfs #239
tank-4.0.4 2024-4-29
1. 修复已知问题
tank-4.0.3 2024-3-26
1. 修复已知问题
tank-4.0.2 2023-11-10
1. 修复BUG https://github.com/eyebluecn/tank/issues/229
2. 新增了分享提取码快速提取
3. 分享文件默认按照创建时间倒序排列。
1. Fix BUG https://github.com/eyebluecn/tank/issues/229
2. Fast share using code.
3. Share list order by create time desc.
tank-4.0.1 2023-09-09
1.修复一些文件扫描的bug https://github.com/eyebluecn/tank/issues/221 https://github.com/eyebluecn/tank/issues/220
1.Fix scan bug. https://github.com/eyebluecn/tank/issues/221 https://github.com/eyebluecn/tank/issues/220
tank-4.0.0 2023-08-13
1.增加空间概念,可以多人共同维护一个共享空间中的文件 #201
2.新增粘贴上传文件功能 #188
3.文件大小单位选择体验优化 #179
4.支持使用url的方式抓取文件
5.一些UI优化
1.Add space, supports multi users maintaining a shared space. #201
2.Add the feature of uploading by pasting files. #188
3.Human readable file size unit. #179
4.Add the feature of uploading by url.
6.Some user experience enhancement.
tank-4.0.0.beta1 2023-07-22
1. Shared space
2. Copy and paste upload
3. Crawl upload
4. Space size selection control upgrade
5. Some style modifications
1. 共享空间
2. 文件复制粘贴上传
3. 文件抓取上传
4. 空间大小选择控件升级
5. 一些样式修改
tank-3.1.5 2022-03-21
1. Support Sqlite.
+2 -2
View File
@@ -1,5 +1,5 @@
# use golang 1.17
FROM golang:1.17
# use golang 1.22.5
FROM golang:1.22.5
# maintainer. Author's name and email.
MAINTAINER eyeblue "eyebluecn@126.com"
+17 -5
View File
@@ -2,12 +2,13 @@
[English Version](https://tank-doc.eyeblue.cn/en)
# 蓝眼云盘(3.1.6)
# 蓝眼云盘(4.2.0)
[在线Demo](https://tanker.eyeblue.cn) (体验账号: demo 密码:123456)
蓝眼云盘是蓝眼开源系列代表作品之一,致力于打造精致,优雅,简约的云盘。核心功能如下:
1. 文件列表
2. 文件/文件夹上传 + 拖拽上传
2. 文件/文件夹上传 + 拖拽上传 + 分片/断点续传
3. 共享空间
3. 文件分享
4. 文件监控统计
5. 回收站
@@ -17,6 +18,7 @@
9. [在线预览及自定义配置预览引擎](https://tank-doc.eyeblue.cn/advance/preview.html)
10. 支持Sqlite和Mysql数据库用以存储文件元信息,Sqlite可以做到开箱即用。
[更多内容请移步至文档](https://tank-doc.eyeblue.cn/)
### 软件截图
@@ -36,17 +38,27 @@
## [安装文档](https://tank-doc.eyeblue.cn/basic/install.html)
### 快速运行
安装依赖
```shell
go mod tidy
```
运行main文件即可:
```shell
./main.go
```
### Contribution
感谢所有蓝眼云盘的贡献者 [@zicla](https://github.com/zicla),[@seaheart](https://github.com/seaheart),[@yemuhe](https://github.com/yemuhe),[@hxsherry](https://github.com/hxsherry)
如果您也想参与进来,请尽情的fork, star, post issue, pull requests
当然你可以加入钉钉群一起直接交流,在钉钉群中可以获取最新beta版本。
当然你可以加入钉钉群(一群已满员)一起直接交流,在钉钉群中可以获取最新beta版本。
![](./build/doc/img/dingding.jpg)
![](./build/doc/img/dingding.jpeg)
群号:23156361
群号:44754005
### Support
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

+21 -20
View File
@@ -1,28 +1,29 @@
{
"files": {
"main.css": "/static/css/main.b03c51fb.css",
"main.js": "/static/js/main.b94b1947.js",
"main.css": "/static/css/main.3e2e293d.css",
"main.js": "/static/js/main.1a935cc6.js",
"static/js/833.2731ecbb.chunk.js": "/static/js/833.2731ecbb.chunk.js",
"static/media/logo.png": "/static/media/logo.847e54ef7fb4b744fad4.png",
"static/media/empty.svg": "/static/media/empty.20ceb38d310075aa4c3cdaae210afb65.svg",
"static/media/psd.svg": "/static/media/psd.bf2ac5411c0132f292787f45855934f2.svg",
"static/media/xls.svg": "/static/media/xls.02895077484083ddc5346ab720cd8d1b.svg",
"static/media/ppt.svg": "/static/media/ppt.7dc0cfc7fdf0e6f66fb94c81fb34575f.svg",
"static/media/doc.svg": "/static/media/doc.01619bc74278a86447e699223663ad64.svg",
"static/media/pdf.svg": "/static/media/pdf.eba1e4d316279fc69682dabae6fe6fa4.svg",
"static/media/video.svg": "/static/media/video.adecb1d58a645ca8d1b1bffad4dbe777.svg",
"static/media/default-skin.svg": "/static/media/default-skin.f64c3af3d0d25b9e4e00.svg",
"static/media/text.svg": "/static/media/text.8000463985257af8534ee29db23e1006.svg",
"static/media/file.svg": "/static/media/file.074f42545edfc31c849bdddb013ad8b2.svg",
"static/media/audio.svg": "/static/media/audio.e7112b210bfaa8d0a2ab69f74a66eea4.svg",
"static/media/image.svg": "/static/media/image.7660bae05269b3c8f2cdf669f04aef3b.svg",
"static/media/folder.svg": "/static/media/folder.0bdd7430280c98bf4970b6af5a061c2c.svg",
"static/media/archive.svg": "/static/media/archive.684c1e42f233aa9d53a8910d4fee091e.svg",
"static/media/empty.svg": "/static/media/empty.1ac6f6fb3e1d8c985f32df632ded4fe1.svg",
"static/media/psd.svg": "/static/media/psd.f2767f53369b5e75d7708a857d8381f4.svg",
"static/media/xls.svg": "/static/media/xls.9f55e13113424a3cc3316acbf072f072.svg",
"static/media/ppt.svg": "/static/media/ppt.8696321d8f3893a15681473769c8283c.svg",
"static/media/doc.svg": "/static/media/doc.017d14ee10ef3d2427494d1b0e52b03e.svg",
"static/media/pdf.svg": "/static/media/pdf.67d0061b2cef7912ed36b895cb8a328a.svg",
"static/media/video.svg": "/static/media/video.09bf3324c0e699b3611bf6bb0fc1b50e.svg",
"static/media/file.svg": "/static/media/file.b6be968e09dc04a45ec49a0dbcef53da.svg",
"static/media/text.svg": "/static/media/text.fa66aadee4b7dd9d51f4d1c863e5f75e.svg",
"static/media/audio.svg": "/static/media/audio.59dab6a9512494e5a34c8a424ab68f39.svg",
"static/media/image.svg": "/static/media/image.ee35022875b37a7962afa08bce8bc1fd.svg",
"static/media/folder.svg": "/static/media/folder.b4ce8a2960e8da846081c692d3065dff.svg",
"static/media/archive.svg": "/static/media/archive.f58837716d1fc6f2e61dc1618dad27a6.svg",
"index.html": "/index.html",
"main.b03c51fb.css.map": "/static/css/main.b03c51fb.css.map",
"main.b94b1947.js.map": "/static/js/main.b94b1947.js.map"
"main.3e2e293d.css.map": "/static/css/main.3e2e293d.css.map",
"main.1a935cc6.js.map": "/static/js/main.1a935cc6.js.map",
"833.2731ecbb.chunk.js.map": "/static/js/833.2731ecbb.chunk.js.map"
},
"entrypoints": [
"static/css/main.b03c51fb.css",
"static/js/main.b94b1947.js"
"static/css/main.3e2e293d.css",
"static/js/main.1a935cc6.js"
]
}
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="eyeblue tank"/><title>EyeblueTank</title><script defer="defer" src="/static/js/main.b94b1947.js"></script><link href="/static/css/main.b03c51fb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run EyeblueTank.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="eyeblue tank"/><title>EyeblueTank</title><script defer="defer" src="/static/js/main.1a935cc6.js"></script><link href="/static/css/main.3e2e293d.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run EyeblueTank.</noscript><div id="root"></div></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -10,14 +10,6 @@ object-assign
http://jedwatson.github.io/classnames
*/
/*! PhotoSwipe - v4.1.3 - 2019-01-08
* http://photoswipe.com
* Copyright (c) 2019 Dmitry Semenov; */
/*! PhotoSwipe Default UI - 4.1.3 - 2019-01-08
* http://photoswipe.com
* Copyright (c) 2019 Dmitry Semenov; */
/*! VelocityJS.org (1.5.2). (C) 2014 Julian Shapiro. MIT @license: en.wikipedia.org/wiki/MIT_License */
/*! VelocityJS.org jQuery Shim (1.0.1). (C) 2014 The jQuery Foundation. MIT @license: en.wikipedia.org/wiki/MIT_License. */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1514189353878" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1197" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M9.367 0h992.97v341.333H9.366z" fill="#55C7F7" p-id="1198"></path><path d="M9.367 341.333h992.97v341.334H9.366z" fill="#F95F5D" p-id="1199"></path><path d="M9.367 682.667h992.97V1024H9.366z" fill="#7ECF3B" p-id="1200"></path><path d="M350.7 0h310.303v1024H350.7z" fill="#FDAF42" p-id="1201"></path><path d="M304.154 386.638V646.05h403.394V386.638H304.154z m356.849 215.97H350.7V429.46h310.303v173.15z" fill="#FFFFFF" p-id="1202"></path></svg>

Before

Width:  |  Height:  |  Size: 828 B

@@ -0,0 +1,14 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1514189353878" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="1197" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M9.367 0h992.97v341.333H9.366z" fill="#55C7F7" p-id="1198"></path>
<path d="M9.367 341.333h992.97v341.334H9.366z" fill="#F95F5D" p-id="1199"></path>
<path d="M9.367 682.667h992.97V1024H9.366z" fill="#7ECF3B" p-id="1200"></path>
<path d="M350.7 0h310.303v1024H350.7z" fill="#FDAF42" p-id="1201"></path>
<path d="M304.154 386.638V646.05h403.394V386.638H304.154z m356.849 215.97H350.7V429.46h310.303v173.15z"
fill="#FFFFFF" p-id="1202"></path>
</svg>

After

Width:  |  Height:  |  Size: 855 B

@@ -0,0 +1,14 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1543321997742" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="3887" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M160 32c-12 0-24.8 4.8-33.6 14.4S112 68 112 80v864c0 12 4.8 24.8 14.4 33.6C136 987.2 148 992 160 992h704c12 0 24.8-4.8 33.6-14.4C907.2 968 912 956 912 944V304L640 32H160z"
fill="#FF5562" p-id="3888"></path>
<path d="M912 304H688c-12 0-24.8-4.8-33.6-14.4-9.6-8.8-14.4-21.6-14.4-33.6V32l272 272z" fill="#FFBBC0"
p-id="3889"></path>
<path d="M669.6 491.2c0-1.6 0.8-4 0-6.4V369.6c0-4.8-2.4-8.8-5.6-12-4-3.2-8-4-12.8-3.2l-250.4 70.4c-7.2 1.6-12 7.2-12 14.4v275.2c-9.6-4-20.8-6.4-32.8-6.4-8.8 0-17.6 0.8-26.4 3.2-40.8 11.2-66.4 43.2-58.4 72.8 6.4 23.2 30.4 37.6 60.8 37.6 8.8 0 17.6-1.6 26.4-3.2 36.8-10.4 60.8-36.8 60-63.2 0.8-1.6 0.8-3.2 0.8-5.6V570.4l220-61.6v136c-9.6-4-20.8-6.4-32.8-6.4-8.8 0-17.6 0.8-26.4 3.2-40.8 11.2-66.4 43.2-58.4 72.8C528 737.6 552 752 582.4 752c8.8 0 17.6-0.8 26.4-3.2 36-9.6 60-36 60-62.4 0.8-1.6 0.8-3.2 0.8-5.6V491.2z m-250.4 48v-53.6l220-61.6v53.6l-220 61.6z"
fill="#FFFFFF" p-id="3890"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1543321997742" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3887" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M160 32c-12 0-24.8 4.8-33.6 14.4S112 68 112 80v864c0 12 4.8 24.8 14.4 33.6C136 987.2 148 992 160 992h704c12 0 24.8-4.8 33.6-14.4C907.2 968 912 956 912 944V304L640 32H160z" fill="#FF5562" p-id="3888"></path><path d="M912 304H688c-12 0-24.8-4.8-33.6-14.4-9.6-8.8-14.4-21.6-14.4-33.6V32l272 272z" fill="#FFBBC0" p-id="3889"></path><path d="M669.6 491.2c0-1.6 0.8-4 0-6.4V369.6c0-4.8-2.4-8.8-5.6-12-4-3.2-8-4-12.8-3.2l-250.4 70.4c-7.2 1.6-12 7.2-12 14.4v275.2c-9.6-4-20.8-6.4-32.8-6.4-8.8 0-17.6 0.8-26.4 3.2-40.8 11.2-66.4 43.2-58.4 72.8 6.4 23.2 30.4 37.6 60.8 37.6 8.8 0 17.6-1.6 26.4-3.2 36.8-10.4 60.8-36.8 60-63.2 0.8-1.6 0.8-3.2 0.8-5.6V570.4l220-61.6v136c-9.6-4-20.8-6.4-32.8-6.4-8.8 0-17.6 0.8-26.4 3.2-40.8 11.2-66.4 43.2-58.4 72.8C528 737.6 552 752 582.4 752c8.8 0 17.6-0.8 26.4-3.2 36-9.6 60-36 60-62.4 0.8-1.6 0.8-3.2 0.8-5.6V491.2z m-250.4 48v-53.6l220-61.6v53.6l-220 61.6z" fill="#FFFFFF" p-id="3890"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1 +0,0 @@
<svg width="264" height="88" viewBox="0 0 264 88" xmlns="http://www.w3.org/2000/svg"><title>default-skin 2</title><g fill="none" fill-rule="evenodd"><g><path d="M67.002 59.5v3.768c-6.307.84-9.184 5.75-10.002 9.732 2.22-2.83 5.564-5.098 10.002-5.098V71.5L73 65.585 67.002 59.5z" id="Shape" fill="#fff"/><g fill="#fff"><path d="M13 29v-5h2v3h3v2h-5zM13 15h5v2h-3v3h-2v-5zM31 15v5h-2v-3h-3v-2h5zM31 29h-5v-2h3v-3h2v5z" id="Shape"/></g><g fill="#fff"><path d="M62 24v5h-2v-3h-3v-2h5zM62 20h-5v-2h3v-3h2v5zM70 20v-5h2v3h3v2h-5zM70 24h5v2h-3v3h-2v-5z"/></g><path d="M20.586 66l-5.656-5.656 1.414-1.414L22 64.586l5.656-5.656 1.414 1.414L23.414 66l5.656 5.656-1.414 1.414L22 67.414l-5.656 5.656-1.414-1.414L20.586 66z" fill="#fff"/><path d="M111.785 65.03L110 63.5l3-3.5h-10v-2h10l-3-3.5 1.785-1.468L117 59l-5.215 6.03z" fill="#fff"/><path d="M152.215 65.03L154 63.5l-3-3.5h10v-2h-10l3-3.5-1.785-1.468L147 59l5.215 6.03z" fill="#fff"/><g><path id="Rectangle-11" fill="#fff" d="M160.957 28.543l-3.25-3.25-1.413 1.414 3.25 3.25z"/><path d="M152.5 27c3.038 0 5.5-2.462 5.5-5.5s-2.462-5.5-5.5-5.5-5.5 2.462-5.5 5.5 2.462 5.5 5.5 5.5z" id="Oval-1" stroke="#fff" stroke-width="1.5"/><path fill="#fff" d="M150 21h5v1h-5z"/></g><g><path d="M116.957 28.543l-1.414 1.414-3.25-3.25 1.414-1.414 3.25 3.25z" fill="#fff"/><path d="M108.5 27c3.038 0 5.5-2.462 5.5-5.5s-2.462-5.5-5.5-5.5-5.5 2.462-5.5 5.5 2.462 5.5 5.5 5.5z" stroke="#fff" stroke-width="1.5"/><path fill="#fff" d="M106 21h5v1h-5z"/><path fill="#fff" d="M109.043 19.008l-.085 5-1-.017.085-5z"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1543300132402" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2107" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M533.947184 38.36489h65.880106v88.119185c117.859409 0.660894 235.60867-1.211639 353.35793 0.660894 25.102953-2.533427 44.81962 16.962943 42.506492 42.07691 1.872533 216.993492-0.440596 434.097133 1.112505 651.090626-1.112505 22.250094 2.191965 46.923466-10.695466 66.640133-16.081751 11.455494-36.899909 10.023557-55.625236 10.904749-110.25913-0.550745-220.408111-0.330447-330.656225-0.330447v88.119185h-68.843113c-167.657764-30.621417-335.745108-58.709407-503.722304-88.119185-0.110149-256.977572 0-513.955144 0-770.822567 168.858388-29.519927 337.826924-58.268811 506.685311-88.339483z" fill="#2A5699" p-id="2108"></path><path d="M599.816275 159.528769h363.491637v704.953477h-363.491637v-88.119185h286.38735v-44.059592h-286.38735v-55.07449h286.38735v-44.059593h-286.38735v-55.07449h286.38735v-44.059592h-286.38735v-55.074491h286.38735v-44.059592h-286.38735v-55.074491h286.38735v-44.059592h-286.38735v-55.07449h286.38735v-44.059593h-286.38735v-77.104286zM254.829667 354.162018c20.928306-1.211639 41.856613-2.092831 62.784919-3.19432 14.649814 74.350562 29.619061 148.590975 45.491529 222.61109 12.446835-76.443393 26.215457-152.666487 39.543484-228.999731 22.029796-0.771043 44.059592-1.982682 65.97924-3.30447-24.89367 106.734362-46.703168 214.349917-73.909966 320.423385-18.39488 9.582961-45.932125-0.440596-67.741623 1.10149-14.649814-72.90761-31.722906-145.38564-44.819621-218.645727-12.898446 71.156242-29.641091 141.651589-44.390039 212.367235a4187.754101 4187.754101 0 0 1-63.666111-3.866229c-18.284731-96.931103-39.763782-193.190297-56.836874-290.341698 18.835476-0.881192 37.792115-1.652235 56.627591-2.313129 11.33433 70.164901 24.221761 139.999355 34.146184 210.274404 15.508976-72.037433 31.37043-144.074867 46.791287-216.1123z" fill="#FFFFFF" p-id="2109"></path></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1543300132402" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="2107" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M533.947184 38.36489h65.880106v88.119185c117.859409 0.660894 235.60867-1.211639 353.35793 0.660894 25.102953-2.533427 44.81962 16.962943 42.506492 42.07691 1.872533 216.993492-0.440596 434.097133 1.112505 651.090626-1.112505 22.250094 2.191965 46.923466-10.695466 66.640133-16.081751 11.455494-36.899909 10.023557-55.625236 10.904749-110.25913-0.550745-220.408111-0.330447-330.656225-0.330447v88.119185h-68.843113c-167.657764-30.621417-335.745108-58.709407-503.722304-88.119185-0.110149-256.977572 0-513.955144 0-770.822567 168.858388-29.519927 337.826924-58.268811 506.685311-88.339483z"
fill="#2A5699" p-id="2108"></path>
<path d="M599.816275 159.528769h363.491637v704.953477h-363.491637v-88.119185h286.38735v-44.059592h-286.38735v-55.07449h286.38735v-44.059593h-286.38735v-55.07449h286.38735v-44.059592h-286.38735v-55.074491h286.38735v-44.059592h-286.38735v-55.074491h286.38735v-44.059592h-286.38735v-55.07449h286.38735v-44.059593h-286.38735v-77.104286zM254.829667 354.162018c20.928306-1.211639 41.856613-2.092831 62.784919-3.19432 14.649814 74.350562 29.619061 148.590975 45.491529 222.61109 12.446835-76.443393 26.215457-152.666487 39.543484-228.999731 22.029796-0.771043 44.059592-1.982682 65.97924-3.30447-24.89367 106.734362-46.703168 214.349917-73.909966 320.423385-18.39488 9.582961-45.932125-0.440596-67.741623 1.10149-14.649814-72.90761-31.722906-145.38564-44.819621-218.645727-12.898446 71.156242-29.641091 141.651589-44.390039 212.367235a4187.754101 4187.754101 0 0 1-63.666111-3.866229c-18.284731-96.931103-39.763782-193.190297-56.836874-290.341698 18.835476-0.881192 37.792115-1.652235 56.627591-2.313129 11.33433 70.164901 24.221761 139.999355 34.146184 210.274404 15.508976-72.037433 31.37043-144.074867 46.791287-216.1123z"
fill="#FFFFFF" p-id="2109"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,20 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1582644951614" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3842"
width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M692.906667 385.024c-11.605333-2.730667-23.210667-6.144-34.816-11.605333l-63.488 25.258666-5.461334 2.048h-5.461333c-8.874667 0-17.749333-4.096-23.210667-11.605333-5.461333-7.509333-6.826667-17.066667-4.778666-25.258667l8.874666-30.72-97.621333-19.797333v400.725333L901.12 402.090667l-105.130667-21.162667c-18.432 5.461333-38.229333 8.874667-59.392 8.874667-13.653333 0-28.672-1.365333-43.690666-4.778667z"
fill="#A6DAFC" p-id="3843"></path>
<path d="M512 502.442667v467.626666l389.802667-100.352V402.090667zM111.274667 413.696l356.352-100.352V580.266667z"
fill="#568EC8" p-id="3844"></path>
<path d="M111.274667 413.696v456.021333L512 970.069333V502.442667z" fill="#A6DAFC" p-id="3845"></path>
<path d="M512 502.442667l389.802667-100.352L1024 546.816 645.802667 658.090667z" fill="#78C5FD" p-id="3846"></path>
<path d="M111.274667 413.696l400.725333 88.746667-122.197333 167.253333L0 569.344z" fill="#CFF0FF"
p-id="3847"></path>
<path d="M886.784 160.426667c-2.730667-6.826667-6.144-13.653333-9.557333-20.48-1.365333-2.048-2.730667-4.096-4.096-6.826667-4.096-6.144-8.192-12.970667-13.653334-18.432s-10.24-11.605333-16.384-16.384c-2.048-2.048-4.096-3.413333-6.144-4.778667-8.192-6.826667-17.066667-12.288-25.941333-17.749333-2.048-1.365333-4.778667-2.730667-6.826667-3.413333-4.778667-2.048-9.557333-4.778667-15.018666-6.144-7.509333-2.730667-15.701333-5.461333-23.893334-6.826667-2.730667-0.682667-6.144-1.365333-8.874666-2.048-0.682667 0-2.048 0-2.730667-0.682667-2.048 0-4.096-0.682667-6.144-0.682666-1.365333 0-2.048 0-3.413333-0.682667-2.048 0-3.413333 0-5.461334-0.682667H727.04c-4.096 0-8.192 0-11.605333 0.682667h-2.048c-4.096 0.682667-8.192 0.682667-12.288 1.365333h-0.682667c-4.096 0.682667-7.509333 1.365333-11.605333 2.048-0.682667 0-1.365333 0-1.365334 0.682667-4.096 0.682667-7.509333 2.048-10.922666 3.413333-0.682667 0-1.365333 0.682667-2.048 0.682667-3.413333 1.365333-7.509333 2.730667-10.922667 4.096-0.682667 0-0.682667 0.682667-1.365333 0.682667-3.413333 1.365333-6.826667 3.413333-10.24 4.778666 0 0-0.682667 0-0.682667 0.682667-3.413333 2.048-6.826667 3.413333-10.24 5.461333-0.682667 0.682667-1.365333 0.682667-2.048 1.365334-3.413333 2.048-6.144 4.096-9.557333 6.144-0.682667 0.682667-1.365333 0.682667-1.365334 1.365333-3.413333 2.048-6.144 4.778667-8.874666 7.509333l-8.192 8.192-1.365334 1.365334c-2.730667 2.730667-4.778667 5.461333-7.509333 8.192-0.682667 0.682667-0.682667 1.365333-1.365333 1.365333-2.048 2.730667-4.778667 6.144-6.826667 8.874667 0 0.682667-0.682667 0.682667-0.682667 1.365333-2.048 3.413333-4.096 6.144-5.461333 9.557333 0 0.682667-0.682667 0.682667-0.682667 1.365334-2.048 3.413333-3.413333 6.826667-4.778666 10.24 0 0.682667-0.682667 1.365333-0.682667 2.048-1.365333 3.413333-2.730667 6.826667-4.096 10.922666 0 0.682667-0.682667 1.365333-0.682667 2.048-1.365333 4.096-2.048 7.509333-3.413333 11.605334-10.24 46.421333 2.730667 92.16 32.085333 127.658666L593.92 339.968l-9.557333 32.768v0.682667h0.682666l58.709334-23.210667 16.384-6.826667c6.144 3.413333 12.970667 6.144 19.797333 8.192s13.653333 4.778667 21.162667 6.144c2.730667 0.682667 6.144 1.365333 8.874666 2.048 0.682667 0 2.048 0 2.730667 0.682667 2.048 0 4.096 0.682667 6.144 0.682667 1.365333 0 2.048 0 3.413333 0.682666 2.048 0 3.413333 0 5.461334 0.682667H739.328c4.096 0 8.192 0 11.605333-0.682667h2.048c4.096-0.682667 8.192-0.682667 12.288-1.365333h0.682667c4.096-0.682667 7.509333-1.365333 11.605333-2.048 0.682667 0 1.365333 0 2.048-0.682667 4.096-0.682667 7.509333-2.048 10.922667-3.413333 0.682667 0 1.365333-0.682667 2.048-0.682667 3.413333-1.365333 7.509333-2.730667 10.922667-4.096 0.682667 0 0.682667-0.682667 1.365333-0.682666 3.413333-1.365333 6.826667-3.413333 10.24-4.778667 0 0 0.682667 0 0.682667-0.682667 3.413333-2.048 6.826667-3.413333 10.24-5.461333 0.682667-0.682667 1.365333-0.682667 2.048-1.365333 3.413333-2.048 6.144-4.096 9.557333-6.144 0.682667-0.682667 1.365333-0.682667 1.365333-1.365334 3.413333-2.048 6.144-4.778667 8.874667-7.509333l8.192-8.192 1.365333-1.365333c2.730667-2.730667 4.778667-5.461333 7.509334-8.192 0.682667-0.682667 0.682667-1.365333 1.365333-2.048 2.048-2.730667 4.778667-6.144 6.826667-8.874667 0-0.682667 0.682667-0.682667 0.682666-1.365333 2.048-3.413333 4.096-6.144 5.461334-9.557334 0-0.682667 0.682667-0.682667 0.682666-1.365333 2.048-3.413333 3.413333-6.826667 4.778667-10.24 0-0.682667 0.682667-1.365333 0.682667-2.048 1.365333-3.413333 2.730667-6.826667 4.096-10.922667 0-0.682667 0.682667-1.365333 0.682666-2.048 1.365333-4.096 2.048-7.509333 3.413334-11.605333 5.461333-28.672 2.730667-57.344-6.826667-83.285333z"
fill="#F9DA3E" p-id="3848"></path>
<path d="M739.328 167.936h48.469333v27.989333h-13.653333v-15.701333h-83.968v15.701333h-13.653333v-27.989333h48.469333c-1.365333-3.413333-3.413333-6.826667-4.778667-10.24l14.336-2.048c2.048 3.413333 3.413333 7.509333 4.778667 12.288z m37.546667 52.565333v12.288h-37.546667v25.258667h48.469333v12.970667H677.205333v-12.970667h48.469334v-25.258667h-37.546667v-12.288h88.746667z m-51.882667-24.576c-10.24 8.874667-22.528 16.384-37.546667 22.528l-5.461333-12.288c14.336-5.461333 25.941333-11.605333 34.816-19.797333l8.192 9.557333z m56.661333 10.922667l-7.509333 11.605333c-11.605333-9.557333-23.210667-17.066667-35.498667-22.528l6.826667-10.24c13.653333 6.144 25.258667 12.970667 36.181333 21.162667z"
fill="#FFFFFF" p-id="3849"></path>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.6 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1514189511483" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6709" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M676.297143 0H145.609143C130.834286 0 118.857143 11.977143 118.857143 35.218286V1005.714286c0 6.308571 11.977143 18.285714 26.752 18.285714h732.781714c14.774857 0 26.752-11.977143 26.752-18.285714V237.312c0-12.726857-1.700571-16.822857-4.699428-19.84L687.670857 4.699429A16.164571 16.164571 0 0 0 676.297143 0z" fill="#E9E9E0" p-id="6710"></path><path d="M685.714286 2.761143V219.428571h216.667428z" fill="#D9D7CA" p-id="6711"></path><path d="M878.390857 1024H145.609143A26.752 26.752 0 0 1 118.857143 997.248V713.142857h786.285714v284.105143c0 14.774857-11.977143 26.752-26.752 26.752z" fill="#C8BDB8" p-id="6712"></path><path d="M338.285714 859.428571m-54.857143 0a54.857143 54.857143 0 1 0 109.714286 0 54.857143 54.857143 0 1 0-109.714286 0Z" fill="#FFFFFF" p-id="6713"></path><path d="M521.142857 859.428571m-54.857143 0a54.857143 54.857143 0 1 0 109.714286 0 54.857143 54.857143 0 1 0-109.714286 0Z" fill="#FFFFFF" p-id="6714"></path><path d="M704 859.428571m-54.857143 0a54.857143 54.857143 0 1 0 109.714286 0 54.857143 54.857143 0 1 0-109.714286 0Z" fill="#FFFFFF" p-id="6715"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,19 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1514189511483" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="6709" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M676.297143 0H145.609143C130.834286 0 118.857143 11.977143 118.857143 35.218286V1005.714286c0 6.308571 11.977143 18.285714 26.752 18.285714h732.781714c14.774857 0 26.752-11.977143 26.752-18.285714V237.312c0-12.726857-1.700571-16.822857-4.699428-19.84L687.670857 4.699429A16.164571 16.164571 0 0 0 676.297143 0z"
fill="#E9E9E0" p-id="6710"></path>
<path d="M685.714286 2.761143V219.428571h216.667428z" fill="#D9D7CA" p-id="6711"></path>
<path d="M878.390857 1024H145.609143A26.752 26.752 0 0 1 118.857143 997.248V713.142857h786.285714v284.105143c0 14.774857-11.977143 26.752-26.752 26.752z"
fill="#C8BDB8" p-id="6712"></path>
<path d="M338.285714 859.428571m-54.857143 0a54.857143 54.857143 0 1 0 109.714286 0 54.857143 54.857143 0 1 0-109.714286 0Z"
fill="#FFFFFF" p-id="6713"></path>
<path d="M521.142857 859.428571m-54.857143 0a54.857143 54.857143 0 1 0 109.714286 0 54.857143 54.857143 0 1 0-109.714286 0Z"
fill="#FFFFFF" p-id="6714"></path>
<path d="M704 859.428571m-54.857143 0a54.857143 54.857143 0 1 0 109.714286 0 54.857143 54.857143 0 1 0-109.714286 0Z"
fill="#FFFFFF" p-id="6715"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1514189634329" class="icon" style="" viewBox="0 0 1219 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10648" xmlns:xlink="http://www.w3.org/1999/xlink" width="238.0859375" height="200"><defs><style type="text/css"></style></defs><path d="M1197.787429 327.192381a19.504762 19.504762 0 0 1 19.553523 19.407238v614.156191c0 34.913524-28.574476 63.24419-63.878095 63.24419H63.878095C28.623238 1024 0 995.718095 0 960.75581V346.599619a19.504762 19.504762 0 0 1 19.602286-19.358476h1178.185143z m-66.80381-213.089524C1179.550476 114.102857 1219.047619 153.209905 1219.047619 201.386667v80.213333H1.657905V88.259048C1.657905 39.497143 41.593905 0 90.794667 0h290.864762c17.408 0 33.987048 7.655619 45.104761 20.967619l78.214096 93.135238h626.005333z" fill="#23B7E5" p-id="10649"></path></svg>

Before

Width:  |  Height:  |  Size: 943 B

@@ -0,0 +1,10 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1514189634329" class="icon" style="" viewBox="0 0 1219 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="10648" width="238.0859375" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M1197.787429 327.192381a19.504762 19.504762 0 0 1 19.553523 19.407238v614.156191c0 34.913524-28.574476 63.24419-63.878095 63.24419H63.878095C28.623238 1024 0 995.718095 0 960.75581V346.599619a19.504762 19.504762 0 0 1 19.602286-19.358476h1178.185143z m-66.80381-213.089524C1179.550476 114.102857 1219.047619 153.209905 1219.047619 201.386667v80.213333H1.657905V88.259048C1.657905 39.497143 41.593905 0 90.794667 0h290.864762c17.408 0 33.987048 7.655619 45.104761 20.967619l78.214096 93.135238h626.005333z"
fill="#23B7E5" p-id="10649"></path>
</svg>

After

Width:  |  Height:  |  Size: 949 B

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1514190539077" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14917" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M128 0h597.3l227.6 227.9v739.2c0 31.4-25.5 56.9-56.9 56.9H128c-31.4 0-56.9-25.5-56.9-56.9V56.9C71.1 25.5 96.6 0 128 0z" fill="#13BE93" p-id="14918"></path><path d="M630.5 1024H128c-25.7 0-47.4-17-54.4-40.4l218.1-218.1c22.2-22.2 58.2-22.2 80.5 0L630.5 1024z" fill="#6BD8BD" p-id="14919"></path><path d="M952.9 858v109.1c0 31.4-25.5 56.9-56.9 56.9H322.4l-4.7-4.7 358-358c22.2-22.2 58.2-22.2 80.5 0L952.9 858z" fill="#9CF9E2" p-id="14920"></path><path d="M725.3 0l227.6 227.6H782.2c-31.4 0-56.9-25.5-56.9-56.9V0z" fill="#69DCC0" p-id="14921"></path><path d="M327.1 483.6m-85.3 0a85.3 85.3 0 1 0 170.6 0 85.3 85.3 0 1 0-170.6 0Z" fill="#E9FFFA" p-id="14922"></path></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1514190539077" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="14917" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M128 0h597.3l227.6 227.9v739.2c0 31.4-25.5 56.9-56.9 56.9H128c-31.4 0-56.9-25.5-56.9-56.9V56.9C71.1 25.5 96.6 0 128 0z"
fill="#13BE93" p-id="14918"></path>
<path d="M630.5 1024H128c-25.7 0-47.4-17-54.4-40.4l218.1-218.1c22.2-22.2 58.2-22.2 80.5 0L630.5 1024z"
fill="#6BD8BD" p-id="14919"></path>
<path d="M952.9 858v109.1c0 31.4-25.5 56.9-56.9 56.9H322.4l-4.7-4.7 358-358c22.2-22.2 58.2-22.2 80.5 0L952.9 858z"
fill="#9CF9E2" p-id="14920"></path>
<path d="M725.3 0l227.6 227.6H782.2c-31.4 0-56.9-25.5-56.9-56.9V0z" fill="#69DCC0" p-id="14921"></path>
<path d="M327.1 483.6m-85.3 0a85.3 85.3 0 1 0 170.6 0 85.3 85.3 0 1 0-170.6 0Z" fill="#E9FFFA" p-id="14922"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,13 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1514284130163" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="1551" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M915.114667 1024H108.885333V0h587.776v218.112H914.773333V1024z m-785.066667-20.821333h763.904V238.933333H675.84V20.821333H129.706667v982.357334z"
fill="#F90E09" p-id="1552"></path>
<path d="M708.266667 10.581333L904.533333 206.848h-196.266666z" fill="#F90E09" p-id="1553"></path>
<path d="M282.965333 792.576c33.450667-23.210667 55.637333-57.344 76.8-91.136-30.378667 25.941333-59.050667 54.954667-76.8 91.136z m396.629334-169.642667c31.402667 15.018667 65.877333 31.061333 101.376 21.504-24.576-29.696-67.584-21.504-101.376-21.504z m-246.101334 23.210667c45.397333-16.725333 91.818667-29.696 138.922667-39.594667-30.378667-26.965333-55.978667-58.368-76.458667-93.525333-16.725333 46.08-38.912 90.112-62.464 133.12z m67.925334-243.370667c14.677333-37.546667 19.114667-83.285333-5.802667-117.418666-7.168 38.912-4.096 79.189333 5.802667 117.418666z m-45.397334-105.130666c4.096-24.917333 35.84-29.696 55.637334-22.186667 12.288 7.850667 18.432 22.869333 18.773333 37.205333 1.365333 44.032-6.485333 87.381333-12.629333 130.730667-3.413333 20.138667 8.874667 38.229333 17.749333 55.296 21.845333 40.277333 52.224 76.458667 92.16 99.669333 48.128-5.802667 98.645333-12.970667 145.749333 2.389334 35.157333 11.946667 26.965333 77.141333-11.264 77.824-53.589333 2.730667-102.058667-23.893333-150.186666-43.349334-66.218667 4.778667-130.730667 24.917333-194.218667 44.032-28.672 46.762667-54.613333 98.986667-101.034667 130.730667-23.210667 18.090667-76.458667 3.072-64.512-32.085333 20.138667-49.152 70.314667-76.117333 114.346667-100.693334 12.629333-6.485333 19.797333-18.773333 26.282667-31.061333 30.037333-57.685333 60.074667-115.370667 84.309333-175.786667-12.629333-56.661333-31.061333-114.688-21.162667-172.714666z"
fill="#F90E09" p-id="1554"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1514284130163" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1551" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M915.114667 1024H108.885333V0h587.776v218.112H914.773333V1024z m-785.066667-20.821333h763.904V238.933333H675.84V20.821333H129.706667v982.357334z" fill="#F90E09" p-id="1552"></path><path d="M708.266667 10.581333L904.533333 206.848h-196.266666z" fill="#F90E09" p-id="1553"></path><path d="M282.965333 792.576c33.450667-23.210667 55.637333-57.344 76.8-91.136-30.378667 25.941333-59.050667 54.954667-76.8 91.136z m396.629334-169.642667c31.402667 15.018667 65.877333 31.061333 101.376 21.504-24.576-29.696-67.584-21.504-101.376-21.504z m-246.101334 23.210667c45.397333-16.725333 91.818667-29.696 138.922667-39.594667-30.378667-26.965333-55.978667-58.368-76.458667-93.525333-16.725333 46.08-38.912 90.112-62.464 133.12z m67.925334-243.370667c14.677333-37.546667 19.114667-83.285333-5.802667-117.418666-7.168 38.912-4.096 79.189333 5.802667 117.418666z m-45.397334-105.130666c4.096-24.917333 35.84-29.696 55.637334-22.186667 12.288 7.850667 18.432 22.869333 18.773333 37.205333 1.365333 44.032-6.485333 87.381333-12.629333 130.730667-3.413333 20.138667 8.874667 38.229333 17.749333 55.296 21.845333 40.277333 52.224 76.458667 92.16 99.669333 48.128-5.802667 98.645333-12.970667 145.749333 2.389334 35.157333 11.946667 26.965333 77.141333-11.264 77.824-53.589333 2.730667-102.058667-23.893333-150.186666-43.349334-66.218667 4.778667-130.730667 24.917333-194.218667 44.032-28.672 46.762667-54.613333 98.986667-101.034667 130.730667-23.210667 18.090667-76.458667 3.072-64.512-32.085333 20.138667-49.152 70.314667-76.117333 114.346667-100.693334 12.629333-6.485333 19.797333-18.773333 26.282667-31.061333 30.037333-57.685333 60.074667-115.370667 84.309333-175.786667-12.629333-56.661333-31.061333-114.688-21.162667-172.714666z" fill="#F90E09" p-id="1554"></path></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1543300178225" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5733" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M537.372818 38.36489h62.454472v99.134083c117.738246 0.660894 235.60867-1.211639 353.236766 0.550745 25.334266-2.423278 45.172097 17.29339 42.627656 42.627655 1.872533 205.758296-0.440596 411.626741 1.10149 617.506201-1.10149 22.250094 2.20298 46.923466-10.574302 66.640134-16.081751 11.664777-36.999043 10.133706-55.834519 11.014898-110.148981-0.550745-220.297962-0.330447-330.557091-0.330447v110.148981h-68.523681c-167.856032-30.621417-336.06454-58.599258-504.041736-88.119185-0.110149-256.977572 0-513.955144 0-770.822567 169.970892-29.420793 339.93077-59.381316 510.110945-88.350498z" fill="#D24625" p-id="5734"></path><path d="M599.82729 170.543667h363.491636v671.908783h-363.491636v-88.119185h264.357554v-44.059592h-264.357554v-55.074491h264.357554v-44.059592h-264.247405c-0.110149-21.5892-0.110149-43.1784-0.231313-64.767601 43.630011 13.53731 93.307202 13.217878 132.299941-13.008594 42.18706-25.003819 64.216856-72.698327 67.851772-120.271672-48.366417-0.330447-96.710805-0.220298-144.956059-0.220298-0.121164-47.914807 0.550745-95.829613-0.991341-143.634271-18.075448 3.524767-36.128866 7.269833-54.083149 11.125047v-109.818534z" fill="#FFFFFF" p-id="5735"></path><path d="M677.041725 246.216017c76.553542 3.524767 140.880546 67.961921 144.956059 144.295165-48.366417 0.550745-96.710805 0.330447-145.066208 0.330447 0-48.245254-0.121164-96.490507 0.110149-144.625612z" fill="#D24625" p-id="5736"></path><path d="M203.940838 329.378497c59.810897 2.863874 132.288926-23.682031 181.855967 21.148605 47.485226 57.607917 34.917227 163.571236-35.467972 198.047867-25.003819 12.777282-53.642554 11.014898-80.849351 10.133706-0.110149 43.068251-0.220298 86.136503-0.110149 129.19374-21.820513-1.872533-43.74016-3.745065-65.538644-5.507449-0.980326-117.628097-1.211639-235.377357 0.110149-353.016469z" fill="#FFFFFF" p-id="5737"></path><path d="M269.380347 388.969096c21.699349-0.991341 48.575701-4.956704 63.214501 15.420857 12.567999 21.5892 11.89609 50.007637 1.442951 72.257732-12.567999 22.69069-40.986436 20.48771-62.895068 23.131286-2.313129-36.899909-2.092831-73.799817-1.762384-110.809875z" fill="#D24625" p-id="5738"></path></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1543300178225" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="5733" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M537.372818 38.36489h62.454472v99.134083c117.738246 0.660894 235.60867-1.211639 353.236766 0.550745 25.334266-2.423278 45.172097 17.29339 42.627656 42.627655 1.872533 205.758296-0.440596 411.626741 1.10149 617.506201-1.10149 22.250094 2.20298 46.923466-10.574302 66.640134-16.081751 11.664777-36.999043 10.133706-55.834519 11.014898-110.148981-0.550745-220.297962-0.330447-330.557091-0.330447v110.148981h-68.523681c-167.856032-30.621417-336.06454-58.599258-504.041736-88.119185-0.110149-256.977572 0-513.955144 0-770.822567 169.970892-29.420793 339.93077-59.381316 510.110945-88.350498z"
fill="#D24625" p-id="5734"></path>
<path d="M599.82729 170.543667h363.491636v671.908783h-363.491636v-88.119185h264.357554v-44.059592h-264.357554v-55.074491h264.357554v-44.059592h-264.247405c-0.110149-21.5892-0.110149-43.1784-0.231313-64.767601 43.630011 13.53731 93.307202 13.217878 132.299941-13.008594 42.18706-25.003819 64.216856-72.698327 67.851772-120.271672-48.366417-0.330447-96.710805-0.220298-144.956059-0.220298-0.121164-47.914807 0.550745-95.829613-0.991341-143.634271-18.075448 3.524767-36.128866 7.269833-54.083149 11.125047v-109.818534z"
fill="#FFFFFF" p-id="5735"></path>
<path d="M677.041725 246.216017c76.553542 3.524767 140.880546 67.961921 144.956059 144.295165-48.366417 0.550745-96.710805 0.330447-145.066208 0.330447 0-48.245254-0.121164-96.490507 0.110149-144.625612z"
fill="#D24625" p-id="5736"></path>
<path d="M203.940838 329.378497c59.810897 2.863874 132.288926-23.682031 181.855967 21.148605 47.485226 57.607917 34.917227 163.571236-35.467972 198.047867-25.003819 12.777282-53.642554 11.014898-80.849351 10.133706-0.110149 43.068251-0.220298 86.136503-0.110149 129.19374-21.820513-1.872533-43.74016-3.745065-65.538644-5.507449-0.980326-117.628097-1.211639-235.377357 0.110149-353.016469z"
fill="#FFFFFF" p-id="5737"></path>
<path d="M269.380347 388.969096c21.699349-0.991341 48.575701-4.956704 63.214501 15.420857 12.567999 21.5892 11.89609 50.007637 1.442951 72.257732-12.567999 22.69069-40.986436 20.48771-62.895068 23.131286-2.313129-36.899909-2.092831-73.799817-1.762384-110.809875z"
fill="#D24625" p-id="5738"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1545064197737" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14227" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M759.725253 0H134.464646v1024h817.131314V191.870707z" fill="#02007A" p-id="14228"></path><path d="M72.40404 708.525253h687.838384v258.585858H72.40404z" fill="#230091" p-id="14229"></path><path d="M82.747475 718.868687h667.151515v237.89899H82.747475zM755.070707 5.171717l191.353535 191.353536h-191.353535z" fill="#FFFFFF" p-id="14230"></path><path d="M506.828283 791.272727v93.090909h25.858586c28.444444 0 41.373737-12.929293 41.373737-45.511111s-12.929293-47.579798-41.373737-47.579798h-25.858586z m-25.858586-25.858586h51.717172c42.408081 0 67.232323 29.99596 67.232323 74.989899S575.612121 910.222222 532.686869 910.222222H480.969697v-144.808081z m-132.39596 104.468687c1.034343 26.375758 22.755556 42.408081 55.337374 42.408081 34.650505 0 55.854545-17.066667 55.854546-43.959596 0-21.20404-12.412121-33.09899-40.856566-39.822222l-16.549495-3.620202c-17.583838-4.137374-24.824242-9.826263-24.824242-19.135354 0-11.894949 10.860606-19.652525 26.892929-19.652525s27.410101 7.757576 28.444444 21.204041h24.307071c-0.517172-24.824242-21.20404-41.890909-52.751515-41.89091S351.676768 782.480808 351.676768 807.822222c0 20.169697 12.412121 33.09899 38.787878 38.787879l18.618182 4.137374c18.10101 4.137374 25.341414 10.343434 25.341414 20.169697 0 11.894949-11.894949 20.169697-28.961616 20.169697s-30.513131-8.791919-32.064646-21.721212l-24.824243 0.517171zM258.585859 788.686869v53.785858h20.686868c18.10101 0 31.030303-9.826263 31.030303-26.892929s-12.412121-26.892929-31.030303-26.892929h-20.686868z m-25.858586-23.272728h56.888889c28.444444 0 46.545455 20.686869 46.545454 49.131314s-17.583838 49.131313-46.545454 49.131313h-31.030303v46.545454h-25.858586v-144.808081z" fill="#230091" p-id="14231"></path><path d="M415.806061 443.216162h-31.030303v137.567676H321.163636V196.525253h93.090909c91.022222 0 140.670707 35.684848 140.670708 122.052525 0.517172 103.434343-92.573737 124.638384-139.119192 124.638384z m-5.688889-192.905051c-6.206061 0-14.99798 0.517172-26.89293 1.551515V387.878788h36.202021C461.834343 387.878788 491.313131 366.157576 491.313131 312.888889c0-40.856566-32.064646-62.577778-81.195959-62.577778z m258.068687 337.19596c70.852525 0 96.711111-50.165657 96.711111-84.816162 0-54.820202-35.684848-67.749495-66.19798-87.40202-25.858586-17.066667-49.648485-23.272727-49.648485-40.856566 0-22.755556 15.515152-27.410101 36.20202-27.410101 25.858586 0 54.820202 2.068687 67.232323 16.032323V306.682828c-12.929293-10.343434-37.236364-13.446465-68.783838-13.446464-67.749495 0-93.608081 44.993939-93.608081 84.29899 0 50.682828 33.616162 65.680808 66.19798 83.781818 21.20404 11.894949 46.545455 20.686869 46.545455 44.476767 0 13.963636-8.791919 27.927273-37.753536 27.927273-26.892929 0-59.991919-10.343434-74.989899-23.789899v59.991919c13.446465 10.343434 40.339394 17.583838 78.09293 17.583839z" fill="#7AB7FF" p-id="14232"></path></svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1545064197737" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="14227" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M759.725253 0H134.464646v1024h817.131314V191.870707z" fill="#02007A" p-id="14228"></path>
<path d="M72.40404 708.525253h687.838384v258.585858H72.40404z" fill="#230091" p-id="14229"></path>
<path d="M82.747475 718.868687h667.151515v237.89899H82.747475zM755.070707 5.171717l191.353535 191.353536h-191.353535z"
fill="#FFFFFF" p-id="14230"></path>
<path d="M506.828283 791.272727v93.090909h25.858586c28.444444 0 41.373737-12.929293 41.373737-45.511111s-12.929293-47.579798-41.373737-47.579798h-25.858586z m-25.858586-25.858586h51.717172c42.408081 0 67.232323 29.99596 67.232323 74.989899S575.612121 910.222222 532.686869 910.222222H480.969697v-144.808081z m-132.39596 104.468687c1.034343 26.375758 22.755556 42.408081 55.337374 42.408081 34.650505 0 55.854545-17.066667 55.854546-43.959596 0-21.20404-12.412121-33.09899-40.856566-39.822222l-16.549495-3.620202c-17.583838-4.137374-24.824242-9.826263-24.824242-19.135354 0-11.894949 10.860606-19.652525 26.892929-19.652525s27.410101 7.757576 28.444444 21.204041h24.307071c-0.517172-24.824242-21.20404-41.890909-52.751515-41.89091S351.676768 782.480808 351.676768 807.822222c0 20.169697 12.412121 33.09899 38.787878 38.787879l18.618182 4.137374c18.10101 4.137374 25.341414 10.343434 25.341414 20.169697 0 11.894949-11.894949 20.169697-28.961616 20.169697s-30.513131-8.791919-32.064646-21.721212l-24.824243 0.517171zM258.585859 788.686869v53.785858h20.686868c18.10101 0 31.030303-9.826263 31.030303-26.892929s-12.412121-26.892929-31.030303-26.892929h-20.686868z m-25.858586-23.272728h56.888889c28.444444 0 46.545455 20.686869 46.545454 49.131314s-17.583838 49.131313-46.545454 49.131313h-31.030303v46.545454h-25.858586v-144.808081z"
fill="#230091" p-id="14231"></path>
<path d="M415.806061 443.216162h-31.030303v137.567676H321.163636V196.525253h93.090909c91.022222 0 140.670707 35.684848 140.670708 122.052525 0.517172 103.434343-92.573737 124.638384-139.119192 124.638384z m-5.688889-192.905051c-6.206061 0-14.99798 0.517172-26.89293 1.551515V387.878788h36.202021C461.834343 387.878788 491.313131 366.157576 491.313131 312.888889c0-40.856566-32.064646-62.577778-81.195959-62.577778z m258.068687 337.19596c70.852525 0 96.711111-50.165657 96.711111-84.816162 0-54.820202-35.684848-67.749495-66.19798-87.40202-25.858586-17.066667-49.648485-23.272727-49.648485-40.856566 0-22.755556 15.515152-27.410101 36.20202-27.410101 25.858586 0 54.820202 2.068687 67.232323 16.032323V306.682828c-12.929293-10.343434-37.236364-13.446465-68.783838-13.446464-67.749495 0-93.608081 44.993939-93.608081 84.29899 0 50.682828 33.616162 65.680808 66.19798 83.781818 21.20404 11.894949 46.545455 20.686869 46.545455 44.476767 0 13.963636-8.791919 27.927273-37.753536 27.927273-26.892929 0-59.991919-10.343434-74.989899-23.789899v59.991919c13.446465 10.343434 40.339394 17.583838 78.09293 17.583839z"
fill="#7AB7FF" p-id="14232"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1514189544355" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8186" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M176 1024l672 0c26.464 0 48-21.536 48-48l0-672c0-0.544-0.256-0.992-0.288-1.504-0.064-0.736-0.256-1.376-0.416-2.08-0.544-2.272-1.472-4.32-2.88-6.112-0.224-0.288-0.192-0.64-0.416-0.928l-256-288c-0.096-0.096-0.224-0.096-0.32-0.192-1.92-2.048-4.352-3.456-7.136-4.288-0.608-0.192-1.152-0.256-1.792-0.352-0.928-0.16-1.792-0.544-2.752-0.544l-448 0c-26.464 0-48 21.536-48 48l0 928c0 26.464 21.536 48 48 48zM640 58.08l204.384 229.92-188.384 0c-7.04 0-16-13.44-16-24l0-205.92zM160 48c0-8.832 7.2-16 16-16l432 0 0 232c0 25.888 20.96 56 48 56l208 0 0 656c0 8.832-7.2 16-16 16l-672 0c-8.96 0-16-7.04-16-16l0-928zM304 448l416 0c8.832 0 16-7.168 16-16s-7.168-16-16-16l-416 0c-8.832 0-16 7.168-16 16s7.168 16 16 16zM304 576l416 0c8.832 0 16-7.168 16-16s-7.168-16-16-16l-416 0c-8.832 0-16 7.168-16 16s7.168 16 16 16zM304 320l192 0c8.832 0 16-7.168 16-16s-7.168-16-16-16l-192 0c-8.832 0-16 7.168-16 16s7.168 16 16 16zM304 704l416 0c8.832 0 16-7.168 16-16s-7.168-16-16-16l-416 0c-8.832 0-16 7.168-16 16s7.168 16 16 16zM304 832l416 0c8.832 0 16-7.168 16-16s-7.168-16-16-16l-416 0c-8.832 0-16 7.168-16 16s7.168 16 16 16z" p-id="8187"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1514189544355" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="8186" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M176 1024l672 0c26.464 0 48-21.536 48-48l0-672c0-0.544-0.256-0.992-0.288-1.504-0.064-0.736-0.256-1.376-0.416-2.08-0.544-2.272-1.472-4.32-2.88-6.112-0.224-0.288-0.192-0.64-0.416-0.928l-256-288c-0.096-0.096-0.224-0.096-0.32-0.192-1.92-2.048-4.352-3.456-7.136-4.288-0.608-0.192-1.152-0.256-1.792-0.352-0.928-0.16-1.792-0.544-2.752-0.544l-448 0c-26.464 0-48 21.536-48 48l0 928c0 26.464 21.536 48 48 48zM640 58.08l204.384 229.92-188.384 0c-7.04 0-16-13.44-16-24l0-205.92zM160 48c0-8.832 7.2-16 16-16l432 0 0 232c0 25.888 20.96 56 48 56l208 0 0 656c0 8.832-7.2 16-16 16l-672 0c-8.96 0-16-7.04-16-16l0-928zM304 448l416 0c8.832 0 16-7.168 16-16s-7.168-16-16-16l-416 0c-8.832 0-16 7.168-16 16s7.168 16 16 16zM304 576l416 0c8.832 0 16-7.168 16-16s-7.168-16-16-16l-416 0c-8.832 0-16 7.168-16 16s7.168 16 16 16zM304 320l192 0c8.832 0 16-7.168 16-16s-7.168-16-16-16l-192 0c-8.832 0-16 7.168-16 16s7.168 16 16 16zM304 704l416 0c8.832 0 16-7.168 16-16s-7.168-16-16-16l-416 0c-8.832 0-16 7.168-16 16s7.168 16 16 16zM304 832l416 0c8.832 0 16-7.168 16-16s-7.168-16-16-16l-416 0c-8.832 0-16 7.168-16 16s7.168 16 16 16z"
p-id="8187"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,23 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1514189554639" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="8871" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M80 34.4h864v960H80z" fill="#8095FF" p-id="8872"></path>
<path d="M176 112m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8873"></path>
<path d="M176 272m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8874"></path>
<path d="M176 432m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8875"></path>
<path d="M176 592m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8876"></path>
<path d="M176 752m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8877"></path>
<path d="M176 912m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8878"></path>
<path d="M864 112m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8879"></path>
<path d="M864 272m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8880"></path>
<path d="M864 432m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8881"></path>
<path d="M864 592m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8882"></path>
<path d="M864 752m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8883"></path>
<path d="M864 912m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8884"></path>
<path d="M648 508L436 362.4c-4.8-3.2-11.2-4-16.8-0.8-5.6 3.2-8.8 8.8-8.8 14.4v290.4c0 5.6 3.2 11.2 8.8 14.4 5.6 3.2 12 2.4 16.8-0.8L648 533.6c4.8-2.4 7.2-8 7.2-12.8 0-4.8-3.2-9.6-7.2-12.8z"
fill="#FFFFFF" p-id="8885"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1514189554639" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8871" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M80 34.4h864v960H80z" fill="#8095FF" p-id="8872"></path><path d="M176 112m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8873"></path><path d="M176 272m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8874"></path><path d="M176 432m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8875"></path><path d="M176 592m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8876"></path><path d="M176 752m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8877"></path><path d="M176 912m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8878"></path><path d="M864 112m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8879"></path><path d="M864 272m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8880"></path><path d="M864 432m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8881"></path><path d="M864 592m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8882"></path><path d="M864 752m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8883"></path><path d="M864 912m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#FFFFFF" p-id="8884"></path><path d="M648 508L436 362.4c-4.8-3.2-11.2-4-16.8-0.8-5.6 3.2-8.8 8.8-8.8 14.4v290.4c0 5.6 3.2 11.2 8.8 14.4 5.6 3.2 12 2.4 16.8-0.8L648 533.6c4.8-2.4 7.2-8 7.2-12.8 0-4.8-3.2-9.6-7.2-12.8z" fill="#FFFFFF" p-id="8885"></path></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1543300142282" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2233" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M534.310676 38.36489h65.428495v88.119185c110.25913 0 220.507245 0.220298 330.766374-0.330447 18.615178 0.771043 39.113903-0.550745 55.305803 10.574302 11.345345 16.302049 10.023557 37.010058 10.783586 55.746399-0.550745 191.218631-0.319432 382.327112-0.220298 573.435594-0.550745 32.053353 2.974022 64.767601-3.745066 96.380358-4.405959 22.910988-31.943204 23.461733-50.338084 24.232776-114.114344 0.330447-228.338837-0.220298-342.56333 0v99.134083h-68.402517c-167.867047-30.511268-336.075555-58.709407-504.151885-88.119185v-770.932716c169.0897-29.420793 338.168386-58.389975 507.136922-88.240349z" fill="#207245" p-id="2234"></path><path d="M599.739171 159.528769h363.491636v693.938579h-363.491636v-66.089389h88.119184v-77.104286h-88.119184v-44.059592h88.119184v-77.104287h-88.119184v-44.059592h88.119184v-77.104287h-88.119184v-44.059592h88.119184v-77.104287h-88.119184v-44.059592h88.119184v-77.104287h-88.119184v-66.089388z" fill="#FFFFFF" p-id="2235"></path><path d="M731.917948 225.618157h154.208573v77.104287h-154.208573v-77.104287z" fill="#207245" p-id="2236"></path><path d="M353.44605 328.827752c24.89367-1.762384 49.897488-3.304469 74.901307-4.516108-29.409778 60.251492-58.929705 120.502985-88.879213 180.53418 30.29097 61.683429 61.242833 122.926263 91.632937 184.609692-26.545904-1.553101-52.98166-3.19432-79.527564-5.066854-18.725327-45.932125-41.526166-90.322164-54.964342-138.358134-14.969246 44.731501-36.349164 86.918561-53.521389 130.74684-24.122627-0.330447-48.256268-1.321788-72.367881-2.313129 28.308288-55.404937 55.625235-111.250471 84.814716-166.324961-24.783521-56.726725-52.001334-112.35196-77.544883-168.748238 24.232776-1.431937 48.454537-2.863874 72.698327-4.185662 16.412198 43.068251 34.366482 85.585758 47.925822 129.7555 14.506621-46.802302 36.216985-90.861894 54.832163-136.133126z" fill="#FFFFFF" p-id="2237"></path><path d="M731.917948 346.782036h154.208573v77.104287h-154.208573v-77.104287zM731.917948 467.945915h154.208573v77.104287h-154.208573v-77.104287zM731.917948 589.109794h154.208573v77.104287h-154.208573v-77.104287zM731.917948 710.273673h154.208573v77.104286h-154.208573v-77.104286z" fill="#207245" p-id="2238"></path></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1543300142282" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="2233" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M534.310676 38.36489h65.428495v88.119185c110.25913 0 220.507245 0.220298 330.766374-0.330447 18.615178 0.771043 39.113903-0.550745 55.305803 10.574302 11.345345 16.302049 10.023557 37.010058 10.783586 55.746399-0.550745 191.218631-0.319432 382.327112-0.220298 573.435594-0.550745 32.053353 2.974022 64.767601-3.745066 96.380358-4.405959 22.910988-31.943204 23.461733-50.338084 24.232776-114.114344 0.330447-228.338837-0.220298-342.56333 0v99.134083h-68.402517c-167.867047-30.511268-336.075555-58.709407-504.151885-88.119185v-770.932716c169.0897-29.420793 338.168386-58.389975 507.136922-88.240349z"
fill="#207245" p-id="2234"></path>
<path d="M599.739171 159.528769h363.491636v693.938579h-363.491636v-66.089389h88.119184v-77.104286h-88.119184v-44.059592h88.119184v-77.104287h-88.119184v-44.059592h88.119184v-77.104287h-88.119184v-44.059592h88.119184v-77.104287h-88.119184v-44.059592h88.119184v-77.104287h-88.119184v-66.089388z"
fill="#FFFFFF" p-id="2235"></path>
<path d="M731.917948 225.618157h154.208573v77.104287h-154.208573v-77.104287z" fill="#207245" p-id="2236"></path>
<path d="M353.44605 328.827752c24.89367-1.762384 49.897488-3.304469 74.901307-4.516108-29.409778 60.251492-58.929705 120.502985-88.879213 180.53418 30.29097 61.683429 61.242833 122.926263 91.632937 184.609692-26.545904-1.553101-52.98166-3.19432-79.527564-5.066854-18.725327-45.932125-41.526166-90.322164-54.964342-138.358134-14.969246 44.731501-36.349164 86.918561-53.521389 130.74684-24.122627-0.330447-48.256268-1.321788-72.367881-2.313129 28.308288-55.404937 55.625235-111.250471 84.814716-166.324961-24.783521-56.726725-52.001334-112.35196-77.544883-168.748238 24.232776-1.431937 48.454537-2.863874 72.698327-4.185662 16.412198 43.068251 34.366482 85.585758 47.925822 129.7555 14.506621-46.802302 36.216985-90.861894 54.832163-136.133126z"
fill="#FFFFFF" p-id="2237"></path>
<path d="M731.917948 346.782036h154.208573v77.104287h-154.208573v-77.104287zM731.917948 467.945915h154.208573v77.104287h-154.208573v-77.104287zM731.917948 589.109794h154.208573v77.104287h-154.208573v-77.104287zM731.917948 710.273673h154.208573v77.104286h-154.208573v-77.104286z"
fill="#207245" p-id="2238"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+1 -1
View File
@@ -11,7 +11,7 @@
@REM prepare the variables.
@REM version name
SET VERSION_NAME=tank-3.1.6
SET VERSION_NAME=tank-4.2.0
ECHO VERSION_NAME: %VERSION_NAME%
@REM golang proxy
SET GOPROXY=https://goproxy.cn
+2 -1
View File
@@ -1,4 +1,5 @@
#!/bin/bash
set -e
###########################################################################
#
@@ -9,7 +10,7 @@
#prepare the variables.
# version name
VERSION_NAME=tank-3.1.6
VERSION_NAME=tank-4.2.0
echo "VERSION_NAME: ${VERSION_NAME}"
# golang proxy
GOPROXY=https://goproxy.cn
+379
View File
@@ -0,0 +1,379 @@
#!/bin/bash
#set -e
###########################################################################
#
# Tank cross-compilation build script
# Supports building for multiple platforms and architectures
#
###########################################################################
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Print usage information
usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -v, --version VERSION Version name (default: tank-4.2.0)"
echo " -o, --os OS Target OS: linux, windows, darwin, or 'all'"
echo " -a, --arch ARCH Target architecture: amd64, arm64, 386, or 'all'"
echo " -p, --platform PLATFORM Specific platform (OS/ARCH format, e.g. linux/amd64)"
echo " --list-platforms List all supported platforms"
echo " --clean Clean dist directory before building"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 # Build for current platform"
echo " $0 -o linux -a amd64 # Build for Linux AMD64"
echo " $0 -p linux/arm64 # Build for Linux ARM64"
echo " $0 -o all -a amd64 # Build for all OS with AMD64"
echo " $0 -o all -a all # Build for all supported platforms"
echo " $0 --clean -o all -a all # Clean and build all platforms"
exit 1
}
# List supported platforms
list_platforms() {
echo "Supported platforms:"
echo " linux/amd64 - Linux 64-bit"
echo " linux/arm64 - Linux ARM64"
echo " linux/386 - Linux 32-bit"
echo " windows/amd64 - Windows 64-bit"
echo " windows/arm64 - Windows ARM64"
echo " windows/386 - Windows 32-bit"
echo " darwin/amd64 - macOS Intel"
echo " darwin/arm64 - macOS Apple Silicon"
exit 0
}
# Log functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Default values
VERSION_NAME="tank-4.2.0"
TARGET_OS=""
TARGET_ARCH=""
SPECIFIC_PLATFORM=""
BUILD_ALL_OS=false
BUILD_ALL_ARCH=false
CLEAN_DIST=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-v|--version)
VERSION_NAME="$2"
shift 2
;;
-o|--os)
if [[ "$2" == "all" ]]; then
BUILD_ALL_OS=true
else
TARGET_OS="$2"
fi
shift 2
;;
-a|--arch)
if [[ "$2" == "all" ]]; then
BUILD_ALL_ARCH=true
else
TARGET_ARCH="$2"
fi
shift 2
;;
-p|--platform)
SPECIFIC_PLATFORM="$2"
shift 2
;;
--list-platforms)
list_platforms
;;
--clean)
CLEAN_DIST=true
shift
;;
-h|--help)
usage
;;
*)
log_error "Unknown option: $1"
usage
;;
esac
done
# Parse specific platform
if [[ -n "$SPECIFIC_PLATFORM" ]]; then
if [[ "$SPECIFIC_PLATFORM" =~ ^([^/]+)/([^/]+)$ ]]; then
TARGET_OS="${BASH_REMATCH[1]}"
TARGET_ARCH="${BASH_REMATCH[2]}"
else
log_error "Invalid platform format. Use OS/ARCH format (e.g., linux/amd64)"
exit 1
fi
fi
# Set defaults if nothing specified
if [[ -z "$TARGET_OS" && "$BUILD_ALL_OS" == false ]]; then
TARGET_OS=$(go env GOOS)
fi
if [[ -z "$TARGET_ARCH" && "$BUILD_ALL_ARCH" == false ]]; then
TARGET_ARCH=$(go env GOARCH)
fi
# Define supported platforms as a simple list
SUPPORTED_PLATFORMS="linux/amd64 linux/arm64 linux/386 windows/amd64 windows/arm64 windows/386 darwin/amd64 darwin/arm64"
# Check if platform is supported
is_platform_supported() {
local platform="$1"
echo "$SUPPORTED_PLATFORMS" | grep -q "\b$platform\b"
}
# Validate platform
validate_platform() {
local os=$1
local arch=$2
local platform="${os}/${arch}"
if ! is_platform_supported "$platform"; then
log_warn "Platform ${platform} may not be fully tested"
return 1
fi
log_info "Platform ${platform} supported"
return 0
}
# Setup directories
GOPROXY=https://goproxy.cn
PACK_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
BUILD_DIR=$(dirname ${PACK_DIR})
PROJECT_DIR=$(dirname ${BUILD_DIR})
DIST_DIR=${PROJECT_DIR}/tmp/dist
log_info "=== Tank Cross-Compilation Build ==="
log_info "Version: ${VERSION_NAME}"
log_info "GOPROXY: ${GOPROXY}"
log_info "Project: ${PROJECT_DIR}"
log_info "Dist: ${DIST_DIR}"
# Clean dist directory if requested
if [[ "$CLEAN_DIST" == true ]]; then
log_info "Cleaning dist directory..."
rm -rf ${DIST_DIR}
fi
# Create dist directory
mkdir -p ${DIST_DIR}
# Build function for specific platform
build_platform() {
local os=$1
local arch=$2
local platform="${os}/${arch}"
log_info "Building ${platform}..."
# Validate platform
validate_platform "$os" "$arch"
# Determine binary name and archive format
local binary_name="tank"
local archive_ext="tar.gz"
if [[ "$os" == "windows" ]]; then
binary_name="tank.exe"
archive_ext="zip"
fi
# Setup paths
local file_name="${VERSION_NAME}.${os}-${arch}.${archive_ext}"
local component_dir="${DIST_DIR}/${VERSION_NAME}-${os}-${arch}"
local dist_path="${DIST_DIR}/${file_name}"
log_info " Target: ${platform}"
log_info " Binary: ${binary_name}"
log_info " Output: ${file_name}"
# Clean previous build
if [[ -d "$component_dir" ]]; then
rm -rf "$component_dir"
fi
mkdir -p "$component_dir"
# Change to project directory
cd ${PROJECT_DIR}
# Cross-compile
log_info " Cross-compiling..."
export GOOS="$os"
export GOARCH="$arch"
export GOPROXY="$GOPROXY"
export CGO_ENABLED=0 # Disable CGO for cross-compilation
log_info " Starting go build..." # 添加此行
if ! go build -mod=readonly -ldflags="-s -w" -o "${binary_name}"; then
log_error "Failed to build for ${platform}"
return 1
fi
log_info " go build completed" # 添加此行
# Copy binary
log_info " Copying binary..."
cp "./${binary_name}" "${component_dir}/"
# Copy configuration and static files
log_info " Copying build files..."
if [[ -d "${BUILD_DIR}/conf" ]]; then
cp -r "${BUILD_DIR}/conf" "${component_dir}/"
fi
if [[ -d "${BUILD_DIR}/html" ]]; then
cp -r "${BUILD_DIR}/html" "${component_dir}/"
fi
if [[ -d "${BUILD_DIR}/service" ]]; then
cp -r "${BUILD_DIR}/service" "${component_dir}/"
fi
# Create archive
log_info " Creating archive..."
cd ${DIST_DIR}
if [[ "$archive_ext" == "zip" ]]; then
# Use zip for Windows
if command -v zip >/dev/null 2>&1; then
zip -r "${file_name}" "$(basename "$component_dir")" >/dev/null 2>&1
else
log_warn "zip command not found, using tar.gz instead"
tar -zcf "${VERSION_NAME}.${os}-${arch}.tar.gz" "$(basename "$component_dir")" >/dev/null 2>&1
fi
else
# Use tar.gz for Unix-like systems
tar -zcf "${file_name}" "$(basename "$component_dir")" >/dev/null 2>&1
fi
if [[ $? -ne 0 ]]; then
log_error "Failed to create archive for ${platform}"
return 1
fi
# Cleanup
cd ${PROJECT_DIR}
rm -f "./${binary_name}"
rm -rf "$component_dir"
# Get file size
local file_size=$(ls -lh "${DIST_DIR}/${file_name}" | awk '{print $5}')
log_success "Built ${file_name} (${file_size})"
return 0
}
# Build execution
build_count=0
success_count=0
failed_builds=()
if [[ "$BUILD_ALL_OS" == true && "$BUILD_ALL_ARCH" == true ]]; then
log_info "Building for all supported platforms..."
# Build all supported platforms explicitly
for os in linux windows darwin; do
for arch in amd64 arm64 386; do
platform="${os}/${arch}"
if is_platform_supported "$platform"; then
((build_count++))
if build_platform "$os" "$arch"; then
((success_count++))
else
failed_builds+=("$platform")
fi
fi
done
done
elif [[ "$BUILD_ALL_OS" == true ]]; then
log_info "Building for all OS with architecture: ${TARGET_ARCH}"
for os in linux windows darwin; do
platform="${os}/${TARGET_ARCH}"
if is_platform_supported "$platform"; then
((build_count++))
if build_platform "$os" "$TARGET_ARCH"; then
((success_count++))
else
failed_builds+=("$platform")
fi
fi
done
elif [[ "$BUILD_ALL_ARCH" == true ]]; then
log_info "Building for OS ${TARGET_OS} with all architectures"
for arch in amd64 arm64 386; do
platform="${TARGET_OS}/${arch}"
if is_platform_supported "$platform"; then
((build_count++))
if build_platform "$TARGET_OS" "$arch"; then
((success_count++))
else
failed_builds+=("$platform")
fi
fi
done
else
log_info "Building for ${TARGET_OS}/${TARGET_ARCH}"
((build_count++))
if build_platform "$TARGET_OS" "$TARGET_ARCH"; then
((success_count++))
else
failed_builds+=("${TARGET_OS}/${TARGET_ARCH}")
fi
fi
# Build summary
echo ""
log_info "=== Build Summary ==="
echo "Total builds: ${build_count}"
echo "Successful: ${success_count}"
echo "Failed: $((build_count - success_count))"
if [[ ${#failed_builds[@]} -gt 0 ]]; then
echo ""
log_warn "Failed builds:"
for platform in "${failed_builds[@]}"; do
echo " - $platform"
done
fi
if [[ $success_count -gt 0 ]]; then
echo ""
log_success "Generated files:"
ls -lh ${DIST_DIR}/*.{tar.gz,zip} 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
echo ""
log_info "Files located in: ${DIST_DIR}"
fi
echo ""
if [[ $((build_count - success_count)) -eq 0 ]]; then
log_success "All builds completed successfully!"
exit 0
else
log_error "Some builds failed!"
exit 1
fi
+3 -3
View File
@@ -11,10 +11,10 @@ const (
DEFAULT_SERVER_PORT = 6010
//db table's prefix. tank31_ means current version is tank:3.1.x
TABLE_PREFIX = "tank31_"
//db table's prefix. tank42_ means current version is tank:4.2.x
TABLE_PREFIX = "tank42_"
VERSION = "3.1.6"
VERSION = "4.2.0"
)
type Config interface {
+47 -30
View File
@@ -12,14 +12,16 @@ import (
type AlienController struct {
BaseController
uploadTokenDao *UploadTokenDao
downloadTokenDao *DownloadTokenDao
matterDao *MatterDao
matterService *MatterService
imageCacheDao *ImageCacheDao
imageCacheService *ImageCacheService
alienService *AlienService
shareService *ShareService
uploadTokenDao *UploadTokenDao
downloadTokenDao *DownloadTokenDao
matterDao *MatterDao
spaceDao *SpaceDao
matterService *MatterService
imageCacheDao *ImageCacheDao
imageCacheService *ImageCacheService
alienService *AlienService
shareService *ShareService
spaceMemberService *SpaceMemberService
}
func (this *AlienController) Init() {
@@ -40,6 +42,11 @@ func (this *AlienController) Init() {
this.matterDao = c
}
b = core.CONTEXT.GetBean(this.spaceDao)
if c, ok := b.(*SpaceDao); ok {
this.spaceDao = c
}
b = core.CONTEXT.GetBean(this.matterService)
if c, ok := b.(*MatterService); ok {
this.matterService = c
@@ -64,6 +71,11 @@ func (this *AlienController) Init() {
if c, ok := b.(*ShareService); ok {
this.shareService = c
}
b = core.CONTEXT.GetBean(this.spaceMemberService)
if b, ok := b.(*SpaceMemberService); ok {
this.spaceMemberService = b
}
}
func (this *AlienController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
@@ -80,7 +92,7 @@ func (this *AlienController) RegisterRoutes() map[string]func(writer http.Respon
return routeMap
}
//handle some special routes, eg. params in the url.
// handle some special routes, eg. params in the url.
func (this *AlienController) HandleRoutes(writer http.ResponseWriter, request *http.Request) (func(writer http.ResponseWriter, request *http.Request), bool) {
path := request.URL.Path
@@ -108,7 +120,7 @@ func (this *AlienController) HandleRoutes(writer http.ResponseWriter, request *h
return nil, false
}
//fetch a upload token for guest. Guest can upload file with this token.
// fetch a upload token for guest. Guest can upload file with this token.
func (this *AlienController) FetchUploadToken(writer http.ResponseWriter, request *http.Request) *result.WebResult {
filename := request.FormValue("filename")
@@ -151,7 +163,8 @@ func (this *AlienController) FetchUploadToken(writer http.ResponseWriter, reques
}
user := this.checkUser(request)
dirMatter := this.matterService.CreateDirectories(request, user, dirPath)
space := this.spaceDao.CheckByUuid(user.SpaceUuid)
dirMatter := this.matterService.CreateDirectories(request, user, space, dirPath)
uploadToken := &UploadToken{
UserUuid: user.Uuid,
@@ -170,7 +183,7 @@ func (this *AlienController) FetchUploadToken(writer http.ResponseWriter, reques
}
//user confirm a file whether uploaded successfully.
// user confirm a file whether uploaded successfully.
func (this *AlienController) Confirm(writer http.ResponseWriter, request *http.Request) *result.WebResult {
matterUuid := request.FormValue("matterUuid")
@@ -188,7 +201,7 @@ func (this *AlienController) Confirm(writer http.ResponseWriter, request *http.R
return this.Success(matter)
}
//a guest upload a file with a upload token.
// a guest upload a file with a upload token.
func (this *AlienController) Upload(writer http.ResponseWriter, request *http.Request) *result.WebResult {
//allow cors.
this.allowCORS(writer)
@@ -216,6 +229,7 @@ func (this *AlienController) Upload(writer http.ResponseWriter, request *http.Re
}
user := this.userDao.CheckByUuid(uploadToken.UserUuid)
space := this.spaceDao.CheckByUuid(user.SpaceUuid)
err = request.ParseMultipartForm(32 << 20)
this.PanicError(err)
@@ -228,9 +242,9 @@ func (this *AlienController) Upload(writer http.ResponseWriter, request *http.Re
panic(result.BadRequest("file size doesn't the one in uploadToken"))
}
dirMatter := this.matterDao.CheckWithRootByUuid(uploadToken.FolderUuid, user)
dirMatter := this.matterDao.CheckWithRootByUuid(uploadToken.FolderUuid, space)
matter := this.matterService.Upload(request, file, user, dirMatter, uploadToken.Filename, uploadToken.Privacy)
matter := this.matterService.Upload(request, file, handler, user, space, dirMatter, uploadToken.Filename, uploadToken.Privacy)
//expire the upload token.
uploadToken.ExpireTime = time.Now()
@@ -239,7 +253,7 @@ func (this *AlienController) Upload(writer http.ResponseWriter, request *http.Re
return this.Success(matter)
}
//crawl a url with uploadToken. guest can visit this method.
// crawl a url with uploadToken. guest can visit this method.
func (this *AlienController) CrawlToken(writer http.ResponseWriter, request *http.Request) *result.WebResult {
//allow cors.
@@ -263,10 +277,11 @@ func (this *AlienController) CrawlToken(writer http.ResponseWriter, request *htt
}
user := this.userDao.CheckByUuid(uploadToken.UserUuid)
space := this.spaceDao.CheckByUuid(user.SpaceUuid)
dirMatter := this.matterDao.CheckWithRootByUuid(uploadToken.FolderUuid, user)
dirMatter := this.matterDao.CheckWithRootByUuid(uploadToken.FolderUuid, space)
matter := this.matterService.AtomicCrawl(request, url, uploadToken.Filename, user, dirMatter, uploadToken.Privacy)
matter := this.matterService.AtomicCrawl(request, url, uploadToken.Filename, user, space, dirMatter, uploadToken.Privacy)
//expire the upload token.
uploadToken.ExpireTime = time.Now()
@@ -275,7 +290,7 @@ func (this *AlienController) CrawlToken(writer http.ResponseWriter, request *htt
return this.Success(matter)
}
//crawl a url directly. only user can visit this method.
// crawl a url directly. only user can visit this method.
func (this *AlienController) CrawlDirect(writer http.ResponseWriter, request *http.Request) *result.WebResult {
filename := request.FormValue("filename")
@@ -291,14 +306,15 @@ func (this *AlienController) CrawlDirect(writer http.ResponseWriter, request *ht
}
user := this.checkUser(request)
dirMatter := this.matterService.CreateDirectories(request, user, dirPath)
space := this.spaceDao.CheckByUuid(user.SpaceUuid)
dirMatter := this.matterService.CreateDirectories(request, user, space, dirPath)
matter := this.matterService.AtomicCrawl(request, url, filename, user, dirMatter, privacy)
matter := this.matterService.AtomicCrawl(request, url, filename, user, space, dirMatter, privacy)
return this.Success(matter)
}
//fetch a download token for guest. Guest can download file with this token.
// fetch a download token for guest. Guest can download file with this token.
func (this *AlienController) FetchDownloadToken(writer http.ResponseWriter, request *http.Request) *result.WebResult {
matterUuid := request.FormValue("matterUuid")
@@ -311,8 +327,9 @@ func (this *AlienController) FetchDownloadToken(writer http.ResponseWriter, requ
user := this.checkUser(request)
matter := this.matterDao.CheckByUuid(matterUuid)
if matter.UserUuid != user.Uuid {
panic(result.BadRequest("matter not belong to you"))
canRead := this.spaceMemberService.canRead(user, matter.SpaceUuid)
if !canRead {
panic(result.BadRequest("no auth to visit this file."))
}
var expireTime time.Time
@@ -338,14 +355,14 @@ func (this *AlienController) FetchDownloadToken(writer http.ResponseWriter, requ
}
//preview a file.
// preview a file.
func (this *AlienController) Preview(writer http.ResponseWriter, request *http.Request, uuid string, filename string) {
this.alienService.PreviewOrDownload(writer, request, uuid, filename, false)
matter := this.alienService.ValidMatter(writer, request, uuid, filename)
this.alienService.PreviewOrDownload(writer, request, matter, false)
}
//download a file.
// download a file.
func (this *AlienController) Download(writer http.ResponseWriter, request *http.Request, uuid string, filename string) {
this.alienService.PreviewOrDownload(writer, request, uuid, filename, true)
matter := this.alienService.ValidMatter(writer, request, uuid, filename)
this.alienService.PreviewOrDownload(writer, request, matter, true)
}
+31 -15
View File
@@ -8,7 +8,7 @@ import (
"time"
)
//@Service
// @Service
type AlienService struct {
BaseBean
matterDao *MatterDao
@@ -19,6 +19,7 @@ type AlienService struct {
shareService *ShareService
imageCacheDao *ImageCacheDao
imageCacheService *ImageCacheService
spaceService *SpaceService
}
func (this *AlienService) Init() {
@@ -63,14 +64,18 @@ func (this *AlienService) Init() {
if c, ok := b.(*ImageCacheService); ok {
this.imageCacheService = c
}
b = core.CONTEXT.GetBean(this.spaceService)
if c, ok := b.(*SpaceService); ok {
this.spaceService = c
}
}
func (this *AlienService) PreviewOrDownload(
// check whether the request params ok.
func (this *AlienService) ValidMatter(
writer http.ResponseWriter,
request *http.Request,
uuid string,
filename string,
withContentDisposition bool) {
filename string) *Matter {
matter := this.matterDao.CheckByUuid(uuid)
@@ -94,8 +99,10 @@ func (this *AlienService) PreviewOrDownload(
}
tokenUser := this.userDao.CheckByUuid(downloadToken.UserUuid)
if matter.UserUuid != tokenUser.Uuid {
panic(result.UNAUTHORIZED)
if matter.SpaceUuid != tokenUser.SpaceUuid {
//whether user has the space's read auth.
this.spaceService.CheckReadableByUuid(request, tokenUser, matter.SpaceUuid)
}
//TODO: expire the download token. If download by chunk, do this later.
@@ -104,18 +111,28 @@ func (this *AlienService) PreviewOrDownload(
} else {
//whether this is myself's matter.
operator := this.findUser(request)
if operator == nil {
panic(result.BadRequest("no auth"))
}
//use share code to auth.
shareUuid := request.FormValue("shareUuid")
shareCode := request.FormValue("shareCode")
shareRootUuid := request.FormValue("shareRootUuid")
this.shareService.ValidateMatter(request, shareUuid, shareCode, operator, shareRootUuid, matter)
if matter.SpaceUuid != operator.SpaceUuid {
//whether user has the space's read auth.
this.spaceService.CheckReadableByUuid(request, operator, matter.SpaceUuid)
}
}
}
return matter
}
func (this *AlienService) PreviewOrDownload(
writer http.ResponseWriter,
request *http.Request,
matter *Matter,
withContentDisposition bool,
) {
//download directory
if matter.Dir {
@@ -135,7 +152,7 @@ func (this *AlienService) PreviewOrDownload(
}
//download the cache image file.
this.matterService.DownloadFile(writer, request, GetUserCacheRootDir(imageCache.Username)+imageCache.Path, imageCache.Name, withContentDisposition)
this.matterService.DownloadFile(writer, request, GetSpaceCacheRootDir(imageCache.Username)+imageCache.Path, imageCache.Name, withContentDisposition)
} else {
this.matterService.DownloadFile(writer, request, matter.AbsolutePath(), matter.Name, withContentDisposition)
@@ -145,7 +162,6 @@ func (this *AlienService) PreviewOrDownload(
//async increase the download times.
go core.RunWithRecovery(func() {
this.matterDao.TimesIncrement(uuid)
this.matterDao.TimesIncrement(matter.Uuid)
})
}
+61 -6
View File
@@ -13,8 +13,10 @@ import (
type BaseController struct {
BaseBean
userDao *UserDao
sessionDao *SessionDao
userDao *UserDao
spaceDao *SpaceDao
spaceService *SpaceService
sessionDao *SessionDao
}
func (this *BaseController) Init() {
@@ -26,6 +28,16 @@ func (this *BaseController) Init() {
this.userDao = b
}
b = core.CONTEXT.GetBean(this.spaceDao)
if b, ok := b.(*SpaceDao); ok {
this.spaceDao = b
}
b = core.CONTEXT.GetBean(this.spaceService)
if b, ok := b.(*SpaceService); ok {
this.spaceService = b
}
b = core.CONTEXT.GetBean(this.sessionDao)
if b, ok := b.(*SessionDao); ok {
this.sessionDao = b
@@ -38,12 +50,12 @@ func (this *BaseController) RegisterRoutes() map[string]func(writer http.Respons
return make(map[string]func(writer http.ResponseWriter, request *http.Request))
}
//handle some special routes, eg. params in the url.
// handle some special routes, eg. params in the url.
func (this *BaseController) HandleRoutes(writer http.ResponseWriter, request *http.Request) (func(writer http.ResponseWriter, request *http.Request), bool) {
return nil, false
}
//wrap the handle method.
// wrap the handle method.
func (this *BaseController) Wrap(f func(writer http.ResponseWriter, request *http.Request) *result.WebResult, qualifiedRole string) func(w http.ResponseWriter, r *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {
@@ -87,7 +99,50 @@ func (this *BaseController) Wrap(f func(writer http.ResponseWriter, request *htt
}
}
//response a success result. 1.string 2. WebResult 3.nil pointer 4.any type
// wrap the handle method without result.
func (this *BaseController) WrapPure(f func(writer http.ResponseWriter, request *http.Request), qualifiedRole string) func(w http.ResponseWriter, r *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {
var webResult *result.WebResult = nil
//if the api not annotated with GUEST. login is required.
if qualifiedRole != USER_ROLE_GUEST {
user := this.checkUser(request)
if user.Status == USER_STATUS_DISABLED {
//check user's status
webResult = result.CustomWebResultI18n(request, result.USER_DISABLED, i18n.UserDisabled)
} else {
if qualifiedRole == USER_ROLE_ADMINISTRATOR && user.Role != USER_ROLE_ADMINISTRATOR {
webResult = result.ConstWebResult(result.UNAUTHORIZED)
}
}
}
//if webResult not nil. response a json. if webResult is nil, return empty body or binary content.
if webResult != nil {
writer.Header().Set("Content-Type", "application/json;charset=UTF-8")
b, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(webResult)
this.PanicError(err)
writer.WriteHeader(result.FetchHttpStatus(webResult.Code))
_, err = fmt.Fprintf(writer, string(b))
this.PanicError(err)
}
//no error.
f(writer, request)
}
}
// response a success result. 1.string 2. WebResult 3.nil pointer 4.any type
func (this *BaseController) Success(data interface{}) *result.WebResult {
var webResult *result.WebResult = nil
if value, ok := data.(string); ok {
@@ -106,7 +161,7 @@ func (this *BaseController) Success(data interface{}) *result.WebResult {
return webResult
}
//allow cors.
// allow cors.
func (this *BaseController) allowCORS(writer http.ResponseWriter) {
util.AllowCORS(writer)
}
+161
View File
@@ -0,0 +1,161 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
"net/http"
)
// ChunkController handles chunked upload API endpoints
type ChunkController struct {
BaseController
chunkService *ChunkService
matterDao *MatterDao
uploadSessionDao *UploadSessionDao
uploadChunkDao *UploadChunkDao
}
func (this *ChunkController) Init() {
this.BaseController.Init()
b := core.CONTEXT.GetBean(this.chunkService)
if b, ok := b.(*ChunkService); ok {
this.chunkService = b
}
b = core.CONTEXT.GetBean(this.matterDao)
if b, ok := b.(*MatterDao); ok {
this.matterDao = b
}
b = core.CONTEXT.GetBean(this.uploadSessionDao)
if b, ok := b.(*UploadSessionDao); ok {
this.uploadSessionDao = b
}
b = core.CONTEXT.GetBean(this.uploadChunkDao)
if b, ok := b.(*UploadChunkDao); ok {
this.uploadChunkDao = b
}
}
func (this *ChunkController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
// Chunked upload endpoints
routeMap["/api/chunk/create/session"] = this.Wrap(this.CreateSession, USER_ROLE_USER)
routeMap["/api/chunk/upload"] = this.Wrap(this.Upload, USER_ROLE_USER)
routeMap["/api/chunk/merge"] = this.Wrap(this.Merge, USER_ROLE_USER)
routeMap["/api/chunk/session/info"] = this.Wrap(this.SessionInfo, USER_ROLE_USER)
routeMap["/api/chunk/cancel"] = this.Wrap(this.Cancel, USER_ROLE_USER)
routeMap["/api/chunk/clean/expired"] = this.Wrap(this.CleanExpired, USER_ROLE_ADMINISTRATOR)
return routeMap
}
// CreateSession creates a new chunked upload session
// Parameters:
// - puuid: parent directory uuid
// - filename: target filename
// - totalSize: total file size in bytes
// - chunkSize: size of each chunk (optional, default 5MB)
// - privacy: whether the file is private (optional, default true)
// - fileMd5: MD5 of the whole file (optional, for verification)
// - spaceUuid: space uuid (optional, default user's private space)
func (this *ChunkController) CreateSession(writer http.ResponseWriter, request *http.Request) *result.WebResult {
puuid := util.ExtractRequestString(request, "puuid")
filename := util.ExtractRequestString(request, "filename")
totalSize := util.ExtractRequestInt64(request, "totalSize")
chunkSize := util.ExtractRequestOptionalInt64(request, "chunkSize", DEFAULT_CHUNK_SIZE)
privacy := util.ExtractRequestOptionalBool(request, "privacy", true)
fileMd5 := util.ExtractRequestOptionalString(request, "fileMd5", "")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
dirMatter := this.matterDao.CheckWithRootByUuid(puuid, space)
session := this.chunkService.CreateSession(request, user, space, dirMatter, filename, totalSize, chunkSize, privacy, fileMd5)
return this.Success(session)
}
// Upload uploads a single chunk
// Parameters:
// - sessionUuid: upload session uuid
// - chunkIndex: chunk index (0-based)
// - file: chunk file data
// - chunkMd5: MD5 of the chunk (optional, for verification)
// - spaceUuid: space uuid (optional)
func (this *ChunkController) Upload(writer http.ResponseWriter, request *http.Request) *result.WebResult {
sessionUuid := util.ExtractRequestString(request, "sessionUuid")
chunkIndex := util.ExtractRequestInt(request, "chunkIndex")
chunkMd5 := util.ExtractRequestOptionalString(request, "chunkMd5", "")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
file, _, err := request.FormFile("file")
if err != nil {
panic(result.BadRequest("file is required: %s", err.Error()))
}
defer file.Close()
chunk := this.chunkService.UploadChunk(request, sessionUuid, chunkIndex, file, chunkMd5, user, space)
return this.Success(chunk)
}
// Merge merges all chunks into the final file
// Parameters:
// - sessionUuid: upload session uuid
// - spaceUuid: space uuid (optional)
func (this *ChunkController) Merge(writer http.ResponseWriter, request *http.Request) *result.WebResult {
sessionUuid := util.ExtractRequestString(request, "sessionUuid")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
matter := this.chunkService.MergeChunks(request, sessionUuid, user, space)
return this.Success(matter)
}
// SessionInfo gets the upload session info including uploaded chunks
// Parameters:
// - sessionUuid: upload session uuid
func (this *ChunkController) SessionInfo(writer http.ResponseWriter, request *http.Request) *result.WebResult {
sessionUuid := util.ExtractRequestString(request, "sessionUuid")
user := this.checkUser(request)
sessionInfo := this.chunkService.GetSessionInfo(sessionUuid, user)
return this.Success(sessionInfo)
}
// Cancel cancels an upload session
// Parameters:
// - sessionUuid: upload session uuid
// - spaceUuid: space uuid (optional)
func (this *ChunkController) Cancel(writer http.ResponseWriter, request *http.Request) *result.WebResult {
sessionUuid := util.ExtractRequestString(request, "sessionUuid")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
this.chunkService.CancelSession(sessionUuid, user, space)
return this.Success("OK")
}
// CleanExpired cleans up expired upload sessions (admin only)
func (this *ChunkController) CleanExpired(writer http.ResponseWriter, request *http.Request) *result.WebResult {
this.chunkService.CleanExpiredSessions()
return this.Success("OK")
}
+212
View File
@@ -0,0 +1,212 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/uuid"
"time"
)
// UploadSessionDao handles database operations for UploadSession
type UploadSessionDao struct {
BaseDao
}
// FindByUuid finds an upload session by uuid
func (this *UploadSessionDao) FindByUuid(sessionUuid string) *UploadSession {
var entity = &UploadSession{}
db := core.CONTEXT.GetDB().Where("uuid = ?", sessionUuid).First(entity)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
panic(db.Error)
}
}
return entity
}
// CheckByUuid checks if session exists, panics if not found
func (this *UploadSessionDao) CheckByUuid(sessionUuid string) *UploadSession {
entity := this.FindByUuid(sessionUuid)
if entity == nil {
panic(result.NotFound("upload session not found with uuid = %s", sessionUuid))
}
return entity
}
// FindByUserUuidAndFileMd5 finds session by user and file MD5 (for quick upload)
func (this *UploadSessionDao) FindByUserUuidAndFileMd5(userUuid string, fileMd5 string) *UploadSession {
var entity = &UploadSession{}
db := core.CONTEXT.GetDB().Where("user_uuid = ? AND file_md5 = ? AND status = ?",
userUuid, fileMd5, UPLOAD_SESSION_STATUS_COMPLETED).First(entity)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
panic(db.Error)
}
}
return entity
}
// FindBySpaceUuidAndFileMd5 finds completed session by space and file MD5
func (this *UploadSessionDao) FindBySpaceUuidAndFileMd5(spaceUuid string, fileMd5 string) *UploadSession {
var entity = &UploadSession{}
db := core.CONTEXT.GetDB().Where("space_uuid = ? AND file_md5 = ? AND status = ?",
spaceUuid, fileMd5, UPLOAD_SESSION_STATUS_COMPLETED).First(entity)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
panic(db.Error)
}
}
return entity
}
// Create creates a new upload session
func (this *UploadSessionDao) Create(session *UploadSession) *UploadSession {
timeUUID, _ := uuid.NewV4()
session.Uuid = timeUUID.String()
session.CreateTime = time.Now()
session.UpdateTime = time.Now()
session.Sort = time.Now().UnixNano() / 1e6
db := core.CONTEXT.GetDB().Create(session)
this.PanicError(db.Error)
return session
}
// Save updates an upload session
func (this *UploadSessionDao) Save(session *UploadSession) *UploadSession {
session.UpdateTime = time.Now()
db := core.CONTEXT.GetDB().Save(session)
this.PanicError(db.Error)
return session
}
// Delete deletes an upload session
func (this *UploadSessionDao) Delete(session *UploadSession) {
db := core.CONTEXT.GetDB().Delete(session)
this.PanicError(db.Error)
}
// DeleteByUuid deletes session by uuid
func (this *UploadSessionDao) DeleteByUuid(sessionUuid string) {
db := core.CONTEXT.GetDB().Where("uuid = ?", sessionUuid).Delete(UploadSession{})
this.PanicError(db.Error)
}
// FindExpiredSessions finds all expired sessions
func (this *UploadSessionDao) FindExpiredSessions() []*UploadSession {
var sessions []*UploadSession
db := core.CONTEXT.GetDB().Where("expire_time < ? AND status = ?",
time.Now(), UPLOAD_SESSION_STATUS_UPLOADING).Find(&sessions)
this.PanicError(db.Error)
return sessions
}
// FindByUserUuid finds all sessions for a user
func (this *UploadSessionDao) FindByUserUuid(userUuid string) []*UploadSession {
var sessions []*UploadSession
db := core.CONTEXT.GetDB().Where("user_uuid = ?", userUuid).Find(&sessions)
this.PanicError(db.Error)
return sessions
}
// DeleteByUserUuid deletes all sessions for a user
func (this *UploadSessionDao) DeleteByUserUuid(userUuid string) {
db := core.CONTEXT.GetDB().Where("user_uuid = ?", userUuid).Delete(UploadSession{})
this.PanicError(db.Error)
}
// UploadChunkDao handles database operations for UploadChunk
type UploadChunkDao struct {
BaseDao
}
// FindByUuid finds a chunk by uuid
func (this *UploadChunkDao) FindByUuid(chunkUuid string) *UploadChunk {
var entity = &UploadChunk{}
db := core.CONTEXT.GetDB().Where("uuid = ?", chunkUuid).First(entity)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
panic(db.Error)
}
}
return entity
}
// FindBySessionUuidAndChunkIndex finds a chunk by session and index
func (this *UploadChunkDao) FindBySessionUuidAndChunkIndex(sessionUuid string, chunkIndex int) *UploadChunk {
var entity = &UploadChunk{}
db := core.CONTEXT.GetDB().Where("session_uuid = ? AND chunk_index = ?", sessionUuid, chunkIndex).First(entity)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
panic(db.Error)
}
}
return entity
}
// FindBySessionUuid finds all chunks for a session
func (this *UploadChunkDao) FindBySessionUuid(sessionUuid string) []*UploadChunk {
var chunks []*UploadChunk
db := core.CONTEXT.GetDB().Where("session_uuid = ?", sessionUuid).Order("chunk_index asc").Find(&chunks)
this.PanicError(db.Error)
return chunks
}
// FindUploadedBySessionUuid finds all uploaded chunks for a session
func (this *UploadChunkDao) FindUploadedBySessionUuid(sessionUuid string) []*UploadChunk {
var chunks []*UploadChunk
db := core.CONTEXT.GetDB().Where("session_uuid = ? AND uploaded = ?", sessionUuid, true).Order("chunk_index asc").Find(&chunks)
this.PanicError(db.Error)
return chunks
}
// CountUploadedBySessionUuid counts uploaded chunks for a session
func (this *UploadChunkDao) CountUploadedBySessionUuid(sessionUuid string) int64 {
var count int64
db := core.CONTEXT.GetDB().Model(&UploadChunk{}).Where("session_uuid = ? AND uploaded = ?", sessionUuid, true).Count(&count)
this.PanicError(db.Error)
return count
}
// Create creates a new chunk record
func (this *UploadChunkDao) Create(chunk *UploadChunk) *UploadChunk {
timeUUID, _ := uuid.NewV4()
chunk.Uuid = timeUUID.String()
chunk.CreateTime = time.Now()
chunk.UpdateTime = time.Now()
chunk.Sort = time.Now().UnixNano() / 1e6
db := core.CONTEXT.GetDB().Create(chunk)
this.PanicError(db.Error)
return chunk
}
// Save updates a chunk record
func (this *UploadChunkDao) Save(chunk *UploadChunk) *UploadChunk {
chunk.UpdateTime = time.Now()
db := core.CONTEXT.GetDB().Save(chunk)
this.PanicError(db.Error)
return chunk
}
// Delete deletes a chunk record
func (this *UploadChunkDao) Delete(chunk *UploadChunk) {
db := core.CONTEXT.GetDB().Delete(chunk)
this.PanicError(db.Error)
}
// DeleteBySessionUuid deletes all chunks for a session
func (this *UploadChunkDao) DeleteBySessionUuid(sessionUuid string) {
db := core.CONTEXT.GetDB().Where("session_uuid = ?", sessionUuid).Delete(UploadChunk{})
this.PanicError(db.Error)
}
+76
View File
@@ -0,0 +1,76 @@
package rest
import (
"time"
)
const (
// Upload session status
UPLOAD_SESSION_STATUS_UPLOADING = "UPLOADING"
UPLOAD_SESSION_STATUS_MERGING = "MERGING"
UPLOAD_SESSION_STATUS_COMPLETED = "COMPLETED"
UPLOAD_SESSION_STATUS_ERROR = "ERROR"
// Chunk directory name
CHUNK_DIR = "chunk"
// Default chunk size: 50MB
DEFAULT_CHUNK_SIZE = 50 * 1024 * 1024
// Max chunk size: 100MB
MAX_CHUNK_SIZE = 100 * 1024 * 1024
// Min chunk size: 5MB (aligned with AWS S3 standard)
MIN_CHUNK_SIZE = 5 * 1024 * 1024
)
// UploadSession represents a chunked upload session
type UploadSession struct {
Uuid string `json:"uuid" gorm:"type:char(36);primary_key;unique"`
Sort int64 `json:"sort" gorm:"type:bigint(20) not null"`
UpdateTime time.Time `json:"updateTime" gorm:"type:timestamp not null;default:CURRENT_TIMESTAMP"`
CreateTime time.Time `json:"createTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
UserUuid string `json:"userUuid" gorm:"type:char(36) not null;index:idx_upload_session_uu"`
SpaceUuid string `json:"spaceUuid" gorm:"type:char(36) not null;index:idx_upload_session_su"`
MatterUuid string `json:"matterUuid" gorm:"type:char(36)"` // Final matter uuid after merge
Puuid string `json:"puuid" gorm:"type:char(36) not null"` // Parent directory uuid
Filename string `json:"filename" gorm:"type:varchar(255) not null"` // Target filename
TotalSize int64 `json:"totalSize" gorm:"type:bigint(20) not null;default:0"` // Total file size
ChunkSize int64 `json:"chunkSize" gorm:"type:bigint(20) not null;default:52428800"` // Size of each chunk (default 50MB)
TotalChunks int `json:"totalChunks" gorm:"type:int not null;default:0"` // Total number of chunks
Privacy bool `json:"privacy" gorm:"type:tinyint(1) not null;default:1"` // Privacy flag
FileMd5 string `json:"fileMd5" gorm:"type:varchar(45)"` // MD5 of the whole file
Status string `json:"status" gorm:"type:varchar(20) not null;default:'UPLOADING'"` // Session status
ExpireTime time.Time `json:"expireTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"` // Expiration time
}
// UploadChunk represents a single chunk in an upload session
type UploadChunk struct {
Uuid string `json:"uuid" gorm:"type:char(36);primary_key;unique"`
Sort int64 `json:"sort" gorm:"type:bigint(20) not null"`
UpdateTime time.Time `json:"updateTime" gorm:"type:timestamp not null;default:CURRENT_TIMESTAMP"`
CreateTime time.Time `json:"createTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
SessionUuid string `json:"sessionUuid" gorm:"type:char(36) not null;index:idx_upload_chunk_su"` // Reference to UploadSession
ChunkIndex int `json:"chunkIndex" gorm:"type:int not null"` // Chunk sequence number (0-based)
ChunkSize int64 `json:"chunkSize" gorm:"type:bigint(20) not null;default:0"` // Actual size of this chunk
ChunkMd5 string `json:"chunkMd5" gorm:"type:varchar(45)"` // MD5 of this chunk
ChunkFilename string `json:"chunkFilename" gorm:"type:varchar(255) not null"` // Temp filename for this chunk
Uploaded bool `json:"uploaded" gorm:"type:tinyint(1) not null;default:0"` // Whether this chunk is uploaded
}
// Get the chunk temp directory for a space
func GetSpaceChunkDir(spaceName string) string {
return GetUserSpaceRootDir(spaceName) + "/" + CHUNK_DIR
}
// Get the session chunk directory
func GetSessionChunkDir(spaceName string, sessionUuid string) string {
return GetSpaceChunkDir(spaceName) + "/" + sessionUuid
}
// UploadSessionInfo is the response structure for session queries
type UploadSessionInfo struct {
Session *UploadSession `json:"session"`
UploadedChunks []int `json:"uploadedChunks"` // List of uploaded chunk indices
MissingChunks []int `json:"missingChunks"` // List of missing chunk indices
}
+581
View File
@@ -0,0 +1,581 @@
package rest
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"sort"
"sync"
"time"
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/i18n"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
)
// ChunkService handles chunked upload operations
//@Service
type ChunkService struct {
BaseBean
uploadSessionDao *UploadSessionDao
uploadChunkDao *UploadChunkDao
matterDao *MatterDao
matterService *MatterService
spaceDao *SpaceDao
userService *UserService
// chunkLocker is used to prevent concurrent uploads of the same chunk
chunkLocker sync.Map
// mergeLocker is used to prevent concurrent merge of the same session
mergeLocker sync.Map
}
func (this *ChunkService) Init() {
this.BaseBean.Init()
b := core.CONTEXT.GetBean(this.uploadSessionDao)
if b, ok := b.(*UploadSessionDao); ok {
this.uploadSessionDao = b
}
b = core.CONTEXT.GetBean(this.uploadChunkDao)
if b, ok := b.(*UploadChunkDao); ok {
this.uploadChunkDao = b
}
b = core.CONTEXT.GetBean(this.matterDao)
if b, ok := b.(*MatterDao); ok {
this.matterDao = b
}
b = core.CONTEXT.GetBean(this.matterService)
if b, ok := b.(*MatterService); ok {
this.matterService = b
}
b = core.CONTEXT.GetBean(this.spaceDao)
if b, ok := b.(*SpaceDao); ok {
this.spaceDao = b
}
b = core.CONTEXT.GetBean(this.userService)
if b, ok := b.(*UserService); ok {
this.userService = b
}
}
// CreateSession creates a new upload session
func (this *ChunkService) CreateSession(
request *http.Request,
user *User,
space *Space,
dirMatter *Matter,
filename string,
totalSize int64,
chunkSize int64,
privacy bool,
fileMd5 string,
) *UploadSession {
if user == nil {
panic(result.BadRequest("user cannot be nil."))
}
if dirMatter == nil {
panic(result.BadRequest("dirMatter cannot be nil."))
}
if dirMatter.Deleted {
panic(result.BadRequest("Dir has been deleted. Cannot upload under it."))
}
// Validate filename
if len(filename) > MATTER_NAME_MAX_LENGTH {
panic(result.BadRequestI18n(request, i18n.MatterNameLengthExceedLimit, len(filename), MATTER_NAME_MAX_LENGTH))
}
CheckMatterName(request, filename)
// Validate chunk size
if chunkSize < MIN_CHUNK_SIZE {
chunkSize = MIN_CHUNK_SIZE
}
if chunkSize > MAX_CHUNK_SIZE {
chunkSize = MAX_CHUNK_SIZE
}
// Validate total size
if totalSize <= 0 {
panic(result.BadRequest("totalSize must be positive"))
}
// Check space size limit
if space.SizeLimit >= 0 {
if totalSize > space.SizeLimit {
panic(result.BadRequestI18n(request, i18n.MatterSizeExceedLimit, util.HumanFileSize(totalSize), util.HumanFileSize(space.SizeLimit)))
}
}
// Check total space limit
if space.TotalSizeLimit >= 0 {
if space.TotalSize+totalSize > space.TotalSizeLimit {
panic(result.BadRequestI18n(request, i18n.MatterSizeExceedTotalLimit, util.HumanFileSize(space.TotalSize), util.HumanFileSize(space.TotalSizeLimit)))
}
}
// Check if file already exists
dbMatter := this.matterDao.FindBySpaceUuidAndPuuidAndDirAndName(space.Uuid, dirMatter.Uuid, false, filename)
if dbMatter != nil {
if dbMatter.Deleted {
panic(result.BadRequestI18n(request, i18n.MatterRecycleBinExist, filename))
} else {
panic(result.BadRequestI18n(request, i18n.MatterExist, filename))
}
}
// Calculate total chunks
totalChunks := int((totalSize + chunkSize - 1) / chunkSize)
// Create session
session := &UploadSession{
UserUuid: user.Uuid,
SpaceUuid: space.Uuid,
Puuid: dirMatter.Uuid,
Filename: filename,
TotalSize: totalSize,
ChunkSize: chunkSize,
TotalChunks: totalChunks,
Privacy: privacy,
FileMd5: fileMd5,
Status: UPLOAD_SESSION_STATUS_UPLOADING,
ExpireTime: time.Now().Add(24 * time.Hour), // 24 hours expiration
}
session = this.uploadSessionDao.Create(session)
// Create chunk directory
chunkDir := GetSessionChunkDir(space.Name, session.Uuid)
util.MakeDirAll(chunkDir)
this.logger.Info("Created upload session %s for file %s, total chunks: %d", session.Uuid, filename, totalChunks)
return session
}
// UploadChunk uploads a single chunk with optional MD5 verification
func (this *ChunkService) UploadChunk(
request *http.Request,
sessionUuid string,
chunkIndex int,
file io.Reader,
clientChunkMd5 string,
user *User,
space *Space,
) *UploadChunk {
// Check session
session := this.uploadSessionDao.CheckByUuid(sessionUuid)
// Validate session ownership
if session.UserUuid != user.Uuid {
panic(result.BadRequest("session does not belong to you"))
}
if session.Status != UPLOAD_SESSION_STATUS_UPLOADING {
panic(result.BadRequest("session status is not uploading"))
}
// Check expiration
if time.Now().After(session.ExpireTime) {
panic(result.BadRequest("upload session has expired"))
}
// Validate chunk index
if chunkIndex < 0 || chunkIndex >= session.TotalChunks {
panic(result.BadRequest("invalid chunk index: %d, total chunks: %d", chunkIndex, session.TotalChunks))
}
// Acquire chunk-level lock to prevent concurrent upload of the same chunk
chunkLockKey := fmt.Sprintf("%s_%d", sessionUuid, chunkIndex)
if _, loaded := this.chunkLocker.LoadOrStore(chunkLockKey, true); loaded {
panic(result.BadRequest("chunk %d is being uploaded by another request, please retry later", chunkIndex))
}
defer this.chunkLocker.Delete(chunkLockKey)
// Check if chunk already exists
existingChunk := this.uploadChunkDao.FindBySessionUuidAndChunkIndex(sessionUuid, chunkIndex)
if existingChunk != nil && existingChunk.Uploaded {
this.logger.Info("Chunk %d already uploaded for session %s", chunkIndex, sessionUuid)
return existingChunk
}
// Prepare chunk file path
chunkFilename := fmt.Sprintf("chunk_%d", chunkIndex)
chunkDir := GetSessionChunkDir(space.Name, sessionUuid)
chunkFilePath := chunkDir + "/" + chunkFilename
// Write chunk to disk (use 0600 for security - only owner can read/write)
destFile, err := os.OpenFile(chunkFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
panic(result.BadRequest("failed to create chunk file: %s", err.Error()))
}
// Calculate MD5 while writing
hash := md5.New()
multiWriter := io.MultiWriter(destFile, hash)
chunkSize, err := io.Copy(multiWriter, file)
destFile.Close()
if err != nil {
os.Remove(chunkFilePath)
panic(result.BadRequest("failed to write chunk: %s", err.Error()))
}
// Validate chunk size to prevent malicious oversized uploads
isLastChunk := chunkIndex == session.TotalChunks-1
if isLastChunk {
// Last chunk: should be <= session.ChunkSize and match expected remainder
expectedLastChunkSize := session.TotalSize - int64(session.TotalChunks-1)*session.ChunkSize
if chunkSize != expectedLastChunkSize {
os.Remove(chunkFilePath)
panic(result.BadRequest("last chunk size mismatch: expected %d, got %d", expectedLastChunkSize, chunkSize))
}
} else {
// Non-last chunks: should be exactly session.ChunkSize
if chunkSize != session.ChunkSize {
os.Remove(chunkFilePath)
panic(result.BadRequest("chunk size mismatch: expected %d, got %d", session.ChunkSize, chunkSize))
}
}
chunkMd5 := hex.EncodeToString(hash.Sum(nil))
// Verify chunk MD5 if client provided one
if clientChunkMd5 != "" && clientChunkMd5 != chunkMd5 {
os.Remove(chunkFilePath)
this.logger.Error("Chunk MD5 verification failed for chunk %d: client=%s, server=%s", chunkIndex, clientChunkMd5, chunkMd5)
panic(result.BadRequest("chunk MD5 verification failed: data may be corrupted during upload"))
}
this.logger.Info("Uploaded chunk %d for session %s, size: %d, md5: %s", chunkIndex, sessionUuid, chunkSize, chunkMd5)
// Create or update chunk record
var chunk *UploadChunk
if existingChunk != nil {
existingChunk.ChunkSize = chunkSize
existingChunk.ChunkMd5 = chunkMd5
existingChunk.ChunkFilename = chunkFilename
existingChunk.Uploaded = true
chunk = this.uploadChunkDao.Save(existingChunk)
} else {
chunk = &UploadChunk{
SessionUuid: sessionUuid,
ChunkIndex: chunkIndex,
ChunkSize: chunkSize,
ChunkMd5: chunkMd5,
ChunkFilename: chunkFilename,
Uploaded: true,
}
chunk = this.uploadChunkDao.Create(chunk)
}
return chunk
}
// GetSessionInfo gets the upload session info including uploaded chunks
func (this *ChunkService) GetSessionInfo(sessionUuid string, user *User) *UploadSessionInfo {
session := this.uploadSessionDao.CheckByUuid(sessionUuid)
// Validate ownership
if session.UserUuid != user.Uuid {
panic(result.BadRequest("session does not belong to you"))
}
// Get uploaded chunks
uploadedChunks := this.uploadChunkDao.FindUploadedBySessionUuid(sessionUuid)
uploadedIndices := make([]int, 0)
uploadedMap := make(map[int]bool)
for _, chunk := range uploadedChunks {
uploadedIndices = append(uploadedIndices, chunk.ChunkIndex)
uploadedMap[chunk.ChunkIndex] = true
}
// Calculate missing chunks
missingIndices := make([]int, 0)
for i := 0; i < session.TotalChunks; i++ {
if !uploadedMap[i] {
missingIndices = append(missingIndices, i)
}
}
return &UploadSessionInfo{
Session: session,
UploadedChunks: uploadedIndices,
MissingChunks: missingIndices,
}
}
// MergeChunks merges all chunks into final file
func (this *ChunkService) MergeChunks(
request *http.Request,
sessionUuid string,
user *User,
space *Space,
) *Matter {
// Use session-level lock instead of user-level lock to allow concurrent uploads of different files
if _, loaded := this.mergeLocker.LoadOrStore(sessionUuid, true); loaded {
panic(result.BadRequest("merge operation is already in progress for this session"))
}
defer this.mergeLocker.Delete(sessionUuid)
session := this.uploadSessionDao.CheckByUuid(sessionUuid)
// Validate ownership
if session.UserUuid != user.Uuid {
panic(result.BadRequest("session does not belong to you"))
}
// Check session expiration
if time.Now().After(session.ExpireTime) {
panic(result.BadRequest("upload session has expired, please start a new upload"))
}
if session.Status == UPLOAD_SESSION_STATUS_COMPLETED {
// Already completed, return the matter
if session.MatterUuid != "" {
return this.matterDao.CheckByUuid(session.MatterUuid)
}
panic(result.BadRequest("session completed but matter not found"))
}
// Allow retry from ERROR status (e.g., previous merge failed)
if session.Status != UPLOAD_SESSION_STATUS_UPLOADING && session.Status != UPLOAD_SESSION_STATUS_ERROR {
panic(result.BadRequest("session status is not uploading or error, current: %s", session.Status))
}
// If retrying from ERROR status, reset to UPLOADING first
if session.Status == UPLOAD_SESSION_STATUS_ERROR {
this.logger.Info("Retrying merge for session %s from ERROR status", sessionUuid)
session.Status = UPLOAD_SESSION_STATUS_UPLOADING
this.uploadSessionDao.Save(session)
}
// Check if all chunks are uploaded
uploadedCount := this.uploadChunkDao.CountUploadedBySessionUuid(sessionUuid)
if int(uploadedCount) < session.TotalChunks {
panic(result.BadRequest("not all chunks uploaded: %d/%d", uploadedCount, session.TotalChunks))
}
// Update status to merging
session.Status = UPLOAD_SESSION_STATUS_MERGING
this.uploadSessionDao.Save(session)
// Get parent directory
var dirMatter *Matter
if session.Puuid == MATTER_ROOT {
dirMatter = NewRootMatter(space)
} else {
dirMatter = this.matterDao.CheckByUuid(session.Puuid)
}
// Prepare final file path
dirAbsolutePath := dirMatter.AbsolutePath()
util.MakeDirAll(dirAbsolutePath)
fileAbsolutePath := dirAbsolutePath + "/" + session.Filename
// Check if file exists
if util.PathExists(fileAbsolutePath) {
this.logger.Error("%s exists, removing it.", fileAbsolutePath)
os.Remove(fileAbsolutePath)
}
// Create final file (use 0644 for normal file permissions)
destFile, err := os.OpenFile(fileAbsolutePath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
session.Status = UPLOAD_SESSION_STATUS_ERROR
this.uploadSessionDao.Save(session)
panic(result.BadRequest("failed to create destination file: %s", err.Error()))
}
// Get all chunks sorted by index
chunks := this.uploadChunkDao.FindBySessionUuid(sessionUuid)
sort.Slice(chunks, func(i, j int) bool {
return chunks[i].ChunkIndex < chunks[j].ChunkIndex
})
chunkDir := GetSessionChunkDir(space.Name, sessionUuid)
// Merge chunks
var totalWritten int64 = 0
for _, chunk := range chunks {
chunkPath := chunkDir + "/" + chunk.ChunkFilename
chunkFile, err := os.Open(chunkPath)
if err != nil {
destFile.Close()
os.Remove(fileAbsolutePath)
session.Status = UPLOAD_SESSION_STATUS_ERROR
this.uploadSessionDao.Save(session)
panic(result.BadRequest("failed to open chunk %d: %s", chunk.ChunkIndex, err.Error()))
}
written, err := io.Copy(destFile, chunkFile)
chunkFile.Close()
if err != nil {
destFile.Close()
os.Remove(fileAbsolutePath)
session.Status = UPLOAD_SESSION_STATUS_ERROR
this.uploadSessionDao.Save(session)
panic(result.BadRequest("failed to write chunk %d: %s", chunk.ChunkIndex, err.Error()))
}
totalWritten += written
}
destFile.Close()
this.logger.Info("Merged %d chunks into file %s, total size: %d", len(chunks), session.Filename, totalWritten)
// Verify file size
if totalWritten != session.TotalSize {
os.Remove(fileAbsolutePath)
session.Status = UPLOAD_SESSION_STATUS_ERROR
this.uploadSessionDao.Save(session)
panic(result.BadRequest("merged file size mismatch: expected %d, got %d", session.TotalSize, totalWritten))
}
// Verify file MD5 if provided
if session.FileMd5 != "" {
mergedFile, err := os.Open(fileAbsolutePath)
if err != nil {
os.Remove(fileAbsolutePath)
session.Status = UPLOAD_SESSION_STATUS_ERROR
this.uploadSessionDao.Save(session)
panic(result.BadRequest("failed to open merged file for MD5 verification: %s", err.Error()))
}
hash := md5.New()
_, err = io.Copy(hash, mergedFile)
mergedFile.Close()
if err != nil {
os.Remove(fileAbsolutePath)
session.Status = UPLOAD_SESSION_STATUS_ERROR
this.uploadSessionDao.Save(session)
panic(result.BadRequest("failed to calculate MD5 of merged file: %s", err.Error()))
}
calculatedMd5 := hex.EncodeToString(hash.Sum(nil))
if calculatedMd5 != session.FileMd5 {
os.Remove(fileAbsolutePath)
session.Status = UPLOAD_SESSION_STATUS_ERROR
this.uploadSessionDao.Save(session)
this.logger.Error("MD5 verification failed for session %s: expected %s, got %s", sessionUuid, session.FileMd5, calculatedMd5)
panic(result.BadRequest("MD5 verification failed: file may be corrupted during upload"))
}
this.logger.Info("MD5 verification passed for session %s", sessionUuid)
}
// Create matter record
dirRelativePath := dirMatter.Path
fileRelativePath := dirRelativePath + "/" + session.Filename
matter := &Matter{
Puuid: dirMatter.Uuid,
UserUuid: user.Uuid,
SpaceName: space.Name,
SpaceUuid: space.Uuid,
Dir: false,
Name: session.Filename,
Md5: session.FileMd5,
Size: totalWritten,
Privacy: session.Privacy,
Path: fileRelativePath,
Prop: EMPTY_JSON_MAP,
VisitTime: time.Now(),
}
matter = this.matterDao.Create(matter)
// Update session status
session.Status = UPLOAD_SESSION_STATUS_COMPLETED
session.MatterUuid = matter.Uuid
this.uploadSessionDao.Save(session)
// Clean up chunk files
go core.RunWithRecovery(func() {
this.cleanupChunks(space.Name, sessionUuid)
this.matterService.ComputeRouteSize(dirMatter.Uuid, user, space)
})
this.logger.Info("Upload session %s completed, matter uuid: %s", sessionUuid, matter.Uuid)
return matter
}
// cleanupChunks removes chunk files and records with improved atomicity
// First delete database records, then delete files (safer approach)
func (this *ChunkService) cleanupChunks(spaceName string, sessionUuid string) {
chunkDir := GetSessionChunkDir(spaceName, sessionUuid)
// First, remove chunk records from database
// This ensures that even if file deletion fails, we don't have orphaned DB records
this.uploadChunkDao.DeleteBySessionUuid(sessionUuid)
// Then remove chunk directory with retry mechanism
maxRetries := 3
for i := 0; i < maxRetries; i++ {
err := os.RemoveAll(chunkDir)
if err == nil {
this.logger.Info("Cleaned up chunks for session %s", sessionUuid)
return
}
this.logger.Error("failed to remove chunk dir %s (attempt %d/%d): %s", chunkDir, i+1, maxRetries, err.Error())
// Wait before retry (exponential backoff)
if i < maxRetries-1 {
time.Sleep(time.Duration(100*(i+1)) * time.Millisecond)
}
}
// If all retries failed, log a critical warning
this.logger.Error("CRITICAL: Failed to remove chunk dir %s after %d attempts. Manual cleanup may be required.", chunkDir, maxRetries)
}
// CancelSession cancels an upload session
func (this *ChunkService) CancelSession(sessionUuid string, user *User, space *Space) {
session := this.uploadSessionDao.CheckByUuid(sessionUuid)
// Validate ownership
if session.UserUuid != user.Uuid {
panic(result.BadRequest("session does not belong to you"))
}
// Clean up
this.cleanupChunks(space.Name, sessionUuid)
// Delete session
this.uploadSessionDao.Delete(session)
this.logger.Info("Cancelled upload session %s", sessionUuid)
}
// CleanExpiredSessions cleans up expired sessions
func (this *ChunkService) CleanExpiredSessions() {
sessions := this.uploadSessionDao.FindExpiredSessions()
for _, session := range sessions {
space := this.spaceDao.FindByUuid(session.SpaceUuid)
if space != nil {
this.cleanupChunks(space.Name, session.Uuid)
}
this.uploadSessionDao.Delete(session)
this.logger.Info("Cleaned expired session %s", session.Uuid)
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ type Dashboard struct {
FileSize int64 `json:"fileSize" gorm:"type:bigint(20) not null;default:0"` //today's file size
TotalFileSize int64 `json:"totalFileSize" gorm:"type:bigint(20) not null;default:0"` //total file's size
AvgCost int64 `json:"avgCost" gorm:"type:bigint(20) not null;default:0"` //api time cost in ms
Dt string `json:"dt" gorm:"type:varchar(45) not null;index:idx_dashboard_dt"` //date. index should unique globally.
Dt string `json:"dt" gorm:"type:varchar(45) not null;index:idx_dashboard_dt"` //date. index should unique globally for sqlite.
}
/**
+16 -3
View File
@@ -25,6 +25,7 @@ type DavController struct {
BaseController
uploadTokenDao *UploadTokenDao
downloadTokenDao *DownloadTokenDao
spaceDao *SpaceDao
matterDao *MatterDao
matterService *MatterService
imageCacheDao *ImageCacheDao
@@ -50,6 +51,11 @@ func (this *DavController) Init() {
this.matterDao = c
}
b = core.CONTEXT.GetBean(this.spaceDao)
if c, ok := b.(*SpaceDao); ok {
this.spaceDao = c
}
b = core.CONTEXT.GetBean(this.matterService)
if c, ok := b.(*MatterService); ok {
this.matterService = c
@@ -71,7 +77,7 @@ func (this *DavController) Init() {
}
}
//Auth user by BasicAuth
// Auth user by BasicAuth
func (this *DavController) CheckCurrentUser(writer http.ResponseWriter, request *http.Request) *User {
username, password, ok := request.BasicAuth()
@@ -100,7 +106,7 @@ func (this *DavController) RegisterRoutes() map[string]func(writer http.Response
return routeMap
}
//handle some special routes, eg. params in the url.
// handle some special routes, eg. params in the url.
func (this *DavController) HandleRoutes(writer http.ResponseWriter, request *http.Request) (func(writer http.ResponseWriter, request *http.Request), bool) {
path := request.URL.Path
@@ -111,10 +117,16 @@ func (this *DavController) HandleRoutes(writer http.ResponseWriter, request *htt
strs := reg.FindStringSubmatch(path)
if len(strs) == 2 {
var f = func(writer http.ResponseWriter, request *http.Request) {
if err := recover(); err != nil {
this.logger.Error("occur error in webdav: %v", err)
}
subPath := strs[1]
//guarantee subPath not end with /
subPath = strings.TrimSuffix(subPath, "/")
this.Index(writer, request, subPath)
}
return f, true
}
@@ -166,7 +178,8 @@ func (this *DavController) Index(writer http.ResponseWriter, request *http.Reque
//this.debug(writer, request, subPath)
user := this.CheckCurrentUser(writer, request)
space := this.spaceDao.CheckByUuid(user.SpaceUuid)
this.davService.HandleDav(writer, request, user, subPath)
this.davService.HandleDav(writer, request, user, space, subPath)
}
+17 -17
View File
@@ -9,19 +9,19 @@ import (
"strconv"
)
//webdav url prefix.
// webdav url prefix.
var WEBDAV_PREFIX = "/api/dav"
//live prop.
// live prop.
type LiveProp struct {
findFn func(user *User, matter *Matter) string
findFn func(space *Space, matter *Matter) string
dir bool
}
//all live prop map.
// all live prop map.
var LivePropMap = map[xml.Name]LiveProp{
{Space: "DAV:", Local: "resourcetype"}: {
findFn: func(user *User, matter *Matter) string {
findFn: func(space *Space, matter *Matter) string {
if matter.Dir {
return `<D:collection xmlns:D="DAV:"/>`
} else {
@@ -31,7 +31,7 @@ var LivePropMap = map[xml.Name]LiveProp{
dir: true,
},
{Space: "DAV:", Local: "displayname"}: {
findFn: func(user *User, matter *Matter) string {
findFn: func(space *Space, matter *Matter) string {
if path.Clean("/"+matter.Name) == "/" {
return ""
} else {
@@ -41,13 +41,13 @@ var LivePropMap = map[xml.Name]LiveProp{
dir: true,
},
{Space: "DAV:", Local: "getcontentlength"}: {
findFn: func(user *User, matter *Matter) string {
findFn: func(space *Space, matter *Matter) string {
return strconv.FormatInt(matter.Size, 10)
},
dir: false,
},
{Space: "DAV:", Local: "getlastmodified"}: {
findFn: func(user *User, matter *Matter) string {
findFn: func(space *Space, matter *Matter) string {
return matter.UpdateTime.UTC().Format(http.TimeFormat)
},
// http://webdav.org/specs/rfc4918.html#PROPERTY_getlastmodified
@@ -68,7 +68,7 @@ var LivePropMap = map[xml.Name]LiveProp{
dir: false,
},
{Space: "DAV:", Local: "getcontenttype"}: {
findFn: func(user *User, matter *Matter) string {
findFn: func(space *Space, matter *Matter) string {
if matter.Dir {
return ""
} else {
@@ -78,7 +78,7 @@ var LivePropMap = map[xml.Name]LiveProp{
dir: false,
},
{Space: "DAV:", Local: "getetag"}: {
findFn: func(user *User, matter *Matter) string {
findFn: func(space *Space, matter *Matter) string {
return fmt.Sprintf(`"%x%x"`, matter.UpdateTime.UnixNano(), matter.Size)
},
// findETag implements ETag as the concatenated hex values of a file's
@@ -91,7 +91,7 @@ var LivePropMap = map[xml.Name]LiveProp{
// active locks on a resource.
{Space: "DAV:", Local: "lockdiscovery"}: {},
{Space: "DAV:", Local: "supportedlock"}: {
findFn: func(user *User, matter *Matter) string {
findFn: func(space *Space, matter *Matter) string {
return `` +
`<D:lockentry xmlns:D="DAV:">` +
`<D:lockscope><D:exclusive/></D:lockscope>` +
@@ -101,11 +101,11 @@ var LivePropMap = map[xml.Name]LiveProp{
dir: true,
},
{Space: "DAV:", Local: "quota-available-bytes"}: {
findFn: func(user *User, matter *Matter) string {
findFn: func(space *Space, matter *Matter) string {
var size int64 = 0
if user.TotalSizeLimit >= 0 {
if user.TotalSizeLimit-user.TotalSize > 0 {
size = user.TotalSizeLimit - user.TotalSize
if space.TotalSizeLimit >= 0 {
if space.TotalSizeLimit-space.TotalSize > 0 {
size = space.TotalSizeLimit - space.TotalSize
} else {
size = 0
}
@@ -118,8 +118,8 @@ var LivePropMap = map[xml.Name]LiveProp{
dir: true,
},
{Space: "DAV:", Local: "quota-used-bytes"}: {
findFn: func(user *User, matter *Matter) string {
return fmt.Sprintf(`%d`, user.TotalSize)
findFn: func(space *Space, matter *Matter) string {
return fmt.Sprintf(`%d`, space.TotalSize)
},
dir: true,
},
+60 -60
View File
@@ -49,7 +49,7 @@ func (this *DavService) Init() {
this.lockSystem = webdav.NewMemLS()
}
//get the depth in header. Not support infinity yet.
// get the depth in header. Not support infinity yet.
func (this *DavService) ParseDepth(request *http.Request) int {
depth := 1
@@ -88,8 +88,8 @@ func (this *DavService) makePropstatResponse(href string, pstats []dav.Propstat)
return &resp
}
//fetch a matter's []dav.Propstat
func (this *DavService) PropstatsFromXmlNames(user *User, matter *Matter, xmlNames []xml.Name) []dav.Propstat {
// fetch a matter's []dav.Propstat
func (this *DavService) PropstatsFromXmlNames(user *User, space *Space, matter *Matter, xmlNames []xml.Name) []dav.Propstat {
propstats := make([]dav.Propstat, 0)
@@ -101,7 +101,7 @@ func (this *DavService) PropstatsFromXmlNames(user *User, matter *Matter, xmlNam
// Otherwise, it must either be a live property or we don't know it.
if liveProp := LivePropMap[xmlName]; liveProp.findFn != nil && (liveProp.dir || !matter.Dir) {
innerXML := liveProp.findFn(user, matter)
innerXML := liveProp.findFn(space, matter)
okProperties = append(okProperties, dav.Property{
XMLName: xmlName,
@@ -162,7 +162,7 @@ func (this *DavService) AllPropXmlNames(matter *Matter) []xml.Name {
return pnames
}
func (this *DavService) Propstats(user *User, matter *Matter, propfind *dav.Propfind) []dav.Propstat {
func (this *DavService) Propstats(user *User, space *Space, matter *Matter, propfind *dav.Propfind) []dav.Propstat {
propstats := make([]dav.Propstat, 0)
if propfind.Propname != nil {
@@ -172,18 +172,18 @@ func (this *DavService) Propstats(user *User, matter *Matter, propfind *dav.Prop
//TODO: if include other things. add to it.
xmlNames := this.AllPropXmlNames(matter)
propstats = this.PropstatsFromXmlNames(user, matter, xmlNames)
propstats = this.PropstatsFromXmlNames(user, space, matter, xmlNames)
} else {
propstats = this.PropstatsFromXmlNames(user, matter, propfind.Prop)
propstats = this.PropstatsFromXmlNames(user, space, matter, propfind.Prop)
}
return propstats
}
//list the directory.
func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
// list the directory.
func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http.Request, user *User, space *Space, subPath string) {
fmt.Printf("PROPFIND %s\n", subPath)
@@ -193,7 +193,7 @@ func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http
propfind := dav.ReadPropfind(request.Body)
//find the matter, if subPath is null, means the root directory.
matter := this.matterDao.CheckWithRootByPath(subPath, user)
matter := this.matterDao.CheckWithRootByPath(subPath, user, space)
var matters []*Matter
if depth == 0 {
@@ -213,7 +213,7 @@ func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http
fmt.Printf("handle Matter %s\n", matter.Path)
propstats := this.Propstats(user, matter, propfind)
propstats := this.Propstats(user, space, matter, propfind)
visitPath := fmt.Sprintf("%s%s", WEBDAV_PREFIX, matter.Path)
response := this.makePropstatResponse(visitPath, propstats)
@@ -228,7 +228,7 @@ func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http
}
//change the file's property
// change the file's property
func (this *DavService) HandleProppatch(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
fmt.Printf("PROPPATCH %s\n", subPath)
@@ -251,7 +251,7 @@ func (this *DavService) HandleProppatch(writer http.ResponseWriter, request *htt
patches, status, err := webdav.ReadProppatch(request.Body)
this.PanicError(err)
fmt.Println("status:%v", status)
fmt.Printf("status:%v\n", status)
//prepare a multiStatusWriter.
multiStatusWriter := &dav.MultiStatusWriter{Writer: writer}
@@ -296,16 +296,16 @@ func (this *DavService) HandleProppatch(writer http.ResponseWriter, request *htt
}
//handle download
func (this *DavService) HandleGetHeadPost(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
// handle download
func (this *DavService) HandleGetHeadPost(writer http.ResponseWriter, request *http.Request, user *User, space *Space, subPath string) {
fmt.Printf("GET %s\n", subPath)
matter := this.matterDao.CheckWithRootByPath(subPath, user)
matter := this.matterDao.CheckWithRootByPath(subPath, user, space)
//if this is a Directory, it means Propfind
if matter.Dir {
this.HandlePropfind(writer, request, user, subPath)
this.HandlePropfind(writer, request, user, space, subPath)
return
}
@@ -314,8 +314,8 @@ func (this *DavService) HandleGetHeadPost(writer http.ResponseWriter, request *h
}
//upload a file
func (this *DavService) HandlePut(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
// upload a file
func (this *DavService) HandlePut(writer http.ResponseWriter, request *http.Request, user *User, space *Space, subPath string) {
fmt.Printf("PUT %s\n", subPath)
@@ -339,23 +339,23 @@ func (this *DavService) HandlePut(writer http.ResponseWriter, request *http.Requ
filename := util.GetFilenameOfPath(subPath)
dirPath := util.GetDirOfPath(subPath)
dirMatter := this.matterDao.CheckWithRootByPath(dirPath, user)
dirMatter := this.matterDao.CheckWithRootByPath(dirPath, user, space)
//if exist delete it.
srcMatter := this.matterDao.findByUserUuidAndPath(user.Uuid, subPath)
if srcMatter != nil {
this.matterService.AtomicDelete(request, srcMatter, user)
this.matterService.AtomicDelete(request, srcMatter, user, space)
}
this.matterService.Upload(request, request.Body, user, dirMatter, filename, true)
this.matterService.Upload(request, request.Body, nil, user, space, dirMatter, filename, true)
//set the status code 201
writer.WriteHeader(http.StatusCreated)
}
//delete file
func (this *DavService) HandleDelete(w http.ResponseWriter, r *http.Request, user *User, subPath string) {
// delete file
func (this *DavService) HandleDelete(w http.ResponseWriter, r *http.Request, user *User, space *Space, subPath string) {
fmt.Printf("DELETE %s\n", subPath)
@@ -371,13 +371,13 @@ func (this *DavService) HandleDelete(w http.ResponseWriter, r *http.Request, use
defer release()
}
matter := this.matterDao.CheckWithRootByPath(subPath, user)
matter := this.matterDao.CheckWithRootByPath(subPath, user, space)
this.matterService.AtomicDelete(r, matter, user)
this.matterService.AtomicDelete(r, matter, user, space)
}
//crate a directory
func (this *DavService) HandleMkcol(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
// crate a directory
func (this *DavService) HandleMkcol(writer http.ResponseWriter, request *http.Request, user *User, space *Space, subPath string) {
fmt.Printf("MKCOL %s\n", subPath)
@@ -395,34 +395,34 @@ func (this *DavService) HandleMkcol(writer http.ResponseWriter, request *http.Re
thisDirName := util.GetFilenameOfPath(subPath)
dirPath := util.GetDirOfPath(subPath)
dirMatter := this.matterDao.FindWithRootByPath(dirPath, user)
dirMatter := this.matterDao.FindWithRootByPath(dirPath, user, space)
if dirMatter == nil {
//throw conflict error
panic(result.CustomWebResult(result.CONFLICT, fmt.Sprintf("%s not exist", dirPath)))
}
//check whether col exists. (RFC2518:8.3.1)
dbMatter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, dirMatter.Uuid, TRUE, thisDirName)
dbMatter := this.matterDao.FindBySpaceNameAndPuuidAndDirAndName(space.Name, dirMatter.Uuid, TRUE, thisDirName)
if dbMatter != nil {
panic(result.CustomWebResult(result.METHOD_NOT_ALLOWED, fmt.Sprintf("%s already exists", dirPath)))
}
//check whether file exists. (RFC2518:8.3.1)
fileMatter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, dirMatter.Uuid, FALSE, thisDirName)
fileMatter := this.matterDao.FindBySpaceNameAndPuuidAndDirAndName(space.Name, dirMatter.Uuid, FALSE, thisDirName)
if fileMatter != nil {
panic(result.CustomWebResult(result.METHOD_NOT_ALLOWED, fmt.Sprintf("%s file already exists", dirPath)))
}
this.matterService.AtomicCreateDirectory(request, dirMatter, thisDirName, user)
this.matterService.AtomicCreateDirectory(request, dirMatter, thisDirName, user, space)
}
//cors options
func (this *DavService) HandleOptions(w http.ResponseWriter, r *http.Request, user *User, subPath string) {
// cors options
func (this *DavService) HandleOptions(w http.ResponseWriter, r *http.Request, user *User, space *Space, subPath string) {
fmt.Printf("OPTIONS %s\n", subPath)
matter := this.matterDao.CheckWithRootByPath(subPath, user)
matter := this.matterDao.CheckWithRootByPath(subPath, user, space)
allow := "OPTIONS, LOCK, PUT, MKCOL"
if matter.Dir {
@@ -439,11 +439,11 @@ func (this *DavService) HandleOptions(w http.ResponseWriter, r *http.Request, us
}
//prepare for moving or copying
// prepare for moving or copying
func (this *DavService) prepareMoveCopy(
writer http.ResponseWriter,
request *http.Request,
user *User, subPath string) (
user *User, space *Space, subPath string) (
srcMatter *Matter,
destDirMatter *Matter,
srcDirPath string,
@@ -506,14 +506,14 @@ func (this *DavService) prepareMoveCopy(
}
//source matter
srcMatter = this.matterDao.CheckWithRootByPath(subPath, user)
srcMatter = this.matterDao.CheckWithRootByPath(subPath, user, space)
//if source matter is root.
if srcMatter.Uuid == MATTER_ROOT {
panic(result.BadRequest("you cannot move the root directory"))
}
destDirMatter = this.matterDao.FindWithRootByPath(destinationDirPath, user)
destDirMatter = this.matterDao.FindWithRootByPath(destinationDirPath, user, space)
if destDirMatter == nil {
//throw conflict error
panic(result.CustomWebResult(result.CONFLICT, fmt.Sprintf("%s not exist", destinationDirPath)))
@@ -523,8 +523,8 @@ func (this *DavService) prepareMoveCopy(
}
//move or rename.
func (this *DavService) HandleMove(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
// move or rename.
func (this *DavService) HandleMove(writer http.ResponseWriter, request *http.Request, user *User, space *Space, subPath string) {
fmt.Printf("MOVE %s\n", subPath)
@@ -541,14 +541,14 @@ func (this *DavService) HandleMove(writer http.ResponseWriter, request *http.Req
defer release()
}
srcMatter, destDirMatter, srcDirPath, destinationDirPath, destinationName, overwrite := this.prepareMoveCopy(writer, request, user, subPath)
srcMatter, destDirMatter, srcDirPath, destinationDirPath, destinationName, overwrite := this.prepareMoveCopy(writer, request, user, space, subPath)
//move to the new directory
if destinationDirPath == srcDirPath {
//if destination path not change. it means rename.
this.matterService.AtomicRename(request, srcMatter, destinationName, overwrite, user)
this.matterService.AtomicRename(request, srcMatter, destinationName, overwrite, user, space)
} else {
this.matterService.AtomicMove(request, srcMatter, destDirMatter, overwrite, user)
this.matterService.AtomicMove(request, srcMatter, destDirMatter, overwrite, user, space)
}
this.logger.Info("finish moving %s => %s", subPath, destDirMatter.Path)
@@ -562,12 +562,12 @@ func (this *DavService) HandleMove(writer http.ResponseWriter, request *http.Req
}
}
//copy file/directory
func (this *DavService) HandleCopy(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
// copy file/directory
func (this *DavService) HandleCopy(writer http.ResponseWriter, request *http.Request, user *User, space *Space, subPath string) {
fmt.Printf("COPY %s\n", subPath)
srcMatter, destDirMatter, _, _, destinationName, overwrite := this.prepareMoveCopy(writer, request, user, subPath)
srcMatter, destDirMatter, _, _, destinationName, overwrite := this.prepareMoveCopy(writer, request, user, space, subPath)
// handle the lock feature.
release, status, err := this.confirmLocks(request, destDirMatter.Path+"/"+destinationName, "")
@@ -579,7 +579,7 @@ func (this *DavService) HandleCopy(writer http.ResponseWriter, request *http.Req
}
//copy to the new directory
this.matterService.AtomicCopy(request, srcMatter, destDirMatter, destinationName, overwrite, user)
this.matterService.AtomicCopy(request, srcMatter, destDirMatter, destinationName, overwrite, user, space)
this.logger.Info("finish copying %s => %s", subPath, destDirMatter.Path)
@@ -688,7 +688,7 @@ func (h *DavService) confirmLocks(r *http.Request, src, dst string) (release fun
return nil, http.StatusLocked, webdav.ErrLocked
}
//lock.
// lock.
func (this *DavService) HandleLock(w http.ResponseWriter, r *http.Request, user *User, subPath string) {
duration, err := webdav.ParseTimeout(r.Header.Get("Timeout"))
@@ -785,7 +785,7 @@ func (this *DavService) HandleLock(w http.ResponseWriter, r *http.Request, user
}
//unlock
// unlock
func (this *DavService) HandleUnlock(w http.ResponseWriter, r *http.Request, user *User, subPath string) {
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
@@ -810,44 +810,44 @@ func (this *DavService) HandleUnlock(w http.ResponseWriter, r *http.Request, use
}
}
//hanle all the request.
func (this *DavService) HandleDav(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
// hanle all the request.
func (this *DavService) HandleDav(writer http.ResponseWriter, request *http.Request, user *User, space *Space, subPath string) {
method := request.Method
if method == "OPTIONS" {
//cors option
this.HandleOptions(writer, request, user, subPath)
this.HandleOptions(writer, request, user, space, subPath)
} else if method == "GET" || method == "HEAD" || method == "POST" {
//get the detail of file. download
this.HandleGetHeadPost(writer, request, user, subPath)
this.HandleGetHeadPost(writer, request, user, space, subPath)
} else if method == "DELETE" {
//delete file
this.HandleDelete(writer, request, user, subPath)
this.HandleDelete(writer, request, user, space, subPath)
} else if method == "PUT" {
//upload file
this.HandlePut(writer, request, user, subPath)
this.HandlePut(writer, request, user, space, subPath)
} else if method == "MKCOL" {
//crate directory
this.HandleMkcol(writer, request, user, subPath)
this.HandleMkcol(writer, request, user, space, subPath)
} else if method == "COPY" {
//copy file/directory
this.HandleCopy(writer, request, user, subPath)
this.HandleCopy(writer, request, user, space, subPath)
} else if method == "MOVE" {
//move/rename a file or directory
this.HandleMove(writer, request, user, subPath)
this.HandleMove(writer, request, user, space, subPath)
} else if method == "LOCK" {
@@ -862,7 +862,7 @@ func (this *DavService) HandleDav(writer http.ResponseWriter, request *http.Requ
} else if method == "PROPFIND" {
//list a directory
this.HandlePropfind(writer, request, user, subPath)
this.HandlePropfind(writer, request, user, space, subPath)
} else if method == "PROPPATCH" {
+1 -1
View File
@@ -10,7 +10,7 @@ type DownloadToken struct {
UpdateTime time.Time `json:"updateTime" gorm:"type:timestamp not null;default:CURRENT_TIMESTAMP"`
CreateTime time.Time `json:"createTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
UserUuid string `json:"userUuid" gorm:"type:char(36) not null"`
MatterUuid string `json:"matterUuid" gorm:"type:char(36) not null;index:idx_download_token_mu"` //index should unique globally.
MatterUuid string `json:"matterUuid" gorm:"type:char(36) not null;index:idx_download_token_mu"` //index should unique globally for sqlite
ExpireTime time.Time `json:"expireTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
Ip string `json:"ip" gorm:"type:varchar(128) not null"`
}
+6 -6
View File
@@ -17,7 +17,7 @@ type ImageCacheDao struct {
BaseDao
}
//find by uuid. if not found return nil.
// find by uuid. if not found return nil.
func (this *ImageCacheDao) FindByUuid(uuid string) *ImageCache {
var entity = &ImageCache{}
db := core.CONTEXT.GetDB().Where("uuid = ?", uuid).First(entity)
@@ -31,7 +31,7 @@ func (this *ImageCacheDao) FindByUuid(uuid string) *ImageCache {
return entity
}
//find by uuid. if not found panic NotFound error
// find by uuid. if not found panic NotFound error
func (this *ImageCacheDao) CheckByUuid(uuid string) *ImageCache {
entity := this.FindByUuid(uuid)
if entity == nil {
@@ -137,7 +137,7 @@ func (this *ImageCacheDao) Save(imageCache *ImageCache) *ImageCache {
func (this *ImageCacheDao) deleteFileAndDir(imageCache *ImageCache) {
filePath := GetUserCacheRootDir(imageCache.Username) + imageCache.Path
filePath := GetSpaceCacheRootDir(imageCache.Username) + imageCache.Path
dirPath := filepath.Dir(filePath)
@@ -152,7 +152,7 @@ func (this *ImageCacheDao) deleteFileAndDir(imageCache *ImageCache) {
}
//delete a file from db and disk.
// delete a file from db and disk.
func (this *ImageCacheDao) Delete(imageCache *ImageCache) {
db := core.CONTEXT.GetDB().Delete(&imageCache)
@@ -162,7 +162,7 @@ func (this *ImageCacheDao) Delete(imageCache *ImageCache) {
}
//delete all the cache of a matter.
// delete all the cache of a matter.
func (this *ImageCacheDao) DeleteByMatterUuid(matterUuid string) {
var wp = &builder.WherePair{}
@@ -208,7 +208,7 @@ func (this *ImageCacheDao) SizeBetweenTime(startTime time.Time, endTime time.Tim
return size
}
//System cleanup.
// System cleanup.
func (this *ImageCacheDao) Cleanup() {
this.logger.Info("[ImageCacheDao]clean up. Delete all ImageCache ")
db := core.CONTEXT.GetDB().Where("uuid is not null").Delete(ImageCache{})
+1 -1
View File
@@ -24,5 +24,5 @@ type ImageCache struct {
// get the absolute path. path in db means relative path.
func (this *ImageCache) AbsolutePath() string {
return GetUserCacheRootDir(this.Username) + this.Path
return GetSpaceCacheRootDir(this.Username) + this.Path
}
+4 -4
View File
@@ -14,7 +14,7 @@ import (
"strings"
)
//@Service
// @Service
type ImageCacheService struct {
BaseBean
imageCacheDao *ImageCacheDao
@@ -92,7 +92,7 @@ func (this *ImageCacheService) ResizeParams(request *http.Request) (needProcess
}
//resize image.
// resize image.
func (this *ImageCacheService) ResizeImage(request *http.Request, filePath string) *image.NRGBA {
diskFile, err := os.Open(filePath)
@@ -148,7 +148,7 @@ func (this *ImageCacheService) ResizeImage(request *http.Request, filePath strin
}
}
//cache an image
// cache an image
func (this *ImageCacheService) cacheImage(writer http.ResponseWriter, request *http.Request, matter *Matter) *ImageCache {
//only these image can do.
@@ -177,7 +177,7 @@ func (this *ImageCacheService) cacheImage(writer http.ResponseWriter, request *h
cacheImageName := util.GetSimpleFileName(matter.Name) + "_" + mode + extension
cacheImageRelativePath := util.GetSimpleFileName(matter.Path) + "_" + mode + extension
cacheImageAbsolutePath := GetUserCacheRootDir(user.Username) + util.GetSimpleFileName(matter.Path) + "_" + mode + extension
cacheImageAbsolutePath := GetSpaceCacheRootDir(user.Username) + util.GetSimpleFileName(matter.Path) + "_" + mode + extension
//create directory
dir := filepath.Dir(cacheImageAbsolutePath)
+31 -11
View File
@@ -23,7 +23,7 @@ import (
"time"
)
//install apis. Only when installing period can be visited.
// install apis. Only when installing period can be visited.
type InstallController struct {
BaseController
uploadTokenDao *UploadTokenDao
@@ -78,7 +78,11 @@ func (this *InstallController) Init() {
&Preference{},
&Session{},
&Share{},
&Space{},
&SpaceMember{},
&UploadToken{},
&UploadSession{},
&UploadChunk{},
&User{},
}
@@ -266,7 +270,7 @@ func (this *InstallController) validateTableMetaList(tableInfoList []*InstallTab
}
//Ping db.
// Ping db.
func (this *InstallController) Verify(writer http.ResponseWriter, request *http.Request) *result.WebResult {
db := this.openDbConnection(writer, request)
@@ -324,7 +328,7 @@ func (this *InstallController) CreateTable(writer http.ResponseWriter, request *
}
//get the list of admin.
// get the list of admin.
func (this *InstallController) AdminList(writer http.ResponseWriter, request *http.Request) *result.WebResult {
db := this.openDbConnection(writer, request)
@@ -342,7 +346,7 @@ func (this *InstallController) AdminList(writer http.ResponseWriter, request *ht
return this.Success(users)
}
//create admin
// create admin
func (this *InstallController) CreateAdmin(writer http.ResponseWriter, request *http.Request) *result.WebResult {
db := this.openDbConnection(writer, request)
@@ -368,9 +372,21 @@ func (this *InstallController) CreateAdmin(writer http.ResponseWriter, request *
panic(result.BadRequestI18n(request, i18n.UsernameExist, adminUsername))
}
user := &User{}
space := &Space{}
timeUUID, _ := uuid.NewV4()
user.Uuid = string(timeUUID.String())
space.Uuid = timeUUID.String()
space.CreateTime = time.Now()
space.UpdateTime = time.Now()
space.Name = adminUsername
space.UserUuid = ""
space.SizeLimit = -1
space.Type = SPACE_TYPE_PRIVATE
db3 := db.Create(space)
this.PanicError(db3.Error)
user := &User{}
timeUUID, _ = uuid.NewV4()
user.Uuid = timeUUID.String()
user.CreateTime = time.Now()
user.UpdateTime = time.Now()
user.LastTime = time.Now()
@@ -378,17 +394,21 @@ func (this *InstallController) CreateAdmin(writer http.ResponseWriter, request *
user.Role = USER_ROLE_ADMINISTRATOR
user.Username = adminUsername
user.Password = util.GetBcrypt(adminPassword)
user.SizeLimit = -1
user.SpaceUuid = space.Uuid
user.Status = USER_STATUS_OK
db3 := db.Create(user)
this.PanicError(db3.Error)
db4 := db.Create(user)
this.PanicError(db4.Error)
space.UserUuid = user.Uuid
db5 := db.Save(space)
this.PanicError(db5.Error)
return this.Success("OK")
}
//(if there is admin in db)Validate admin.
// (if there is admin in db)Validate admin.
func (this *InstallController) ValidateAdmin(writer http.ResponseWriter, request *http.Request) *result.WebResult {
db := this.openDbConnection(writer, request)
@@ -422,7 +442,7 @@ func (this *InstallController) ValidateAdmin(writer http.ResponseWriter, request
}
//Finish the installation
// Finish the installation
func (this *InstallController) Finish(writer http.ResponseWriter, request *http.Request) *result.WebResult {
dbType := request.FormValue("dbType")
+229 -222
View File
@@ -2,11 +2,11 @@ package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/i18n"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
"net/http"
"strconv"
"sort"
"strings"
)
@@ -18,6 +18,7 @@ type MatterController struct {
downloadTokenDao *DownloadTokenDao
imageCacheDao *ImageCacheDao
shareDao *ShareDao
spaceDao *SpaceDao
shareService *ShareService
bridgeDao *BridgeDao
imageCacheService *ImageCacheService
@@ -51,6 +52,11 @@ func (this *MatterController) Init() {
this.shareDao = b
}
b = core.CONTEXT.GetBean(this.spaceDao)
if b, ok := b.(*SpaceDao); ok {
this.spaceDao = b
}
b = core.CONTEXT.GetBean(this.shareService)
if b, ok := b.(*ShareService); ok {
this.shareService = b
@@ -70,6 +76,9 @@ func (this *MatterController) Init() {
func (this *MatterController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
routeMap["/api/matter/detail"] = this.Wrap(this.Detail, USER_ROLE_USER)
routeMap["/api/matter/page"] = this.Wrap(this.Page, USER_ROLE_USER)
routeMap["/api/matter/search"] = this.Wrap(this.Search, USER_ROLE_USER)
routeMap["/api/matter/create/directory"] = this.Wrap(this.CreateDirectory, USER_ROLE_USER)
routeMap["/api/matter/upload"] = this.Wrap(this.Upload, USER_ROLE_USER)
@@ -84,8 +93,6 @@ func (this *MatterController) RegisterRoutes() map[string]func(writer http.Respo
routeMap["/api/matter/rename"] = this.Wrap(this.Rename, USER_ROLE_USER)
routeMap["/api/matter/change/privacy"] = this.Wrap(this.ChangePrivacy, USER_ROLE_USER)
routeMap["/api/matter/move"] = this.Wrap(this.Move, USER_ROLE_USER)
routeMap["/api/matter/detail"] = this.Wrap(this.Detail, USER_ROLE_USER)
routeMap["/api/matter/page"] = this.Wrap(this.Page, USER_ROLE_GUEST)
//mirror local files.
routeMap["/api/matter/mirror"] = this.Wrap(this.Mirror, USER_ROLE_USER)
@@ -96,145 +103,150 @@ func (this *MatterController) RegisterRoutes() map[string]func(writer http.Respo
func (this *MatterController) Detail(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := request.FormValue("uuid")
if uuid == "" {
panic(result.BadRequest("uuid cannot be null"))
}
uuid := util.ExtractRequestString(request, "uuid")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckReadableByUuid(request, user, spaceUuid)
matter := this.matterService.Detail(request, uuid)
user := this.checkUser(request)
if matter.UserUuid != user.Uuid {
if matter.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
}
//add the user's info.
if space.Uuid == user.SpaceUuid {
matter.User = user
} else {
matter.User = this.userDao.FindByUuid(matter.UserUuid)
}
return this.Success(matter)
}
func (this *MatterController) Page(writer http.ResponseWriter, request *http.Request) *result.WebResult {
pageStr := request.FormValue("page")
pageSizeStr := request.FormValue("pageSize")
orderCreateTime := request.FormValue("orderCreateTime")
orderUpdateTime := request.FormValue("orderUpdateTime")
orderDeleteTime := request.FormValue("orderDeleteTime")
orderSort := request.FormValue("orderSort")
orderTimes := request.FormValue("orderTimes")
page := util.ExtractRequestOptionalInt(request, "page", 0)
pageSize := util.ExtractRequestOptionalInt(request, "pageSize", 200)
orderCreateTime := util.ExtractRequestOptionalString(request, "orderCreateTime", "")
orderUpdateTime := util.ExtractRequestOptionalString(request, "orderUpdateTime", "")
orderDeleteTime := util.ExtractRequestOptionalString(request, "orderDeleteTime", "")
orderSort := util.ExtractRequestOptionalString(request, "orderSort", "")
orderTimes := util.ExtractRequestOptionalString(request, "orderTimes", "")
orderDir := util.ExtractRequestOptionalString(request, "orderDir", "")
orderSize := util.ExtractRequestOptionalString(request, "orderSize", "")
orderName := util.ExtractRequestOptionalString(request, "orderName", "")
puuid := request.FormValue("puuid")
name := request.FormValue("name")
dir := request.FormValue("dir")
deleted := request.FormValue("deleted")
orderDir := request.FormValue("orderDir")
orderSize := request.FormValue("orderSize")
orderName := request.FormValue("orderName")
extensionsStr := request.FormValue("extensions")
puuid := util.ExtractRequestOptionalString(request, "puuid", "")
name := util.ExtractRequestOptionalString(request, "name", "")
dir := util.ExtractRequestOptionalString(request, "dir", "")
deleted := util.ExtractRequestOptionalString(request, "deleted", "")
extensionsStr := util.ExtractRequestOptionalString(request, "extensions", "")
var userUuid string
//auth by shareUuid.
shareUuid := request.FormValue("shareUuid")
shareCode := request.FormValue("shareCode")
shareRootUuid := request.FormValue("shareRootUuid")
if shareUuid != "" {
if puuid == "" {
panic(result.BadRequest("puuid cannot be null"))
}
dirMatter := this.matterDao.CheckByUuid(puuid)
if !dirMatter.Dir {
panic(result.BadRequest("puuid is not a directory"))
}
user := this.findUser(request)
this.shareService.ValidateMatter(request, shareUuid, shareCode, user, shareRootUuid, dirMatter)
puuid = dirMatter.Uuid
} else {
//if cannot auth by share. Then login is required.
user := this.checkUser(request)
userUuid = user.Uuid
}
var page int
if pageStr != "" {
page, _ = strconv.Atoi(pageStr)
}
pageSize := 200
if pageSizeStr != "" {
tmp, err := strconv.Atoi(pageSizeStr)
if err == nil {
pageSize = tmp
}
}
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
this.spaceService.CheckReadableByUuid(request, user, spaceUuid)
var extensions []string
if extensionsStr != "" {
extensions = strings.Split(extensionsStr, ",")
}
sortArray := []builder.OrderPair{
{
Key: "dir",
Value: orderDir,
},
{
Key: "create_time",
Value: orderCreateTime,
},
{
Key: "update_time",
Value: orderUpdateTime,
},
{
Key: "delete_time",
Value: orderDeleteTime,
},
{
Key: "sort",
Value: orderSort,
},
{
Key: "size",
Value: orderSize,
},
{
Key: "name",
Value: orderName,
},
{
Key: "times",
Value: orderTimes,
},
}
pager := this.matterDao.Page(page, pageSize, puuid, userUuid, name, dir, deleted, extensions, sortArray)
pager := this.matterService.Page(
request,
page,
pageSize,
orderCreateTime,
orderUpdateTime,
orderDeleteTime,
orderSort,
orderTimes,
orderDir,
orderSize,
orderName,
puuid,
name,
dir,
deleted,
extensions,
spaceUuid,
)
return this.Success(pager)
}
func (this *MatterController) CreateDirectory(writer http.ResponseWriter, request *http.Request) *result.WebResult {
// DFS search.
func (this *MatterController) Search(writer http.ResponseWriter, request *http.Request) *result.WebResult {
puuid := request.FormValue("puuid")
name := request.FormValue("name")
limit := util.ExtractRequestOptionalInt(request, "limit", 200)
puuid := util.ExtractRequestString(request, "puuid")
keyword := util.ExtractRequestString(request, "keyword")
deletedStr := util.ExtractRequestOptionalString(request, "deleted", "")
var deleted = false
if deletedStr == TRUE {
deleted = true
}
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
this.spaceService.CheckReadableByUuid(request, user, spaceUuid)
var dirMatter = this.matterDao.CheckWithRootByUuid(puuid, user)
matters := this.matterService.DfsSearch(
request,
limit,
puuid,
keyword,
spaceUuid,
deleted,
)
matter := this.matterService.AtomicCreateDirectory(request, dirMatter, name, user)
//sort.
//从小到大自定义排序
sort.Slice(matters, func(i, j int) bool {
matter1 := matters[i]
matter2 := matters[j]
if matter1.Dir {
return true
} else if matter2.Dir {
return false
} else {
cmp := strings.Compare(matter1.Name, matter2.Name)
if cmp < 0 {
return true
} else if cmp == 0 {
return true
} else {
return false
}
}
})
return this.Success(matters)
}
func (this *MatterController) CreateDirectory(writer http.ResponseWriter, request *http.Request) *result.WebResult {
puuid := util.ExtractRequestString(request, "puuid")
name := util.ExtractRequestString(request, "name")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
var dirMatter = this.matterDao.CheckWithRootByUuid(puuid, space)
matter := this.matterService.AtomicCreateDirectory(request, dirMatter, name, user, space)
return this.Success(matter)
}
func (this *MatterController) Upload(writer http.ResponseWriter, request *http.Request) *result.WebResult {
puuid := util.ExtractRequestString(request, "puuid")
privacy := util.ExtractRequestOptionalBool(request, "privacy", true)
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
puuid := request.FormValue("puuid")
privacyStr := request.FormValue("privacy")
file, handler, err := request.FormFile("file")
this.PanicError(err)
defer func() {
@@ -242,14 +254,10 @@ func (this *MatterController) Upload(writer http.ResponseWriter, request *http.R
this.PanicError(err)
}()
user := this.checkUser(request)
privacy := privacyStr == TRUE
err = request.ParseMultipartForm(32 << 20)
this.PanicError(err)
//for IE browser. filename may contains filepath.
//for IE browser. filename may contain filepath.
fileName := handler.Filename
pos := strings.LastIndex(fileName, "\\")
if pos != -1 {
@@ -260,65 +268,77 @@ func (this *MatterController) Upload(writer http.ResponseWriter, request *http.R
fileName = fileName[pos+1:]
}
dirMatter := this.matterDao.CheckWithRootByUuid(puuid, user)
dirMatter := this.matterDao.CheckWithRootByUuid(puuid, space)
//support upload simultaneously
matter := this.matterService.Upload(request, file, user, dirMatter, fileName, privacy)
matter := this.matterService.Upload(request, file, handler, user, space, dirMatter, fileName, privacy)
return this.Success(matter)
}
//crawl a file by url.
// crawl a file by url.
func (this *MatterController) Crawl(writer http.ResponseWriter, request *http.Request) *result.WebResult {
url := request.FormValue("url")
destPath := request.FormValue("destPath")
filename := request.FormValue("filename")
url := util.ExtractRequestString(request, "url")
destPath := util.ExtractRequestOptionalString(request, "destPath", "")
puuid := util.ExtractRequestOptionalString(request, "puuid", "")
filename := util.ExtractRequestString(request, "filename")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
dirMatter := this.matterService.CreateDirectories(request, user, destPath)
var dirMatter *Matter
if puuid != "" {
dirMatter = this.matterDao.CheckWithRootByUuid(puuid, space)
if dirMatter.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
}
if !dirMatter.Dir {
panic(" puuid is not a dir.")
}
} else {
if destPath == "" {
panic(" puuid or destPath cannot be null")
}
dirMatter = this.matterService.CreateDirectories(request, user, space, destPath)
}
if url == "" || (!strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://")) {
panic(" url must start with http:// or https://")
}
if filename == "" {
panic("filename cannot be null")
}
matter := this.matterService.AtomicCrawl(request, url, filename, user, dirMatter, true)
matter := this.matterService.AtomicCrawl(request, url, filename, user, space, dirMatter, true)
return this.Success(matter)
}
//soft delete.
// soft delete.
func (this *MatterController) SoftDelete(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := request.FormValue("uuid")
if uuid == "" {
panic(result.BadRequest("uuid cannot be null"))
}
matter := this.matterDao.CheckByUuid(uuid)
uuid := util.ExtractRequestString(request, "uuid")
user := this.checkUser(request)
if matter.UserUuid != user.Uuid {
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
matter := this.matterDao.CheckByUuid(uuid)
if matter.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
}
this.matterService.AtomicSoftDelete(request, matter, user)
this.matterService.AtomicSoftDelete(request, matter, user, space)
return this.Success("OK")
}
func (this *MatterController) SoftDeleteBatch(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuids := request.FormValue("uuids")
if uuids == "" {
panic(result.BadRequest("uuids cannot be null"))
}
uuids := util.ExtractRequestString(request, "uuids")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
uuidArray := strings.Split(uuids, ",")
@@ -332,7 +352,7 @@ func (this *MatterController) SoftDeleteBatch(writer http.ResponseWriter, reques
continue
}
if matter.UserUuid != user.Uuid {
if matter.SpaceUuid != spaceUuid {
panic(result.UNAUTHORIZED)
}
@@ -340,24 +360,24 @@ func (this *MatterController) SoftDeleteBatch(writer http.ResponseWriter, reques
}
for _, matter := range matters {
this.matterService.AtomicSoftDelete(request, matter, user)
this.matterService.AtomicSoftDelete(request, matter, user, space)
}
return this.Success("OK")
}
//recovery delete.
// recovery delete.
func (this *MatterController) Recovery(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := request.FormValue("uuid")
if uuid == "" {
panic(result.BadRequest("uuid cannot be null"))
}
uuid := util.ExtractRequestString(request, "uuid")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
matter := this.matterDao.CheckByUuid(uuid)
user := this.checkUser(request)
if matter.UserUuid != user.Uuid {
if matter.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
}
@@ -366,13 +386,13 @@ func (this *MatterController) Recovery(writer http.ResponseWriter, request *http
return this.Success("OK")
}
//recovery batch.
// recovery batch.
func (this *MatterController) RecoveryBatch(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuids := util.ExtractRequestString(request, "uuids")
uuids := request.FormValue("uuids")
if uuids == "" {
panic(result.BadRequest("uuids cannot be null"))
}
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
uuidArray := strings.Split(uuids, ",")
@@ -385,8 +405,7 @@ func (this *MatterController) RecoveryBatch(writer http.ResponseWriter, request
continue
}
user := this.checkUser(request)
if matter.UserUuid != user.Uuid {
if matter.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
}
@@ -397,35 +416,34 @@ func (this *MatterController) RecoveryBatch(writer http.ResponseWriter, request
return this.Success("OK")
}
//complete delete.
// complete delete.
func (this *MatterController) Delete(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := request.FormValue("uuid")
if uuid == "" {
panic(result.BadRequest("uuid cannot be null"))
}
matter := this.matterDao.CheckByUuid(uuid)
uuid := util.ExtractRequestString(request, "uuid")
user := this.checkUser(request)
if matter.UserUuid != user.Uuid {
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
matter := this.matterDao.CheckByUuid(uuid)
if matter.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
}
this.matterService.AtomicDelete(request, matter, user)
this.matterService.AtomicDelete(request, matter, user, space)
return this.Success("OK")
}
func (this *MatterController) DeleteBatch(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuids := request.FormValue("uuids")
if uuids == "" {
panic(result.BadRequest("uuids cannot be null"))
}
uuids := util.ExtractRequestString(request, "uuids")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
uuidArray := strings.Split(uuids, ",")
user := this.checkUser(request)
matters := make([]*Matter, 0)
for _, uuid := range uuidArray {
@@ -436,7 +454,7 @@ func (this *MatterController) DeleteBatch(writer http.ResponseWriter, request *h
continue
}
if matter.UserUuid != user.Uuid {
if matter.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
}
@@ -445,13 +463,13 @@ func (this *MatterController) DeleteBatch(writer http.ResponseWriter, request *h
for _, matter := range matters {
this.matterService.AtomicDelete(request, matter, user)
this.matterService.AtomicDelete(request, matter, user, space)
}
return this.Success("OK")
}
//manual clean expired deleted matters.
// manual clean expired deleted matters.
func (this *MatterController) CleanExpiredDeletedMatters(writer http.ResponseWriter, request *http.Request) *result.WebResult {
this.matterService.CleanExpiredDeletedMatters()
@@ -461,29 +479,32 @@ func (this *MatterController) CleanExpiredDeletedMatters(writer http.ResponseWri
func (this *MatterController) Rename(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := request.FormValue("uuid")
name := request.FormValue("name")
uuid := util.ExtractRequestString(request, "uuid")
name := util.ExtractRequestString(request, "name")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
matter := this.matterDao.CheckByUuid(uuid)
if matter.UserUuid != user.Uuid {
if matter.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
}
this.matterService.AtomicRename(request, matter, name, false, user)
this.matterService.AtomicRename(request, matter, name, false, user, space)
return this.Success(matter)
}
func (this *MatterController) ChangePrivacy(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := request.FormValue("uuid")
privacyStr := request.FormValue("privacy")
privacy := false
if privacyStr == TRUE {
privacy = true
}
uuid := util.ExtractRequestString(request, "uuid")
privacy := util.ExtractRequestOptionalBool(request, "privacy", false)
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
matter := this.matterDao.CheckByUuid(uuid)
@@ -495,8 +516,7 @@ func (this *MatterController) ChangePrivacy(writer http.ResponseWriter, request
panic(result.BadRequest("not changed. Invalid operation."))
}
user := this.checkUser(request)
if matter.UserUuid != user.Uuid {
if matter.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
}
@@ -507,25 +527,21 @@ func (this *MatterController) ChangePrivacy(writer http.ResponseWriter, request
}
func (this *MatterController) Move(writer http.ResponseWriter, request *http.Request) *result.WebResult {
srcUuidsStr := request.FormValue("srcUuids")
destUuid := request.FormValue("destUuid")
srcUuidsStr := util.ExtractRequestString(request, "srcUuids")
destUuid := util.ExtractRequestString(request, "destUuid")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
var srcUuids []string
if srcUuidsStr == "" {
panic(result.BadRequest("srcUuids cannot be null"))
} else {
srcUuids = strings.Split(srcUuidsStr, ",")
}
srcUuids = strings.Split(srcUuidsStr, ",")
user := this.checkUser(request)
var destMatter = this.matterDao.CheckWithRootByUuid(destUuid, user)
var destMatter = this.matterDao.CheckWithRootByUuid(destUuid, space)
if !destMatter.Dir {
panic(result.BadRequest("destination is not a directory"))
}
if destMatter.UserUuid != user.Uuid {
if destMatter.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
}
@@ -552,49 +568,41 @@ func (this *MatterController) Move(writer http.ResponseWriter, request *http.Req
panic(result.BadRequestI18n(request, i18n.MatterExist, srcMatter.Name))
}
if srcMatter.UserUuid != destMatter.UserUuid {
panic("owner not the same")
if srcMatter.SpaceUuid != destMatter.SpaceUuid {
panic("space not the same")
}
srcMatters = append(srcMatters, srcMatter)
}
this.matterService.AtomicMoveBatch(request, srcMatters, destMatter, user)
this.matterService.AtomicMoveBatch(request, srcMatters, destMatter, user, space)
return this.Success(nil)
}
//mirror local files to EyeblueTank
// mirror local files to EyeblueTank
func (this *MatterController) Mirror(writer http.ResponseWriter, request *http.Request) *result.WebResult {
srcPath := request.FormValue("srcPath")
destPath := request.FormValue("destPath")
overwriteStr := request.FormValue("overwrite")
srcPath := util.ExtractRequestString(request, "srcPath")
destPath := util.ExtractRequestString(request, "destPath")
overwrite := util.ExtractRequestOptionalBool(request, "overwrite", false)
if srcPath == "" {
panic(result.BadRequest("srcPath cannot be null"))
}
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckWritableByUuid(request, user, spaceUuid)
overwrite := false
if overwriteStr == TRUE {
overwrite = true
}
user := this.userDao.checkUser(request)
this.matterService.AtomicMirror(request, srcPath, destPath, overwrite, user)
this.matterService.AtomicMirror(request, srcPath, destPath, overwrite, user, space)
return this.Success(nil)
}
//download zip.
// download zip.
func (this *MatterController) Zip(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuids := request.FormValue("uuids")
if uuids == "" {
panic(result.BadRequest("uuids cannot be null"))
}
uuids := util.ExtractRequestString(request, "uuids")
user := this.checkUser(request)
spaceUuid := util.ExtractRequestOptionalString(request, "spaceUuid", user.SpaceUuid)
space := this.spaceService.CheckReadableByUuid(request, user, spaceUuid)
uuidArray := strings.Split(uuids, ",")
@@ -610,11 +618,10 @@ func (this *MatterController) Zip(writer http.ResponseWriter, request *http.Requ
}
}
user := this.checkUser(request)
puuid := matters[0].Puuid
for _, m := range matters {
if m.UserUuid != user.Uuid {
if m.SpaceUuid != space.Uuid {
panic(result.UNAUTHORIZED)
} else if m.Puuid != puuid {
panic(result.BadRequest("puuid not same"))
+200 -28
View File
@@ -1,15 +1,16 @@
package rest
import (
"math"
"os"
"time"
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
"github.com/eyebluecn/tank/code/tool/uuid"
"gorm.io/gorm"
"math"
"os"
"time"
)
type MatterDao struct {
@@ -46,7 +47,7 @@ func (this *MatterDao) FindByUuid(uuid string) *Matter {
return entity
}
//find by uuid. if not found panic NotFound error
// find by uuid. if not found panic NotFound error
func (this *MatterDao) CheckByUuid(uuid string) *Matter {
entity := this.FindByUuid(uuid)
if entity == nil {
@@ -56,7 +57,7 @@ func (this *MatterDao) CheckByUuid(uuid string) *Matter {
}
// find by uuid. if uuid=root, then return the Root Matter
func (this *MatterDao) CheckWithRootByUuid(uuid string, user *User) *Matter {
func (this *MatterDao) CheckWithRootByUuid(uuid string, space *Space) *Matter {
if uuid == "" {
panic(result.BadRequest("uuid cannot be null."))
@@ -64,10 +65,10 @@ func (this *MatterDao) CheckWithRootByUuid(uuid string, user *User) *Matter {
var matter *Matter
if uuid == MATTER_ROOT {
if user == nil {
panic(result.BadRequest("user cannot be null."))
if space == nil {
panic(result.BadRequest("space cannot be null."))
}
matter = NewRootMatter(user)
matter = NewRootMatter(space)
} else {
matter = this.CheckByUuid(uuid)
}
@@ -76,7 +77,7 @@ func (this *MatterDao) CheckWithRootByUuid(uuid string, user *User) *Matter {
}
// find by path. if path=/, then return the Root Matter
func (this *MatterDao) CheckWithRootByPath(path string, user *User) *Matter {
func (this *MatterDao) CheckWithRootByPath(path string, user *User, space *Space) *Matter {
var matter *Matter
@@ -85,7 +86,7 @@ func (this *MatterDao) CheckWithRootByPath(path string, user *User) *Matter {
}
if path == "" || path == "/" {
matter = NewRootMatter(user)
matter = NewRootMatter(space)
} else {
matter = this.checkByUserUuidAndPath(user.Uuid, path)
}
@@ -94,7 +95,7 @@ func (this *MatterDao) CheckWithRootByPath(path string, user *User) *Matter {
}
// find by path. if path=/, then return the Root Matter
func (this *MatterDao) FindWithRootByPath(path string, user *User) *Matter {
func (this *MatterDao) FindWithRootByPath(path string, user *User, space *Space) *Matter {
var matter *Matter
@@ -103,7 +104,7 @@ func (this *MatterDao) FindWithRootByPath(path string, user *User) *Matter {
}
if path == "" || path == "/" {
matter = NewRootMatter(user)
matter = NewRootMatter(space)
} else {
matter = this.findByUserUuidAndPath(user.Uuid, path)
}
@@ -175,6 +176,68 @@ func (this *MatterDao) CountByUserUuidAndPuuidAndDirAndName(userUuid string, puu
return int(count)
}
func (this *MatterDao) CountBySpaceUuidAndPuuidAndDirAndName(spaceUuid string, puuid string, dir bool, name string) int {
var matter Matter
var count int64
var wp = &builder.WherePair{}
if puuid != "" {
wp = wp.And(&builder.WherePair{Query: "puuid = ?", Args: []interface{}{puuid}})
}
if spaceUuid != "" {
wp = wp.And(&builder.WherePair{Query: "space_uuid = ?", Args: []interface{}{spaceUuid}})
}
if name != "" {
wp = wp.And(&builder.WherePair{Query: "name = ?", Args: []interface{}{name}})
}
wp = wp.And(&builder.WherePair{Query: "dir = ?", Args: []interface{}{dir}})
db := core.CONTEXT.GetDB().
Model(&matter).
Where(wp.Query, wp.Args...).
Count(&count)
this.PanicError(db.Error)
return int(count)
}
func (this *MatterDao) FindBySpaceUuidAndPuuidAndDirAndName(spaceUuid string, puuid string, dir bool, name string) *Matter {
var matter = &Matter{}
var wp = &builder.WherePair{}
if puuid != "" {
wp = wp.And(&builder.WherePair{Query: "puuid = ?", Args: []interface{}{puuid}})
}
if spaceUuid != "" {
wp = wp.And(&builder.WherePair{Query: "space_uuid = ?", Args: []interface{}{spaceUuid}})
}
if name != "" {
wp = wp.And(&builder.WherePair{Query: "name = ?", Args: []interface{}{name}})
}
wp = wp.And(&builder.WherePair{Query: "dir = ?", Args: []interface{}{dir}})
db := core.CONTEXT.GetDB().Where(wp.Query, wp.Args...).First(matter)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
this.PanicError(db.Error)
}
}
return matter
}
func (this *MatterDao) FindByUserUuidAndPuuidAndDirAndName(userUuid string, puuid string, dir string, name string) *Matter {
var matter = &Matter{}
@@ -212,6 +275,69 @@ func (this *MatterDao) FindByUserUuidAndPuuidAndDirAndName(userUuid string, puui
return matter
}
func (this *MatterDao) FindBySpaceNameAndPuuidAndDirAndName(spaceName string, puuid string, dir string, name string) *Matter {
var matter = &Matter{}
var wp = &builder.WherePair{}
if puuid != "" {
wp = wp.And(&builder.WherePair{Query: "puuid = ?", Args: []interface{}{puuid}})
}
if spaceName != "" {
wp = wp.And(&builder.WherePair{Query: "space_name = ?", Args: []interface{}{spaceName}})
}
if name != "" {
wp = wp.And(&builder.WherePair{Query: "name = ?", Args: []interface{}{name}})
}
if dir == TRUE {
wp = wp.And(&builder.WherePair{Query: "dir = ?", Args: []interface{}{true}})
} else if dir == FALSE {
wp = wp.And(&builder.WherePair{Query: "dir = ?", Args: []interface{}{false}})
}
db := core.CONTEXT.GetDB().Where(wp.Query, wp.Args...).First(matter)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
this.PanicError(db.Error)
}
}
return matter
}
func (this *MatterDao) FindByPuuidAndSpaceUuid(puuid string, spaceUuid string, sortArray []builder.OrderPair) []*Matter {
var matters []*Matter
var wp = &builder.WherePair{}
wp = wp.And(&builder.WherePair{Query: "puuid = ? AND space_uuid = ?", Args: []interface{}{puuid, spaceUuid}})
if sortArray == nil {
sortArray = []builder.OrderPair{
{
Key: "dir",
Value: DIRECTION_DESC,
},
{
Key: "create_time",
Value: DIRECTION_DESC,
},
}
}
db := core.CONTEXT.GetDB().Model(&Matter{}).Where(wp.Query, wp.Args...).Order(this.GetSortString(sortArray)).Find(&matters)
this.PanicError(db.Error)
return matters
}
func (this *MatterDao) FindByPuuidAndUserUuid(puuid string, userUuid string, sortArray []builder.OrderPair) []*Matter {
return this.FindByPuuidAndUserUuidAndDeleted(puuid, userUuid, "", sortArray)
}
@@ -255,11 +381,14 @@ func (this *MatterDao) FindByUuids(uuids []string, sortArray []builder.OrderPair
return matters
}
// pagination is 0 base.
func (this *MatterDao) PlainPage(
page int,
pageSize int,
puuid string,
userUuid string,
spaceUuid string,
name string,
dir string,
deleted string,
@@ -277,6 +406,10 @@ func (this *MatterDao) PlainPage(
wp = wp.And(&builder.WherePair{Query: "user_uuid = ?", Args: []interface{}{userUuid}})
}
if spaceUuid != "" {
wp = wp.And(&builder.WherePair{Query: "space_uuid = ?", Args: []interface{}{spaceUuid}})
}
if name != "" {
wp = wp.And(&builder.WherePair{Query: "name LIKE ?", Args: []interface{}{"%" + name + "%"}})
}
@@ -320,18 +453,19 @@ func (this *MatterDao) PlainPage(
return int(count), matters
}
func (this *MatterDao) Page(page int, pageSize int, puuid string, userUuid string, name string, dir string, deleted string, extensions []string, sortArray []builder.OrderPair) *Pager {
func (this *MatterDao) Page(page int, pageSize int, puuid string, userUuid string, spaceUuid string, name string, dir string, deleted string, extensions []string, sortArray []builder.OrderPair) *Pager {
count, matters := this.PlainPage(page, pageSize, puuid, userUuid, name, dir, deleted, nil, extensions, sortArray)
count, matters := this.PlainPage(page, pageSize, puuid, userUuid, spaceUuid, name, dir, deleted, nil, extensions, sortArray)
pager := NewPager(page, pageSize, count, matters)
return pager
}
//handle matter page by page.
// handle matter page by page.
func (this *MatterDao) PageHandle(
puuid string,
userUuid string,
spaceUuid string,
name string,
dir string,
deleted string,
@@ -349,13 +483,13 @@ func (this *MatterDao) PageHandle(
}
}
count, _ := this.PlainPage(0, pageSize, puuid, userUuid, name, dir, deleted, deleteTimeBefore, nil, sortArray)
count, _ := this.PlainPage(0, pageSize, puuid, userUuid, spaceUuid, name, dir, deleted, deleteTimeBefore, nil, sortArray)
if count > 0 {
var totalPages = int(math.Ceil(float64(count) / float64(pageSize)))
var page int
for page = 0; page < totalPages; page++ {
_, matters := this.PlainPage(0, pageSize, puuid, userUuid, name, dir, deleted, deleteTimeBefore, nil, sortArray)
_, matters := this.PlainPage(0, pageSize, puuid, userUuid, spaceUuid, name, dir, deleted, deleteTimeBefore, nil, sortArray)
for _, matter := range matters {
fun(matter)
}
@@ -385,7 +519,7 @@ func (this *MatterDao) Save(matter *Matter) *Matter {
return matter
}
//download time add 1
// download time add 1
func (this *MatterDao) TimesIncrement(matterUuid string) {
db := core.CONTEXT.GetDB().Model(&Matter{}).Where("uuid = ?", matterUuid).Updates(map[string]interface{}{"times": gorm.Expr("times + 1"), "visit_time": time.Now()})
this.PanicError(db.Error)
@@ -411,7 +545,27 @@ func (this *MatterDao) SizeByPuuidAndUserUuid(matterUuid string, userUuid string
return sumSize
}
//delete a file from db and disk.
func (this *MatterDao) SizeByPuuidAndSpaceUuid(matterUuid string, spaceUuid string) int64 {
var wp = &builder.WherePair{Query: "puuid = ? AND space_uuid = ?", Args: []interface{}{matterUuid, spaceUuid}}
var count int64
db := core.CONTEXT.GetDB().Model(&Matter{}).Where(wp.Query, wp.Args...).Count(&count)
if count == 0 {
return 0
}
var sumSize int64
db = core.CONTEXT.GetDB().Model(&Matter{}).Where(wp.Query, wp.Args...).Select("SUM(size)")
this.PanicError(db.Error)
row := db.Row()
err := row.Scan(&sumSize)
core.PanicError(err)
return sumSize
}
// delete a file from db and disk.
func (this *MatterDao) Delete(matter *Matter) {
// recursive if dir
@@ -425,9 +579,10 @@ func (this *MatterDao) Delete(matter *Matter) {
//delete from db.
db := core.CONTEXT.GetDB().Delete(&matter)
this.PanicError(db.Error)
//delete dir from disk.
util.DeleteEmptyDir(matter.AbsolutePath())
if util.PathExists(matter.AbsolutePath()) {
//delete dir from disk.
util.DeleteEmptyDir(matter.AbsolutePath())
}
} else {
@@ -450,7 +605,7 @@ func (this *MatterDao) Delete(matter *Matter) {
}
}
//soft delete a file or dir
// soft delete a file or dir
func (this *MatterDao) SoftDelete(matter *Matter) {
//soft delete from db.
@@ -459,7 +614,7 @@ func (this *MatterDao) SoftDelete(matter *Matter) {
}
//recovery a file
// recovery a file
func (this *MatterDao) Recovery(matter *Matter) {
//recovery from db.
@@ -501,7 +656,7 @@ func (this *MatterDao) SizeBetweenTime(startTime time.Time, endTime time.Time) i
return size
}
//find by userUuid and path. if not found, return nil
// find by userUuid and path. if not found, return nil
func (this *MatterDao) findByUserUuidAndPath(userUuid string, path string) *Matter {
var wp = &builder.WherePair{Query: "user_uuid = ? AND path = ?", Args: []interface{}{userUuid, path}}
@@ -520,7 +675,7 @@ func (this *MatterDao) findByUserUuidAndPath(userUuid string, path string) *Matt
return matter
}
//find by userUuid and path. if not found, panic
// find by userUuid and path. if not found, panic
func (this *MatterDao) checkByUserUuidAndPath(userUuid string, path string) *Matter {
if path == "" {
@@ -555,6 +710,11 @@ func (this *MatterDao) SumSizeByUserUuidAndPath(userUuid string, path string) in
}
func (this *MatterDao) UpdateSize(matterUuid string, size int64) {
db := core.CONTEXT.GetDB().Model(&Matter{}).Where("uuid = ?", matterUuid).Update("size", size)
this.PanicError(db.Error)
}
func (this *MatterDao) CountByUserUuidAndPath(userUuid string, path string) int64 {
var wp = &builder.WherePair{Query: "user_uuid = ? AND path like ?", Args: []interface{}{userUuid, path + "%"}}
@@ -567,7 +727,19 @@ func (this *MatterDao) CountByUserUuidAndPath(userUuid string, path string) int6
}
//统计总共有多少条。
func (this *MatterDao) CountByUserUuid(userUuid string) int64 {
var wp = &builder.WherePair{Query: "user_uuid = ?", Args: []interface{}{userUuid}}
var count int64
db := core.CONTEXT.GetDB().Model(&Matter{}).Where(wp.Query, wp.Args...).Count(&count)
core.PanicError(db.Error)
return count
}
// 统计总共有多少条。
func (this *MatterDao) Count() int64 {
var count int64
@@ -578,7 +750,7 @@ func (this *MatterDao) Count() int64 {
}
//System cleanup.
// System cleanup.
func (this *MatterDao) Cleanup() {
this.logger.Info("[MatterDao] clean up. Delete all Matter record in db and on disk.")
db := core.CONTEXT.GetDB().Where("uuid is not null").Delete(Matter{})
+29 -26
View File
@@ -34,9 +34,10 @@ type Matter struct {
Sort int64 `json:"sort" gorm:"type:bigint(20) not null"`
UpdateTime time.Time `json:"updateTime" gorm:"type:timestamp not null;default:CURRENT_TIMESTAMP"`
CreateTime time.Time `json:"createTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
Puuid string `json:"puuid" gorm:"type:char(36);index:idx_matter_puuid"` //index should unique globally.
Puuid string `json:"puuid" gorm:"type:char(36);index:idx_matter_puuid"` //index should unique globally for sqlite.
UserUuid string `json:"userUuid" gorm:"type:char(36);index:idx_matter_uu"`
Username string `json:"username" gorm:"type:varchar(45) not null"`
//TODO: check field usage.
SpaceName string `json:"space_name" gorm:"type:varchar(45) not null"`
Dir bool `json:"dir" gorm:"type:tinyint(1) not null;default:0"`
Name string `json:"name" gorm:"type:varchar(255) not null"`
Md5 string `json:"md5" gorm:"type:varchar(45)"`
@@ -44,39 +45,42 @@ type Matter struct {
Privacy bool `json:"privacy" gorm:"type:tinyint(1) not null;default:0"`
Path string `json:"path" gorm:"type:varchar(1024)"`
Times int64 `json:"times" gorm:"type:bigint(20) not null;default:0"`
Parent *Matter `json:"parent" gorm:"-"`
Children []*Matter `json:"-" gorm:"-"`
Prop string `json:"prop" gorm:"type:varchar(1024) not null;default:'{}'"`
VisitTime time.Time `json:"visitTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
Deleted bool `json:"deleted" gorm:"type:tinyint(1) not null;index:idx_matter_del;default:0"`
DeleteTime time.Time `json:"deleteTime" gorm:"type:timestamp not null;index:idx_matter_delt;default:'2018-01-01 00:00:00'"`
SpaceUuid string `json:"spaceUuid" gorm:"type:char(36) not null;index:idx_matter_space_uuid"`
User *User `json:"user" gorm:"-"`
Parent *Matter `json:"parent" gorm:"-"`
Children []*Matter `json:"-" gorm:"-"`
}
// get matter's absolute path. the Path property is relative path in db.
func (this *Matter) AbsolutePath() string {
return GetUserMatterRootDir(this.Username) + this.Path
return GetSpaceMatterRootDir(this.SpaceName) + this.Path
}
func (this *Matter) MimeType() string {
return util.GetMimeType(util.GetExtension(this.Name))
}
//Create a root matter. It's convenient for copy and move
func NewRootMatter(user *User) *Matter {
// Create a root matter. It's convenient for copy and move
func NewRootMatter(space *Space) *Matter {
matter := &Matter{}
matter.Uuid = MATTER_ROOT
matter.UserUuid = user.Uuid
matter.Username = user.Username
matter.UserUuid = space.UserUuid
matter.SpaceUuid = space.Uuid
matter.SpaceName = space.Name
matter.Dir = true
matter.Path = ""
matter.CreateTime = user.CreateTime
matter.UpdateTime = user.UpdateTime
matter.VisitTime = user.UpdateTime
matter.CreateTime = space.CreateTime
matter.UpdateTime = space.UpdateTime
matter.VisitTime = space.UpdateTime
return matter
}
//get user's space absolute path
// get user's space absolute path
func GetUserSpaceRootDir(username string) (rootDirPath string) {
rootDirPath = fmt.Sprintf("%s/%s", core.CONFIG.MatterPath(), username)
@@ -84,31 +88,31 @@ func GetUserSpaceRootDir(username string) (rootDirPath string) {
return rootDirPath
}
//get user's root absolute path
func GetUserMatterRootDir(username string) (rootDirPath string) {
// get user's root absolute path
func GetSpaceMatterRootDir(spaceName string) (rootDirPath string) {
rootDirPath = fmt.Sprintf("%s/%s/%s", core.CONFIG.MatterPath(), username, MATTER_ROOT)
rootDirPath = fmt.Sprintf("%s/%s/%s", core.CONFIG.MatterPath(), spaceName, MATTER_ROOT)
return rootDirPath
}
//get user's cache absolute path
func GetUserCacheRootDir(username string) (rootDirPath string) {
// get user's cache absolute path
func GetSpaceCacheRootDir(spaceName string) (rootDirPath string) {
rootDirPath = fmt.Sprintf("%s/%s/%s", core.CONFIG.MatterPath(), username, MATTER_CACHE)
rootDirPath = fmt.Sprintf("%s/%s/%s", core.CONFIG.MatterPath(), spaceName, MATTER_CACHE)
return rootDirPath
}
//get user's zip absolute path
func GetUserZipRootDir(username string) (rootDirPath string) {
// get user's zip absolute path
func GetSpaceZipRootDir(spaceName string) (rootDirPath string) {
rootDirPath = fmt.Sprintf("%s/%s/%s", core.CONFIG.MatterPath(), username, MATTER_ZIP)
rootDirPath = fmt.Sprintf("%s/%s/%s", core.CONFIG.MatterPath(), spaceName, MATTER_ZIP)
return rootDirPath
}
//check matter's name. If error, panic.
// check matter's name. If error, panic.
func CheckMatterName(request *http.Request, name string) string {
if name == "" {
@@ -127,7 +131,7 @@ func CheckMatterName(request *http.Request, name string) string {
return name
}
//fetch the props
// fetch the props
func (this *Matter) FetchPropMap() map[string]string {
m := make(map[string]string)
@@ -144,13 +148,12 @@ func (this *Matter) FetchPropMap() map[string]string {
return m
}
//fetch the props
// fetch the props
func (this *Matter) SetPropMap(propMap map[string]string) {
b, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(propMap)
if err != nil {
panic(err)
}
this.Prop = string(b)
}
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -62,7 +62,7 @@ func (this *PreferenceController) RegisterRoutes() map[string]func(writer http.R
return routeMap
}
//ping the application. Return current version.
// ping the application. Return current version.
func (this *PreferenceController) Ping(writer http.ResponseWriter, request *http.Request) *result.WebResult {
return this.Success(core.VERSION)
@@ -76,7 +76,7 @@ func (this *PreferenceController) Fetch(writer http.ResponseWriter, request *htt
return this.Success(preference)
}
//edit basic info.
// edit basic info.
func (this *PreferenceController) Edit(writer http.ResponseWriter, request *http.Request) *result.WebResult {
name := request.FormValue("name")
@@ -89,6 +89,7 @@ func (this *PreferenceController) Edit(writer http.ResponseWriter, request *http
downloadDirMaxNumStr := request.FormValue("downloadDirMaxNum")
defaultTotalSizeLimitStr := request.FormValue("defaultTotalSizeLimit")
allowRegisterStr := request.FormValue("allowRegister")
allowChunkUploadStr := request.FormValue("allowChunkUpload")
deletedKeepDaysStr := request.FormValue("deletedKeepDays")
if name == "" {
@@ -140,6 +141,11 @@ func (this *PreferenceController) Edit(writer http.ResponseWriter, request *http
allowRegister = true
}
var allowChunkUpload = false
if allowChunkUploadStr == TRUE {
allowChunkUpload = true
}
preference := this.preferenceDao.Fetch()
oldDeletedKeepDays := preference.DeletedKeepDays
preference.Name = name
@@ -151,6 +157,7 @@ func (this *PreferenceController) Edit(writer http.ResponseWriter, request *http
preference.DownloadDirMaxNum = downloadDirMaxNum
preference.DefaultTotalSizeLimit = defaultTotalSizeLimit
preference.AllowRegister = allowRegister
preference.AllowChunkUpload = allowChunkUpload
preference.DeletedKeepDays = deletedKeepDays
preference = this.preferenceService.Save(preference)
@@ -163,7 +170,7 @@ func (this *PreferenceController) Edit(writer http.ResponseWriter, request *http
return this.Success(preference)
}
//edit preview config.
// edit preview config.
func (this *PreferenceController) EditPreviewConfig(writer http.ResponseWriter, request *http.Request) *result.WebResult {
previewConfig := request.FormValue("previewConfig")
@@ -200,7 +207,7 @@ func (this *PreferenceController) EditScanConfig(writer http.ResponseWriter, req
//validate scope.
if scanConfig.Scope == SCAN_SCOPE_CUSTOM {
if len(scanConfig.Usernames) == 0 {
if len(scanConfig.SpaceNames) == 0 {
panic(result.BadRequest("scope cannot be null"))
}
} else if scanConfig.Scope == SCAN_SCOPE_ALL {
@@ -219,14 +226,14 @@ func (this *PreferenceController) EditScanConfig(writer http.ResponseWriter, req
return this.Success(preference)
}
//scan immediately according the current config.
// scan immediately according the current config.
func (this *PreferenceController) ScanOnce(writer http.ResponseWriter, request *http.Request) *result.WebResult {
this.taskService.doScanTask()
return this.Success("OK")
}
//cleanup system data.
// cleanup system data.
func (this *PreferenceController) SystemCleanup(writer http.ResponseWriter, request *http.Request) *result.WebResult {
user := this.checkUser(request)
+5 -4
View File
@@ -19,6 +19,7 @@ type Preference struct {
DownloadDirMaxNum int64 `json:"downloadDirMaxNum" gorm:"type:bigint(20) not null;default:-1"`
DefaultTotalSizeLimit int64 `json:"defaultTotalSizeLimit" gorm:"type:bigint(20) not null;default:-1"`
AllowRegister bool `json:"allowRegister" gorm:"type:tinyint(1) not null;default:0"`
AllowChunkUpload bool `json:"allowChunkUpload" gorm:"type:tinyint(1) not null;default:0"`
PreviewConfig string `json:"previewConfig" gorm:"type:text"`
ScanConfig string `json:"scanConfig" gorm:"type:text"`
DeletedKeepDays int64 `json:"deletedKeepDays" gorm:"type:bigint(20) not null;default:7"`
@@ -32,19 +33,19 @@ const (
SCAN_SCOPE_CUSTOM = "CUSTOM"
)
//scan config struct.
// scan config struct.
type ScanConfig struct {
//whether enable the scan task.
Enable bool `json:"enable"`
//when to process the task. five fields. @every 1s
Cron string `json:"cron"`
//username
Usernames []string `json:"usernames"`
//spaceNames
SpaceNames []string `json:"spaceNames"`
//scan scope. see SCAN_SCOPE
Scope string `json:"scope"`
}
//fetch the scan config
// fetch the scan config
func (this *Preference) FetchScanConfig() *ScanConfig {
json := this.ScanConfig
+108 -29
View File
@@ -19,6 +19,7 @@ type ShareController struct {
matterDao *MatterDao
matterService *MatterService
shareService *ShareService
alienService *AlienService
}
func (this *ShareController) Init() {
@@ -49,6 +50,11 @@ func (this *ShareController) Init() {
this.shareService = b
}
b = core.CONTEXT.GetBean(this.alienService)
if b, ok := b.(*AlienService); ok {
this.alienService = b
}
}
func (this *ShareController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
@@ -63,43 +69,28 @@ func (this *ShareController) RegisterRoutes() map[string]func(writer http.Respon
routeMap["/api/share/browse"] = this.Wrap(this.Browse, USER_ROLE_GUEST)
routeMap["/api/share/zip"] = this.Wrap(this.Zip, USER_ROLE_GUEST)
routeMap["/api/share/matter/page"] = this.Wrap(this.MatterPage, USER_ROLE_GUEST)
routeMap["/api/share/matter/preview"] = this.WrapPure(this.MatterPreview, USER_ROLE_GUEST)
routeMap["/api/share/matter/download"] = this.WrapPure(this.MatterDownload, USER_ROLE_GUEST)
return routeMap
}
func (this *ShareController) Create(writer http.ResponseWriter, request *http.Request) *result.WebResult {
matterUuids := request.FormValue("matterUuids")
expireInfinityStr := request.FormValue("expireInfinity")
expireTimeStr := request.FormValue("expireTime")
if matterUuids == "" {
panic(result.BadRequest("matterUuids cannot be null"))
}
var expireTime time.Time
expireInfinity := false
if expireInfinityStr == TRUE {
expireInfinity = true
expireTime = time.Now()
} else {
if expireTimeStr == "" {
panic(result.BadRequest("time format error"))
} else {
expireTime = util.ConvertDateTimeStringToTime(expireTimeStr)
}
uuidArray := util.ExtractRequestArray(request, "matterUuids")
expireInfinity := util.ExtractRequestBool(request, "expireInfinity")
spaceUuid := util.ExtractRequestString(request, "spaceUuid")
var expireTime = time.Now()
if !expireInfinity {
expireTime = util.ExtractRequestTime(request, "expireTime")
if expireTime.Before(time.Now()) {
panic(result.BadRequest("expire time cannot before now"))
}
}
uuidArray := strings.Split(matterUuids, ",")
if len(uuidArray) == 0 {
panic(result.BadRequest("share at least one file"))
} else if len(uuidArray) > SHARE_MAX_NUM {
if len(uuidArray) > SHARE_MAX_NUM {
panic(result.BadRequestI18n(request, i18n.ShareNumExceedLimit, len(uuidArray), SHARE_MAX_NUM))
}
@@ -148,6 +139,7 @@ func (this *ShareController) Create(writer http.ResponseWriter, request *http.Re
Code: util.RandomString4(),
ExpireInfinity: expireInfinity,
ExpireTime: expireTime,
SpaceUuid: spaceUuid,
}
this.shareDao.Create(share)
@@ -192,14 +184,14 @@ func (this *ShareController) DeleteBatch(writer http.ResponseWriter, request *ht
for _, uuid := range uuidArray {
imageCache := this.shareDao.FindByUuid(uuid)
share := this.shareDao.FindByUuid(uuid)
user := this.checkUser(request)
if imageCache.UserUuid != user.Uuid {
if share.UserUuid != user.Uuid {
panic(result.UNAUTHORIZED)
}
this.shareDao.Delete(imageCache)
this.shareDao.Delete(share)
}
return this.Success("OK")
@@ -266,6 +258,7 @@ func (this *ShareController) CheckShare(writer http.ResponseWriter, request *htt
return this.shareService.CheckShare(request, shareUuid, code, user)
}
// get the basic info of this share.
func (this *ShareController) Browse(writer http.ResponseWriter, request *http.Request) *result.WebResult {
shareUuid := request.FormValue("shareUuid")
@@ -375,3 +368,89 @@ func (this *ShareController) Zip(writer http.ResponseWriter, request *http.Reque
return nil
}
// matter list under a share.
func (this *ShareController) MatterPage(writer http.ResponseWriter, request *http.Request) *result.WebResult {
page := util.ExtractRequestOptionalInt(request, "page", 0)
pageSize := util.ExtractRequestOptionalInt(request, "pageSize", 200)
orderCreateTime := util.ExtractRequestOptionalString(request, "orderCreateTime", "")
orderUpdateTime := util.ExtractRequestOptionalString(request, "orderUpdateTime", "")
orderDeleteTime := util.ExtractRequestOptionalString(request, "orderDeleteTime", "")
orderSort := util.ExtractRequestOptionalString(request, "orderSort", "")
orderTimes := util.ExtractRequestOptionalString(request, "orderTimes", "")
orderDir := util.ExtractRequestOptionalString(request, "orderDir", "")
orderSize := util.ExtractRequestOptionalString(request, "orderSize", "")
orderName := util.ExtractRequestOptionalString(request, "orderName", "")
puuid := util.ExtractRequestString(request, "puuid")
name := util.ExtractRequestOptionalString(request, "name", "")
dir := util.ExtractRequestOptionalString(request, "dir", "")
deleted := util.ExtractRequestOptionalString(request, "deleted", "")
extensionsStr := util.ExtractRequestOptionalString(request, "extensions", "")
//auth by shareUuid.
shareUuid := util.ExtractRequestString(request, "shareUuid")
shareCode := util.ExtractRequestString(request, "shareCode")
shareRootUuid := util.ExtractRequestString(request, "shareRootUuid")
//validate the puuid.
dirMatter := this.matterDao.CheckByUuid(puuid)
if !dirMatter.Dir {
panic(result.BadRequest("puuid is not a directory"))
}
//validate the shareUuid,shareCode,shareRootUuid.
user := this.findUser(request)
share := this.shareService.ValidateMatter(request, shareUuid, shareCode, user, shareRootUuid, dirMatter)
puuid = dirMatter.Uuid
var extensions []string
if extensionsStr != "" {
extensions = strings.Split(extensionsStr, ",")
}
pager := this.matterService.Page(
request,
page,
pageSize,
orderCreateTime,
orderUpdateTime,
orderDeleteTime,
orderSort,
orderTimes,
orderDir,
orderSize,
orderName,
puuid,
name,
dir,
deleted,
extensions,
share.SpaceUuid,
)
return this.Success(pager)
}
func (this *ShareController) MatterPreviewOrDownload(writer http.ResponseWriter, request *http.Request, withContentDisposition bool) {
//auth by shareUuid.
matterUuid := util.ExtractRequestString(request, "matterUuid")
shareUuid := util.ExtractRequestString(request, "shareUuid")
shareCode := util.ExtractRequestString(request, "shareCode")
shareRootUuid := util.ExtractRequestString(request, "shareRootUuid")
matter := this.matterDao.CheckByUuid(matterUuid)
operator := this.findUser(request)
this.shareService.ValidateMatter(request, shareUuid, shareCode, operator, shareRootUuid, matter)
this.alienService.PreviewOrDownload(writer, request, matter, withContentDisposition)
}
func (this *ShareController) MatterPreview(writer http.ResponseWriter, request *http.Request) {
this.MatterPreviewOrDownload(writer, request, false)
}
func (this *ShareController) MatterDownload(writer http.ResponseWriter, request *http.Request) {
this.MatterPreviewOrDownload(writer, request, true)
}
+1
View File
@@ -33,6 +33,7 @@ type Share struct {
Code string `json:"code" gorm:"type:varchar(45) not null"`
ExpireInfinity bool `json:"expireInfinity" gorm:"type:tinyint(1) not null;default:0"`
ExpireTime time.Time `json:"expireTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
SpaceUuid string `json:"spaceUuid" gorm:"type:char(36) not null;index:idx_share_space_uuid"`
DirMatter *Matter `json:"dirMatter" gorm:"-"`
Matters []*Matter `json:"matters" gorm:"-"`
}
+11 -9
View File
@@ -11,7 +11,7 @@ import (
"time"
)
//@Service
// @Service
type ShareService struct {
BaseBean
shareDao *ShareDao
@@ -74,18 +74,13 @@ func (this *ShareService) CheckShare(request *http.Request, shareUuid string, co
return share
}
//check whether a user can access a matter. shareRootUuid is matter's parent(or parent's parent and so on)
func (this *ShareService) ValidateMatter(request *http.Request, shareUuid string, code string, user *User, shareRootUuid string, matter *Matter) {
// check whether a user can access a matter. shareRootUuid is matter's parent(or parent's parent and so on)
func (this *ShareService) ValidateMatter(request *http.Request, shareUuid string, code string, user *User, shareRootUuid string, matter *Matter) *Share {
if matter == nil {
panic(result.Unauthorized("matter cannot be nil"))
}
//if self's matter, ok.
if user != nil && matter.UserUuid == user.Uuid {
return
}
if shareUuid == "" || code == "" || shareRootUuid == "" {
panic(result.Unauthorized("shareUuid,code,shareRootUuid cannot be null"))
}
@@ -114,9 +109,11 @@ func (this *ShareService) ValidateMatter(request *http.Request, shareUuid string
}
}
return share
}
//delete user's shares and corresponding bridges.
// delete user's shares and corresponding bridges.
func (this *ShareService) DeleteSharesByUser(request *http.Request, currentUser *User) {
//delete share and bridges.
@@ -140,3 +137,8 @@ func (this *ShareService) DeleteSharesByUser(request *http.Request, currentUser
}
}
// edit space's info.
func (this *ShareService) Edit(spaceUuid string, sizeLimit int64, totalSizeLimit int64) {
}
+163
View File
@@ -0,0 +1,163 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/i18n"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
"net/http"
)
type SpaceController struct {
BaseController
spaceDao *SpaceDao
spaceMemberDao *SpaceMemberDao
spaceMemberService *SpaceMemberService
matterDao *MatterDao
matterService *MatterService
spaceService *SpaceService
userService *UserService
}
func (this *SpaceController) Init() {
this.BaseController.Init()
b := core.CONTEXT.GetBean(this.spaceDao)
if b, ok := b.(*SpaceDao); ok {
this.spaceDao = b
}
b = core.CONTEXT.GetBean(this.spaceMemberDao)
if b, ok := b.(*SpaceMemberDao); ok {
this.spaceMemberDao = b
}
b = core.CONTEXT.GetBean(this.spaceMemberService)
if b, ok := b.(*SpaceMemberService); ok {
this.spaceMemberService = b
}
b = core.CONTEXT.GetBean(this.matterDao)
if b, ok := b.(*MatterDao); ok {
this.matterDao = b
}
b = core.CONTEXT.GetBean(this.matterService)
if b, ok := b.(*MatterService); ok {
this.matterService = b
}
b = core.CONTEXT.GetBean(this.spaceService)
if b, ok := b.(*SpaceService); ok {
this.spaceService = b
}
b = core.CONTEXT.GetBean(this.userService)
if b, ok := b.(*UserService); ok {
this.userService = b
}
}
func (this *SpaceController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
routeMap["/api/space/create"] = this.Wrap(this.Create, USER_ROLE_ADMINISTRATOR)
routeMap["/api/space/edit"] = this.Wrap(this.Edit, USER_ROLE_ADMINISTRATOR)
routeMap["/api/space/delete"] = this.Wrap(this.Delete, USER_ROLE_ADMINISTRATOR)
routeMap["/api/space/detail"] = this.Wrap(this.Detail, USER_ROLE_USER)
routeMap["/api/space/page"] = this.Wrap(this.Page, USER_ROLE_USER)
return routeMap
}
func (this *SpaceController) Create(writer http.ResponseWriter, request *http.Request) *result.WebResult {
//space's name
name := util.ExtractRequestString(request, "name")
sizeLimit := util.ExtractRequestInt64(request, "sizeLimit")
totalSizeLimit := util.ExtractRequestInt64(request, "totalSizeLimit")
//create related space.
space := this.spaceService.CreateSpace(request, name, nil, sizeLimit, totalSizeLimit, SPACE_TYPE_SHARED)
return this.Success(space)
}
func (this *SpaceController) Edit(writer http.ResponseWriter, request *http.Request) *result.WebResult {
//space's uuid
uuid := util.ExtractRequestString(request, "uuid")
sizeLimit := util.ExtractRequestInt64(request, "sizeLimit")
totalSizeLimit := util.ExtractRequestInt64(request, "totalSizeLimit")
user := this.checkUser(request)
space := this.spaceService.Edit(request, user, uuid, sizeLimit, totalSizeLimit)
return this.Success(space)
}
func (this *SpaceController) Delete(writer http.ResponseWriter, request *http.Request) *result.WebResult {
//space's name
uuid := util.ExtractRequestString(request, "uuid")
space := this.spaceDao.CheckByUuid(uuid)
//when space has members, cannot delete.
memberCount := this.spaceMemberDao.CountBySpaceUuid(uuid)
if memberCount > 0 {
panic(result.BadRequest("space has members, cannot be deleted."))
}
//TODO: when space has files, cannot delete.
//delete the space.
this.spaceDao.Delete(space)
return this.Success(nil)
}
func (this *SpaceController) Detail(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := util.ExtractRequestString(request, "uuid")
user := this.checkUser(request)
space := this.spaceDao.CheckByUuid(uuid)
canRead := this.spaceMemberService.canRead(user, space.Uuid)
if !canRead {
panic(result.BadRequestI18n(request, i18n.PermissionDenied))
}
return this.Success(space)
}
func (this *SpaceController) Page(writer http.ResponseWriter, request *http.Request) *result.WebResult {
page := util.ExtractRequestOptionalInt(request, "page", 0)
pageSize := util.ExtractRequestOptionalInt(request, "pageSize", 20)
orderCreateTime := util.ExtractRequestOptionalString(request, "orderCreateTime", "")
spaceType := util.ExtractRequestOptionalString(request, "type", "")
name := util.ExtractRequestOptionalString(request, "name", "")
user := this.checkUser(request)
sortArray := []builder.OrderPair{
{
Key: "create_time",
Value: orderCreateTime,
},
}
var pager *Pager
if user.Role == USER_ROLE_USER {
if spaceType != SPACE_TYPE_SHARED {
panic(result.BadRequest("user can only query shared space type."))
}
pager = this.spaceDao.SelfPage(page, pageSize, user.Uuid, spaceType, sortArray)
} else if user.Role == USER_ROLE_ADMINISTRATOR {
pager = this.spaceDao.Page(page, pageSize, spaceType, name, sortArray)
}
return this.Success(pager)
}
+205
View File
@@ -0,0 +1,205 @@
package rest
import (
"fmt"
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/result"
"gorm.io/gorm"
"math"
"github.com/eyebluecn/tank/code/tool/uuid"
"time"
)
type SpaceDao struct {
BaseDao
}
// find by uuid. if not found return nil.
func (this *SpaceDao) FindByUuid(uuid string) *Space {
var entity = &Space{}
db := core.CONTEXT.GetDB().Where("uuid = ?", uuid).First(entity)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
panic(db.Error)
}
}
return entity
}
// find by uuid. if not found panic NotFound error
func (this *SpaceDao) CheckByUuid(uuid string) *Space {
entity := this.FindByUuid(uuid)
if entity == nil {
panic(result.NotFound("not found record with uuid = %s", uuid))
}
return entity
}
func (this *SpaceDao) CountByName(name string) int {
var count int64
db := core.CONTEXT.GetDB().
Model(&Space{}).
Where("name = ?", name).
Count(&count)
this.PanicError(db.Error)
return int(count)
}
func (this *SpaceDao) FindByName(name string) *Space {
var space = &Space{}
db := core.CONTEXT.GetDB().Where(&Space{Name: name}).First(space)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
panic(db.Error)
}
}
return space
}
func (this *SpaceDao) CountByUserUuid(userUuid string) int {
var count int64
db := core.CONTEXT.GetDB().
Model(&Space{}).
Where("user_uuid = ?", userUuid).
Count(&count)
this.PanicError(db.Error)
return int(count)
}
// TODO:
func (this *SpaceDao) SelfPage(page int, pageSize int, userUuid string, spaceType string, sortArray []builder.OrderPair) *Pager {
countSqlTemplate := fmt.Sprintf("SELECT COUNT(*) FROM `%sspace` WHERE uuid IN (SELECT space_uuid FROM `%sspace_member` WHERE user_uuid = ?) AND type = ?", core.TABLE_PREFIX, core.TABLE_PREFIX)
if spaceType == SPACE_TYPE_PRIVATE {
countSqlTemplate = fmt.Sprintf("SELECT COUNT(*) FROM `%sspace` WHERE user_uuid = ? AND type = ?", core.TABLE_PREFIX)
}
var count int
core.CONTEXT.GetDB().Raw(countSqlTemplate, userUuid, spaceType).Scan(&count)
orderByString := this.GetSortString(sortArray)
if orderByString == "" {
orderByString = "uuid"
}
querySqlTemplate := fmt.Sprintf("SELECT * FROM `%sspace` WHERE uuid IN (SELECT space_uuid FROM `%sspace_member` WHERE user_uuid = ?) AND type = ? ORDER BY ? LIMIT ?,?", core.TABLE_PREFIX, core.TABLE_PREFIX)
if spaceType == SPACE_TYPE_PRIVATE {
querySqlTemplate = fmt.Sprintf("SELECT * FROM `%sspace` WHERE user_uuid = ? AND type = ? ORDER BY ? LIMIT ?,?", core.TABLE_PREFIX)
}
var spaces []*Space
core.CONTEXT.GetDB().Raw(querySqlTemplate, userUuid, spaceType, orderByString, page*pageSize, pageSize).Scan(&spaces)
pager := NewPager(page, pageSize, count, spaces)
return pager
}
func (this *SpaceDao) Page(page int, pageSize int, spaceType string, name string, sortArray []builder.OrderPair) *Pager {
count, spaces := this.PlainPage(page, pageSize, spaceType, name, sortArray)
pager := NewPager(page, pageSize, count, spaces)
return pager
}
func (this *SpaceDao) PlainPage(page int, pageSize int, spaceType string, name string, sortArray []builder.OrderPair) (int, []*Space) {
var wp = &builder.WherePair{}
if spaceType != "" {
wp = wp.And(&builder.WherePair{Query: "type = ?", Args: []interface{}{spaceType}})
}
if name != "" {
wp = wp.And(&builder.WherePair{Query: "name LIKE ?", Args: []interface{}{"%" + name + "%"}})
}
var conditionDB *gorm.DB
conditionDB = core.CONTEXT.GetDB().Model(&Space{}).Where(wp.Query, wp.Args...)
var count int64 = 0
db := conditionDB.Count(&count)
this.PanicError(db.Error)
var spaces []*Space
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&spaces)
this.PanicError(db.Error)
return int(count), spaces
}
func (this *SpaceDao) Create(space *Space) *Space {
timeUUID, _ := uuid.NewV4()
space.Uuid = string(timeUUID.String())
space.CreateTime = time.Now()
space.UpdateTime = time.Now()
space.Sort = time.Now().UnixNano() / 1e6
db := core.CONTEXT.GetDB().Create(space)
this.PanicError(db.Error)
return space
}
func (this *SpaceDao) Save(space *Space) *Space {
space.UpdateTime = time.Now()
db := core.CONTEXT.GetDB().Save(space)
this.PanicError(db.Error)
return space
}
func (this *SpaceDao) UpdateTotalSize(spaceUuid string, totalSize int64) {
db := core.CONTEXT.GetDB().Model(&Space{}).Where("uuid = ?", spaceUuid).Update("total_size", totalSize)
this.PanicError(db.Error)
}
// handle user page by page.
func (this *SpaceDao) PageHandle(fun func(space *Space)) {
pageSize := 1000
sortArray := []builder.OrderPair{
{
Key: "uuid",
Value: DIRECTION_ASC,
},
}
count, _ := this.PlainPage(0, pageSize, "", "", sortArray)
if count > 0 {
var totalPages = int(math.Ceil(float64(count) / float64(pageSize)))
var page int
for page = 0; page < totalPages; page++ {
_, spaces := this.PlainPage(0, pageSize, "", "", sortArray)
for _, space := range spaces {
fun(space)
}
}
}
}
func (this *SpaceDao) DeleteByUserUuid(userUuid string) {
db := core.CONTEXT.GetDB().Where("user_uuid = ?", userUuid).Delete(Space{})
this.PanicError(db.Error)
}
func (this *SpaceDao) Delete(space *Space) {
db := core.CONTEXT.GetDB().Delete(&space)
this.PanicError(db.Error)
}
// System cleanup.
func (this *SpaceDao) Cleanup() {
this.logger.Info("[SpaceDao] clean up. Delete all Space")
db := core.CONTEXT.GetDB().Where("uuid is not null").Delete(Space{})
this.PanicError(db.Error)
}
+219
View File
@@ -0,0 +1,219 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/i18n"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
"net/http"
"strings"
)
type SpaceMemberController struct {
BaseController
spaceMemberDao *SpaceMemberDao
spaceDao *SpaceDao
bridgeDao *BridgeDao
matterDao *MatterDao
matterService *MatterService
spaceMemberService *SpaceMemberService
}
func (this *SpaceMemberController) Init() {
this.BaseController.Init()
b := core.CONTEXT.GetBean(this.spaceMemberDao)
if b, ok := b.(*SpaceMemberDao); ok {
this.spaceMemberDao = b
}
b = core.CONTEXT.GetBean(this.spaceDao)
if b, ok := b.(*SpaceDao); ok {
this.spaceDao = b
}
b = core.CONTEXT.GetBean(this.bridgeDao)
if b, ok := b.(*BridgeDao); ok {
this.bridgeDao = b
}
b = core.CONTEXT.GetBean(this.matterDao)
if b, ok := b.(*MatterDao); ok {
this.matterDao = b
}
b = core.CONTEXT.GetBean(this.matterService)
if b, ok := b.(*MatterService); ok {
this.matterService = b
}
b = core.CONTEXT.GetBean(this.spaceMemberService)
if b, ok := b.(*SpaceMemberService); ok {
this.spaceMemberService = b
}
}
func (this *SpaceMemberController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
//admin user can create/edit/delete
routeMap["/api/space/member/create"] = this.Wrap(this.Create, USER_ROLE_USER)
routeMap["/api/space/member/edit"] = this.Wrap(this.Edit, USER_ROLE_USER)
routeMap["/api/space/member/delete"] = this.Wrap(this.Delete, USER_ROLE_USER)
routeMap["/api/space/member/detail"] = this.Wrap(this.Detail, USER_ROLE_USER)
routeMap["/api/space/member/mine"] = this.Wrap(this.Mine, USER_ROLE_USER)
routeMap["/api/space/member/page"] = this.Wrap(this.Page, USER_ROLE_USER)
return routeMap
}
func (this *SpaceMemberController) Create(writer http.ResponseWriter, request *http.Request) *result.WebResult {
spaceUuid := util.ExtractRequestString(request, "spaceUuid")
userUuidsStr := util.ExtractRequestString(request, "userUuids")
spaceRole := util.ExtractRequestString(request, "role")
if spaceRole != SPACE_MEMBER_ROLE_READ_ONLY && spaceRole != SPACE_MEMBER_ROLE_READ_WRITE && spaceRole != SPACE_MEMBER_ROLE_ADMIN {
panic("spaceRole is not correct")
}
//validate userUuids
if userUuidsStr == "" {
panic("userUuids is required")
}
userUuids := strings.Split(userUuidsStr, ",")
// check operator's permission
currentUser := this.checkUser(request)
canManage := this.spaceMemberService.canManage(currentUser, spaceUuid)
if !canManage {
panic(result.BadRequestI18n(request, i18n.PermissionDenied))
}
//check whether exists.
for _, userUuid := range userUuids {
spaceMember := this.spaceMemberDao.FindBySpaceUuidAndUserUuid(spaceUuid, userUuid)
user := this.userDao.CheckByUuid(userUuid)
if spaceMember != nil {
panic(result.BadRequestI18n(request, i18n.SpaceMemberExist, user.Username))
}
}
//check whether space exists.
space := this.spaceDao.CheckByUuid(spaceUuid)
//check whether exists.
for _, userUuid := range userUuids {
user := this.userDao.CheckByUuid(userUuid)
this.spaceMemberService.CreateMember(space, user, spaceRole)
}
return this.Success("OK")
}
func (this *SpaceMemberController) Edit(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := util.ExtractRequestString(request, "uuid")
spaceRole := util.ExtractRequestString(request, "role")
if spaceRole != SPACE_MEMBER_ROLE_READ_ONLY && spaceRole != SPACE_MEMBER_ROLE_READ_WRITE && spaceRole != SPACE_MEMBER_ROLE_ADMIN {
panic("spaceRole is not correct")
}
spaceMember := this.spaceMemberDao.CheckByUuid(uuid)
currentUser := this.checkUser(request)
canManage := this.spaceMemberService.canManage(currentUser, spaceMember.SpaceUuid)
if !canManage {
panic(result.BadRequestI18n(request, i18n.PermissionDenied))
}
spaceMember.Role = spaceRole
spaceMember = this.spaceMemberDao.Save(spaceMember)
return this.Success(spaceMember)
}
func (this *SpaceMemberController) Delete(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := util.ExtractRequestString(request, "uuid")
spaceMember := this.spaceMemberDao.CheckByUuid(uuid)
user := this.checkUser(request)
canManage := this.spaceMemberService.canManage(user, spaceMember.SpaceUuid)
if !canManage {
panic(result.BadRequestI18n(request, i18n.PermissionDenied))
}
this.spaceMemberDao.Delete(spaceMember)
return this.Success("OK")
}
func (this *SpaceMemberController) Detail(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := util.ExtractRequestString(request, "uuid")
spaceMember := this.spaceMemberDao.CheckByUuid(uuid)
user := this.checkUser(request)
if spaceMember.UserUuid != user.Uuid {
panic(result.UNAUTHORIZED)
}
return this.Success(spaceMember)
}
// find my role in the space.
func (this *SpaceMemberController) Mine(writer http.ResponseWriter, request *http.Request) *result.WebResult {
spaceUuid := util.ExtractRequestString(request, "spaceUuid")
user := this.checkUser(request)
spaceMember := this.spaceMemberDao.FindBySpaceUuidAndUserUuid(spaceUuid, user.Uuid)
if spaceMember == nil {
spaceMember = &SpaceMember{SpaceUuid: spaceUuid, Role: SPACE_MEMBER_GUEST}
}
if user.Role == USER_ROLE_ADMINISTRATOR {
spaceMember.Role = SPACE_MEMBER_ROLE_ADMIN
}
return this.Success(spaceMember)
}
func (this *SpaceMemberController) Page(writer http.ResponseWriter, request *http.Request) *result.WebResult {
page := util.ExtractRequestOptionalInt(request, "page", 0)
pageSize := util.ExtractRequestOptionalInt(request, "pageSize", 20)
orderCreateTime := util.ExtractRequestOptionalString(request, "orderCreateTime", "")
spaceUuid := util.ExtractRequestString(request, "spaceUuid")
user := this.checkUser(request)
canRead := this.spaceMemberService.canRead(user, spaceUuid)
if !canRead {
panic(result.BadRequestI18n(request, i18n.PermissionDenied))
}
sortArray := []builder.OrderPair{
{
Key: "create_time",
Value: orderCreateTime,
},
}
pager := this.spaceMemberDao.Page(page, pageSize, spaceUuid, sortArray)
//fill the space's user. FIXME: user better way to get User.
if pager != nil {
for _, spaceMember := range pager.Data.([]*SpaceMember) {
spaceMember.User = this.userDao.FindByUuid(spaceMember.UserUuid)
}
}
return this.Success(pager)
}
+139
View File
@@ -0,0 +1,139 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/result"
"gorm.io/gorm"
"github.com/eyebluecn/tank/code/tool/uuid"
"time"
)
type SpaceMemberDao struct {
BaseDao
}
// find by uuid. if not found return nil.
func (this *SpaceMemberDao) FindByUuid(uuid string) *SpaceMember {
var entity = &SpaceMember{}
db := core.CONTEXT.GetDB().Where("uuid = ?", uuid).First(entity)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
panic(db.Error)
}
}
return entity
}
// find by uuid. if not found panic NotFound error
func (this *SpaceMemberDao) CheckByUuid(uuid string) *SpaceMember {
entity := this.FindByUuid(uuid)
if entity == nil {
panic(result.NotFound("not found record with uuid = %s", uuid))
}
return entity
}
// find by spaceUuid and userUuid. if not found return nil.
func (this *SpaceMemberDao) FindBySpaceUuidAndUserUuid(spaceUuid string, userUuid string) *SpaceMember {
var entity = &SpaceMember{}
db := core.CONTEXT.GetDB().Where("space_uuid = ? AND user_uuid = ?", spaceUuid, userUuid).First(entity)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
panic(db.Error)
}
}
return entity
}
func (this *SpaceMemberDao) Page(page int, pageSize int, spaceUuid string, sortArray []builder.OrderPair) *Pager {
count, spaceMembers := this.PlainPage(page, pageSize, spaceUuid, sortArray)
pager := NewPager(page, pageSize, count, spaceMembers)
return pager
}
func (this *SpaceMemberDao) PlainPage(page int, pageSize int, spaceUuid string, sortArray []builder.OrderPair) (int, []*SpaceMember) {
var wp = &builder.WherePair{}
if spaceUuid != "" {
wp = wp.And(&builder.WherePair{Query: "space_uuid = ?", Args: []interface{}{spaceUuid}})
}
var conditionDB *gorm.DB
conditionDB = core.CONTEXT.GetDB().Model(&SpaceMember{}).Where(wp.Query, wp.Args...)
var count int64 = 0
db := conditionDB.Count(&count)
this.PanicError(db.Error)
var spaceMembers []*SpaceMember
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&spaceMembers)
this.PanicError(db.Error)
return int(count), spaceMembers
}
func (this *SpaceMemberDao) Create(spaceMember *SpaceMember) *SpaceMember {
timeUUID, _ := uuid.NewV4()
spaceMember.Uuid = string(timeUUID.String())
spaceMember.CreateTime = time.Now()
spaceMember.UpdateTime = time.Now()
spaceMember.Sort = time.Now().UnixNano() / 1e6
db := core.CONTEXT.GetDB().Create(spaceMember)
this.PanicError(db.Error)
return spaceMember
}
func (this *SpaceMemberDao) Save(spaceMember *SpaceMember) *SpaceMember {
spaceMember.UpdateTime = time.Now()
db := core.CONTEXT.GetDB().Save(spaceMember)
this.PanicError(db.Error)
return spaceMember
}
func (this *SpaceMemberDao) Delete(spaceMember *SpaceMember) {
db := core.CONTEXT.GetDB().Delete(&spaceMember)
this.PanicError(db.Error)
}
func (this *SpaceMemberDao) DeleteBySpaceUuid(spaceUuid string) {
var wp = &builder.WherePair{}
wp = wp.And(&builder.WherePair{Query: "space_uuid = ?", Args: []interface{}{spaceUuid}})
db := core.CONTEXT.GetDB().Where(wp.Query, wp.Args).Delete(SpaceMember{})
this.PanicError(db.Error)
}
func (this *SpaceMemberDao) CountBySpaceUuid(spaceUuid string) int {
var count int64
db := core.CONTEXT.GetDB().
Model(&SpaceMember{}).
Where("space_uuid = ?", spaceUuid).
Count(&count)
this.PanicError(db.Error)
return int(count)
}
// System cleanup.
func (this *SpaceMemberDao) Cleanup() {
this.logger.Info("[SpaceMemberDao] clean up. Delete all SpaceMember")
db := core.CONTEXT.GetDB().Where("uuid is not null").Delete(SpaceMember{})
this.PanicError(db.Error)
}
+30
View File
@@ -0,0 +1,30 @@
package rest
import (
"time"
)
const (
//guest member
SPACE_MEMBER_GUEST = "GUEST"
//read only member
SPACE_MEMBER_ROLE_READ_ONLY = "READ_ONLY"
//read write member
SPACE_MEMBER_ROLE_READ_WRITE = "READ_WRITE"
//admin member
SPACE_MEMBER_ROLE_ADMIN = "ADMIN"
)
/**
* space member
*/
type SpaceMember struct {
Uuid string `json:"uuid" gorm:"type:char(36);primary_key;unique"`
Sort int64 `json:"sort" gorm:"type:bigint(20) not null"`
UpdateTime time.Time `json:"updateTime" gorm:"type:timestamp not null;default:CURRENT_TIMESTAMP"`
CreateTime time.Time `json:"createTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
SpaceUuid string `json:"spaceUuid" gorm:"type:char(36);index:idx_space_member_su"`
UserUuid string `json:"userUuid" gorm:"type:char(36);index:idx_space_member_uu"`
Role string `json:"role" gorm:"type:varchar(45)"`
User *User `json:"user" gorm:"-"`
}
+145
View File
@@ -0,0 +1,145 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
)
// @Service
type SpaceMemberService struct {
BaseBean
spaceMemberDao *SpaceMemberDao
matterDao *MatterDao
bridgeDao *BridgeDao
userDao *UserDao
}
func (this *SpaceMemberService) Init() {
this.BaseBean.Init()
b := core.CONTEXT.GetBean(this.spaceMemberDao)
if b, ok := b.(*SpaceMemberDao); ok {
this.spaceMemberDao = b
}
b = core.CONTEXT.GetBean(this.matterDao)
if b, ok := b.(*MatterDao); ok {
this.matterDao = b
}
b = core.CONTEXT.GetBean(this.bridgeDao)
if b, ok := b.(*BridgeDao); ok {
this.bridgeDao = b
}
b = core.CONTEXT.GetBean(this.userDao)
if b, ok := b.(*UserDao); ok {
this.userDao = b
}
}
func (this *SpaceMemberService) Detail(uuid string) *SpaceMember {
spaceMember := this.spaceMemberDao.CheckByUuid(uuid)
return spaceMember
}
// create space
func (this *SpaceMemberService) CreateMember(space *Space, user *User, spaceRole string) *SpaceMember {
spaceMember := &SpaceMember{
SpaceUuid: space.Uuid,
UserUuid: user.Uuid,
Role: spaceRole,
}
spaceMember = this.spaceMemberDao.Create(spaceMember)
return spaceMember
}
// 当前用户对于此空间,是否有管理权限。
func (this *SpaceMemberService) canManage(user *User, spaceUuid string) bool {
if user.Role == USER_ROLE_ADMINISTRATOR {
return true
}
if user.SpaceUuid == spaceUuid {
return true
}
//only space's admin can add member.
spaceMember := this.spaceMemberDao.FindBySpaceUuidAndUserUuid(spaceUuid, user.Uuid)
return this.canManageBySpaceMember(user, spaceMember)
}
// 当前用户对于此空间,是否有可读权限。
func (this *SpaceMemberService) canRead(user *User, spaceUuid string) bool {
if user.Role == USER_ROLE_ADMINISTRATOR {
return true
}
if user.SpaceUuid == spaceUuid {
return true
}
//only space's admin can add member.
spaceMember := this.spaceMemberDao.FindBySpaceUuidAndUserUuid(spaceUuid, user.Uuid)
return this.canReadBySpaceMember(user, spaceMember)
}
// 当前用户对于此空间,是否有可写权限。
func (this *SpaceMemberService) canWrite(user *User, spaceUuid string) bool {
if user.Role == USER_ROLE_ADMINISTRATOR {
return true
}
if user.SpaceUuid == spaceUuid {
return true
}
//only space's admin can add member.
spaceMember := this.spaceMemberDao.FindBySpaceUuidAndUserUuid(spaceUuid, user.Uuid)
return this.canWriteBySpaceMember(user, spaceMember)
}
// 当前用户对于此空间,是否有管理权限。
func (this *SpaceMemberService) canManageBySpaceMember(user *User, member *SpaceMember) bool {
if user.Role == USER_ROLE_ADMINISTRATOR {
return true
}
//only space's admin can add member.
if member != nil && member.Role == SPACE_MEMBER_ROLE_ADMIN {
return true
}
return false
}
// 当前用户对于此空间,是否有可读权限。
func (this *SpaceMemberService) canReadBySpaceMember(user *User, member *SpaceMember) bool {
if user.Role == USER_ROLE_ADMINISTRATOR {
return true
}
//only space's admin can add member.
if member != nil {
return true
}
return false
}
// 当前用户对于此空间,是否有科协权限。
func (this *SpaceMemberService) canWriteBySpaceMember(user *User, member *SpaceMember) bool {
if user.Role == USER_ROLE_ADMINISTRATOR {
return true
}
//only space's admin can add member.
if member != nil && (member.Role == SPACE_MEMBER_ROLE_ADMIN || member.Role == SPACE_MEMBER_ROLE_READ_WRITE) {
return true
}
return false
}
+29
View File
@@ -0,0 +1,29 @@
package rest
import (
"time"
)
const (
//private space
SPACE_TYPE_PRIVATE = "PRIVATE"
//group shared space.
SPACE_TYPE_SHARED = "SHARED"
)
/**
* shared space
*/
type Space struct {
Uuid string `json:"uuid" gorm:"type:char(36);primary_key;unique"`
Sort int64 `json:"sort" gorm:"type:bigint(20) not null"`
UpdateTime time.Time `json:"updateTime" gorm:"type:timestamp not null;default:CURRENT_TIMESTAMP"`
CreateTime time.Time `json:"createTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
Name string `json:"name" gorm:"type:varchar(100) not null;unique"`
UserUuid string `json:"userUuid" gorm:"type:char(36)"`
SizeLimit int64 `json:"sizeLimit" gorm:"type:bigint(20) not null;default:-1"`
TotalSizeLimit int64 `json:"totalSizeLimit" gorm:"type:bigint(20) not null;default:-1"`
TotalSize int64 `json:"totalSize" gorm:"type:bigint(20) not null;default:0"`
Type string `json:"type" gorm:"type:varchar(45)"`
User *User `json:"user" gorm:"-"`
}
+170
View File
@@ -0,0 +1,170 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/i18n"
"github.com/eyebluecn/tank/code/tool/result"
"net/http"
"regexp"
)
// @Service
type SpaceService struct {
BaseBean
spaceDao *SpaceDao
spaceMemberService *SpaceMemberService
matterDao *MatterDao
bridgeDao *BridgeDao
userDao *UserDao
}
func (this *SpaceService) Init() {
this.BaseBean.Init()
b := core.CONTEXT.GetBean(this.spaceDao)
if b, ok := b.(*SpaceDao); ok {
this.spaceDao = b
}
b = core.CONTEXT.GetBean(this.spaceMemberService)
if b, ok := b.(*SpaceMemberService); ok {
this.spaceMemberService = b
}
b = core.CONTEXT.GetBean(this.matterDao)
if b, ok := b.(*MatterDao); ok {
this.matterDao = b
}
b = core.CONTEXT.GetBean(this.bridgeDao)
if b, ok := b.(*BridgeDao); ok {
this.bridgeDao = b
}
b = core.CONTEXT.GetBean(this.userDao)
if b, ok := b.(*UserDao); ok {
this.userDao = b
}
}
func (this *SpaceService) Detail(uuid string) *Space {
space := this.spaceDao.CheckByUuid(uuid)
return space
}
// create space
func (this *SpaceService) CreateSpace(
request *http.Request,
name string,
user *User,
sizeLimit int64,
totalSizeLimit int64,
spaceType string) *Space {
userUuid := ""
//validation work.
if m, _ := regexp.MatchString(USERNAME_PATTERN, name); !m {
panic(result.BadRequestI18n(request, i18n.SpaceNameError))
}
if spaceType == SPACE_TYPE_PRIVATE {
if user == nil {
panic("private space requires user.")
}
userUuid = user.Uuid
if this.spaceDao.CountByUserUuid(userUuid) > 0 {
panic(result.BadRequestI18n(request, i18n.SpaceExclusive, name))
}
} else if spaceType == SPACE_TYPE_SHARED {
} else {
panic("Not supported spaceType:" + spaceType)
}
if this.spaceDao.CountByName(name) > 0 {
panic(result.BadRequestI18n(request, i18n.SpaceNameExist, name))
}
space := &Space{
Name: name,
UserUuid: userUuid,
SizeLimit: sizeLimit,
TotalSizeLimit: totalSizeLimit,
TotalSize: 0,
Type: spaceType,
}
space = this.spaceDao.Create(space)
return space
}
// checkout a adminAble space.
func (this *SpaceService) CheckAdminAbleByUuid(request *http.Request, user *User, spaceUuid string) *Space {
space := this.spaceDao.CheckByUuid(spaceUuid)
if space.Type == SPACE_TYPE_PRIVATE && user.Uuid == space.UserUuid {
return space
}
manage := this.spaceMemberService.canManage(user, spaceUuid)
if !manage {
panic(result.BadRequestI18n(request, i18n.PermissionDenied))
}
return space
}
// checkout a writable space.
func (this *SpaceService) CheckWritableByUuid(request *http.Request, user *User, spaceUuid string) *Space {
space := this.spaceDao.CheckByUuid(spaceUuid)
if space.Type == SPACE_TYPE_PRIVATE && user.Uuid == space.UserUuid {
return space
}
writable := this.spaceMemberService.canWrite(user, spaceUuid)
if !writable {
panic(result.BadRequestI18n(request, i18n.PermissionDenied))
}
return space
}
// checkout a readable space.
func (this *SpaceService) CheckReadableByUuid(request *http.Request, user *User, spaceUuid string) *Space {
space := this.spaceDao.CheckByUuid(spaceUuid)
if space.Type == SPACE_TYPE_PRIVATE && user.Uuid == space.UserUuid {
return space
}
manage := this.spaceMemberService.canRead(user, spaceUuid)
if !manage {
panic(result.BadRequestI18n(request, i18n.PermissionDenied))
}
return space
}
// edit space's info
func (this *SpaceService) Edit(request *http.Request, user *User, spaceUuid string, sizeLimit int64, totalSizeLimit int64) *Space {
space := this.CheckAdminAbleByUuid(request, user, spaceUuid)
if sizeLimit < 0 && sizeLimit != -1 {
panic("sizeLimit cannot be negative expect -1.")
}
if totalSizeLimit < 0 && totalSizeLimit != -1 {
panic("totalSizeLimit cannot be negative expect -1.")
}
space.SizeLimit = sizeLimit
space.TotalSizeLimit = totalSizeLimit
space = this.spaceDao.Save(space)
return space
}
+55 -18
View File
@@ -8,14 +8,16 @@ import (
)
// system tasks service
//@Service
// @Service
type TaskService struct {
BaseBean
footprintService *FootprintService
dashboardService *DashboardService
preferenceService *PreferenceService
matterService *MatterService
chunkService *ChunkService
userDao *UserDao
spaceDao *SpaceDao
//whether scan task is running
scanTaskRunning bool
@@ -44,15 +46,25 @@ func (this *TaskService) Init() {
if b, ok := b.(*MatterService); ok {
this.matterService = b
}
b = core.CONTEXT.GetBean(this.chunkService)
if b, ok := b.(*ChunkService); ok {
this.chunkService = b
}
b = core.CONTEXT.GetBean(this.userDao)
if b, ok := b.(*UserDao); ok {
this.userDao = b
}
b = core.CONTEXT.GetBean(this.spaceDao)
if b, ok := b.(*SpaceDao); ok {
this.spaceDao = b
}
this.scanTaskRunning = false
}
//init the clean footprint task.
// init the clean footprint task.
func (this *TaskService) InitCleanFootprintTask() {
//use standard cron expression. 5 fields. ()
@@ -65,7 +77,7 @@ func (this *TaskService) InitCleanFootprintTask() {
this.logger.Info("[cron job] Every day 00:10 delete Footprint data of 8 days ago.")
}
//init the elt task.
// init the elt task.
func (this *TaskService) InitEtlTask() {
expression := "5 0 * * *"
@@ -77,7 +89,7 @@ func (this *TaskService) InitEtlTask() {
this.logger.Info("[cron job] Everyday 00:05 ETL dashboard data.")
}
//init the clean deleted matters task.
// init the clean deleted matters task.
func (this *TaskService) InitCleanDeletedMattersTask() {
expression := "0 1 * * *"
@@ -89,7 +101,19 @@ func (this *TaskService) InitCleanDeletedMattersTask() {
this.logger.Info("[cron job] Everyday 01:00 Clean deleted matters.")
}
//scan task.
// init the clean chunk sessions task.
func (this *TaskService) InitCleanExpiredChunkSessions() {
expression := "0 2 * * *"
cronJob := cron.New()
_, err := cronJob.AddFunc(expression, this.chunkService.CleanExpiredSessions)
core.PanicError(err)
cronJob.Start()
this.logger.Info("[cron job] Everyday 02:00 Clean expired chunk sessions.")
}
// scan task.
func (this *TaskService) doScanTask() {
if this.scanTaskRunning {
@@ -121,31 +145,41 @@ func (this *TaskService) doScanTask() {
if scanConfig.Scope == SCAN_SCOPE_ALL {
//scan all user's root folder.
this.userDao.PageHandle("", "", func(user *User) {
this.spaceDao.PageHandle(func(space *Space) {
core.RunWithRecovery(func() {
this.matterService.DeleteByPhysics(request, user)
this.matterService.ScanPhysics(request, user)
this.logger.Info("scan spaceName = %s", space.Name)
//find user by space
user := this.userDao.FindByUuid(space.UserUuid)
if user == nil {
user = this.userDao.FindAnAdmin()
}
this.matterService.DeleteByPhysics(request, user, space)
this.matterService.ScanPhysics(request, user, space)
})
})
} else if scanConfig.Scope == SCAN_SCOPE_CUSTOM {
//scan custom user's folder.
for _, username := range scanConfig.Usernames {
user := this.userDao.FindByUsername(username)
if user == nil {
this.logger.Error("username = %s not exist.", username)
for _, spaceName := range scanConfig.SpaceNames {
space := this.spaceDao.FindByName(spaceName)
if space == nil {
this.logger.Error("name = %s not exist.", spaceName)
} else {
this.logger.Info("scan custom user folder. username = %s", username)
this.logger.Info("scan custom user folder. spaceName = %s", spaceName)
core.RunWithRecovery(func() {
this.matterService.DeleteByPhysics(request, user)
this.matterService.ScanPhysics(request, user)
//find user by space
user := this.userDao.FindByUuid(space.UserUuid)
if user == nil {
user = this.userDao.FindAnAdmin()
}
this.matterService.DeleteByPhysics(request, user, space)
this.matterService.ScanPhysics(request, user, space)
})
@@ -155,7 +189,7 @@ func (this *TaskService) doScanTask() {
}
//init the scan task.
// init the scan task.
func (this *TaskService) InitScanTask() {
if this.scanTaskCron != nil {
@@ -195,6 +229,9 @@ func (this *TaskService) Bootstrap() {
//load the clean deleted matters task.
this.InitCleanDeletedMattersTask()
//load the clean chunk sessions task.
this.InitCleanExpiredChunkSessions()
//load the scan task.
this.InitScanTask()
+91 -81
View File
@@ -16,6 +16,8 @@ type UserController struct {
BaseController
preferenceService *PreferenceService
userService *UserService
spaceDao *SpaceDao
spaceService *SpaceService
matterService *MatterService
}
@@ -32,6 +34,14 @@ func (this *UserController) Init() {
this.userService = b
}
b = core.CONTEXT.GetBean(this.spaceDao)
if b, ok := b.(*SpaceDao); ok {
this.spaceDao = b
}
b = core.CONTEXT.GetBean(this.spaceService)
if b, ok := b.(*SpaceService); ok {
this.spaceService = b
}
b = core.CONTEXT.GetBean(this.matterService)
if b, ok := b.(*MatterService); ok {
this.matterService = b
@@ -54,6 +64,7 @@ func (this *UserController) RegisterRoutes() map[string]func(writer http.Respons
routeMap["/api/user/change/password"] = this.Wrap(this.ChangePassword, USER_ROLE_USER)
routeMap["/api/user/reset/password"] = this.Wrap(this.ResetPassword, USER_ROLE_ADMINISTRATOR)
routeMap["/api/user/page"] = this.Wrap(this.Page, USER_ROLE_ADMINISTRATOR)
routeMap["/api/user/search"] = this.Wrap(this.Search, USER_ROLE_USER)
routeMap["/api/user/toggle/status"] = this.Wrap(this.ToggleStatus, USER_ROLE_ADMINISTRATOR)
routeMap["/api/user/transfiguration"] = this.Wrap(this.Transfiguration, USER_ROLE_ADMINISTRATOR)
routeMap["/api/user/scan"] = this.Wrap(this.Scan, USER_ROLE_ADMINISTRATOR)
@@ -96,7 +107,7 @@ func (this *UserController) innerLogin(writer http.ResponseWriter, request *http
this.userDao.Save(user)
}
//login by username and password
// login by username and password
func (this *UserController) Login(writer http.ResponseWriter, request *http.Request) *result.WebResult {
username := request.FormValue("username")
@@ -116,10 +127,14 @@ func (this *UserController) Login(writer http.ResponseWriter, request *http.Requ
}
this.innerLogin(writer, request, user)
//append the space info.
space := this.spaceDao.FindByUuid(user.SpaceUuid)
user.Space = space
return this.Success(user)
}
//login by authentication.
// login by authentication.
func (this *UserController) AuthenticationLogin(writer http.ResponseWriter, request *http.Request) *result.WebResult {
authentication := request.FormValue("authentication")
@@ -140,13 +155,18 @@ func (this *UserController) AuthenticationLogin(writer http.ResponseWriter, requ
return this.Success(user)
}
//fetch current user's info.
// fetch current user's info.
func (this *UserController) Info(writer http.ResponseWriter, request *http.Request) *result.WebResult {
user := this.checkUser(request)
//append the space info.
space := this.spaceDao.FindByUuid(user.SpaceUuid)
user.Space = space
return this.Success(user)
}
//register by username and password. After registering, will auto login.
// register by username and password. After registering, will auto login.
func (this *UserController) Register(writer http.ResponseWriter, request *http.Request) *result.WebResult {
username := request.FormValue("username")
@@ -169,15 +189,7 @@ func (this *UserController) Register(writer http.ResponseWriter, request *http.R
panic(result.BadRequestI18n(request, i18n.UsernameExist, username))
}
user := &User{
Role: USER_ROLE_USER,
Username: username,
Password: util.GetBcrypt(password),
TotalSizeLimit: preference.DefaultTotalSizeLimit,
Status: USER_STATUS_OK,
}
user = this.userDao.Create(user)
user := this.userService.CreateUser(request, username, -1, preference.DefaultTotalSizeLimit, password, USER_ROLE_USER)
//auto login
this.innerLogin(writer, request, user)
@@ -190,32 +202,11 @@ func (this *UserController) Create(writer http.ResponseWriter, request *http.Req
username := request.FormValue("username")
password := request.FormValue("password")
role := request.FormValue("role")
sizeLimitStr := request.FormValue("sizeLimit")
totalSizeLimitStr := request.FormValue("totalSizeLimit")
//only admin can edit user's sizeLimit
var sizeLimit int64 = 0
if sizeLimitStr == "" {
panic("user's limit size is required")
} else {
intSizeLimit, err := strconv.Atoi(sizeLimitStr)
if err != nil {
this.PanicError(err)
}
sizeLimit = int64(intSizeLimit)
}
var totalSizeLimit int64 = 0
if totalSizeLimitStr == "" {
panic("user's total limit size is required")
} else {
intTotalSizeLimit, err := strconv.Atoi(totalSizeLimitStr)
if err != nil {
this.PanicError(err)
}
totalSizeLimit = int64(intTotalSizeLimit)
}
sizeLimit := util.ExtractRequestInt64(request, "sizeLimit")
totalSizeLimit := util.ExtractRequestInt64(request, "totalSizeLimit")
//validation work.
if m, _ := regexp.MatchString(USERNAME_PATTERN, username); !m {
panic(result.BadRequestI18n(request, i18n.UsernameError))
}
@@ -227,21 +218,16 @@ func (this *UserController) Create(writer http.ResponseWriter, request *http.Req
if this.userDao.CountByUsername(username) > 0 {
panic(result.BadRequestI18n(request, i18n.UsernameExist, username))
}
if this.spaceDao.CountByName(username) > 0 {
panic(result.BadRequestI18n(request, i18n.SpaceNameExist, username))
}
//check user role.
if role != USER_ROLE_USER && role != USER_ROLE_ADMINISTRATOR {
role = USER_ROLE_USER
panic(result.BadRequestI18n(request, i18n.UserRoleError))
}
user := &User{
Username: username,
Password: util.GetBcrypt(password),
Role: role,
SizeLimit: sizeLimit,
TotalSizeLimit: totalSizeLimit,
Status: USER_STATUS_OK,
}
user = this.userDao.Create(user)
user := this.userService.CreateUser(request, username, sizeLimit, totalSizeLimit, password, role)
return this.Success(user)
}
@@ -250,54 +236,50 @@ func (this *UserController) Edit(writer http.ResponseWriter, request *http.Reque
uuid := request.FormValue("uuid")
avatarUrl := request.FormValue("avatarUrl")
sizeLimitStr := request.FormValue("sizeLimit")
totalSizeLimitStr := request.FormValue("totalSizeLimit")
role := request.FormValue("role")
user := this.checkUser(request)
sizeLimit := util.ExtractRequestInt64(request, "sizeLimit")
totalSizeLimit := util.ExtractRequestInt64(request, "totalSizeLimit")
operator := this.checkUser(request)
currentUser := this.userDao.CheckByUuid(uuid)
currentUser.AvatarUrl = avatarUrl
if user.Role == USER_ROLE_ADMINISTRATOR {
//only admin can edit user's sizeLimit
var sizeLimit int64 = 0
if sizeLimitStr == "" {
panic("user's limit size is required")
} else {
intSizeLimit, err := strconv.Atoi(sizeLimitStr)
if err != nil {
this.PanicError(err)
}
sizeLimit = int64(intSizeLimit)
}
currentUser.SizeLimit = sizeLimit
var totalSizeLimit int64 = 0
if totalSizeLimitStr == "" {
panic("user's total limit size is required")
} else {
intTotalSizeLimit, err := strconv.Atoi(totalSizeLimitStr)
if err != nil {
this.PanicError(err)
}
totalSizeLimit = int64(intTotalSizeLimit)
}
currentUser.TotalSizeLimit = totalSizeLimit
if operator.Role == USER_ROLE_ADMINISTRATOR {
//only admin can edit user's role and sizeLimit
if role == USER_ROLE_USER || role == USER_ROLE_ADMINISTRATOR {
currentUser.Role = role
}
} else if user.Uuid != uuid {
} else if operator.Uuid == uuid {
//cannot edit sizeLimit, totalSizeLimit
space := this.spaceDao.CheckByUuid(currentUser.SpaceUuid)
if space.SizeLimit != sizeLimit {
this.logger.Error(" %s try to modify sizeLimit from %d to %d.", operator.Uuid, space.SizeLimit, sizeLimit)
panic(result.BadRequestI18n(request, i18n.PermissionDenied))
}
if space.TotalSizeLimit != totalSizeLimit {
this.logger.Error(" %s try to modify TotalSizeLimit from %d to %d.", operator.Uuid, space.TotalSizeLimit, totalSizeLimit)
panic(result.BadRequestI18n(request, i18n.PermissionDenied))
}
} else {
panic(result.UNAUTHORIZED)
}
//edit user's info
currentUser = this.userDao.Save(currentUser)
//edit user's private space info.
space := this.spaceService.Edit(request, operator, currentUser.SpaceUuid, sizeLimit, totalSizeLimit)
//remove cache user.
this.userService.RemoveCacheUserByUuid(currentUser.Uuid)
currentUser.Space = space
return this.Success(currentUser)
}
@@ -307,6 +289,10 @@ func (this *UserController) Detail(writer http.ResponseWriter, request *http.Req
user := this.userDao.CheckByUuid(uuid)
//append the space info.
space := this.spaceDao.FindByUuid(user.SpaceUuid)
user.Space = space
return this.Success(user)
}
@@ -391,9 +377,32 @@ func (this *UserController) Page(writer http.ResponseWriter, request *http.Reque
pager := this.userDao.Page(page, pageSize, username, status, sortArray)
//append the space info. FIXME: user better way to get Space.
for _, u := range pager.Data.([]*User) {
space := this.spaceDao.FindByUuid(u.SpaceUuid)
u.Space = space
}
return this.Success(pager)
}
func (this *UserController) Search(writer http.ResponseWriter, request *http.Request) *result.WebResult {
keyword := request.FormValue("keyword")
pager := this.userDao.Page(0, 10, keyword, "", nil)
var resultList []*User = make([]*User, 0)
for _, u := range pager.Data.([]*User) {
resultList = append(resultList, &User{
Uuid: u.Uuid,
Username: u.Username,
})
}
return this.Success(resultList)
}
func (this *UserController) ToggleStatus(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := request.FormValue("uuid")
@@ -439,13 +448,14 @@ func (this *UserController) Transfiguration(writer http.ResponseWriter, request
return this.Success(session.Uuid)
}
//scan user's physics files. create index into EyeblueTank
// scan user's physics files. create index into EyeblueTank
func (this *UserController) Scan(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := request.FormValue("uuid")
currentUser := this.userDao.CheckByUuid(uuid)
this.matterService.DeleteByPhysics(request, currentUser)
this.matterService.ScanPhysics(request, currentUser)
space := this.spaceDao.CheckByUuid(currentUser.SpaceUuid)
this.matterService.DeleteByPhysics(request, currentUser, space)
this.matterService.ScanPhysics(request, currentUser, space)
return this.Success("OK")
}
@@ -494,7 +504,7 @@ func (this *UserController) ChangePassword(writer http.ResponseWriter, request *
return this.Success(user)
}
//admin reset password.
// admin reset password.
func (this *UserController) ResetPassword(writer http.ResponseWriter, request *http.Request) *result.WebResult {
userUuid := request.FormValue("userUuid")
+29 -7
View File
@@ -11,10 +11,17 @@ import (
type UserDao struct {
BaseDao
spaceDao *SpaceDao
}
func (this *UserDao) Init() {
this.BaseDao.Init()
b := core.CONTEXT.GetBean(this.spaceDao)
if b, ok := b.(*SpaceDao); ok {
this.spaceDao = b
}
}
func (this *UserDao) Create(user *User) *User {
@@ -36,7 +43,7 @@ func (this *UserDao) Create(user *User) *User {
return user
}
//find by uuid. if not found return nil.
// find by uuid. if not found return nil.
func (this *UserDao) FindByUuid(uuid string) *User {
var entity = &User{}
db := core.CONTEXT.GetDB().Where("uuid = ?", uuid).First(entity)
@@ -50,7 +57,7 @@ func (this *UserDao) FindByUuid(uuid string) *User {
return entity
}
//find by uuid. if not found panic NotFound error
// find by uuid. if not found panic NotFound error
func (this *UserDao) CheckByUuid(uuid string) *User {
entity := this.FindByUuid(uuid)
if entity == nil {
@@ -73,6 +80,20 @@ func (this *UserDao) FindByUsername(username string) *User {
return user
}
func (this *UserDao) FindAnAdmin() *User {
var user = &User{}
db := core.CONTEXT.GetDB().Where(&User{Role: USER_ROLE_ADMINISTRATOR}).First(user)
if db.Error != nil {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
return nil
} else {
panic(db.Error)
}
}
return user
}
func (this *UserDao) Page(page int, pageSize int, username string, status string, sortArray []builder.OrderPair) *Pager {
count, users := this.PlainPage(page, pageSize, username, status, sortArray)
@@ -111,8 +132,8 @@ func (this *UserDao) PlainPage(page int, pageSize int, username string, status s
return int(count), users
}
//handle user page by page.
func (this *UserDao) PageHandle(username string, status string, fun func(user *User)) {
// handle user page by page.
func (this *UserDao) PageHandle(username string, status string, fun func(user *User, space *Space)) {
//delete share and bridges.
pageSize := 1000
@@ -129,7 +150,8 @@ func (this *UserDao) PageHandle(username string, status string, fun func(user *U
for page = 0; page < totalPages; page++ {
_, users := this.PlainPage(0, pageSize, username, status, sortArray)
for _, u := range users {
fun(u)
space := this.spaceDao.CheckByUuid(u.SpaceUuid)
fun(u, space)
}
}
}
@@ -154,7 +176,7 @@ func (this *UserDao) Save(user *User) *User {
return user
}
//find all 2.0 users.
// find all 2.0 users.
func (this *UserDao) FindUsers20() []*User {
var users []*User
var wp = &builder.WherePair{}
@@ -179,7 +201,7 @@ func (this *UserDao) Delete(user *User) {
this.PanicError(db.Error)
}
//System cleanup.
// System cleanup.
func (this *UserDao) Cleanup() {
this.logger.Info("[UserDao] clean up. Delete all User")
db := core.CONTEXT.GetDB().Where("uuid is not null and role != ?", USER_ROLE_ADMINISTRATOR).Delete(User{})
+14 -14
View File
@@ -27,18 +27,18 @@ const (
)
type User struct {
Uuid string `json:"uuid" gorm:"type:char(36);primary_key;unique"`
Sort int64 `json:"sort" gorm:"type:bigint(20) not null"`
UpdateTime time.Time `json:"updateTime" gorm:"type:timestamp not null;default:CURRENT_TIMESTAMP"`
CreateTime time.Time `json:"createTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
Role string `json:"role" gorm:"type:varchar(45)"`
Username string `json:"username" gorm:"type:varchar(45) not null;unique"`
Password string `json:"-" gorm:"type:varchar(255)"`
AvatarUrl string `json:"avatarUrl" gorm:"type:varchar(255)"`
LastIp string `json:"lastIp" gorm:"type:varchar(128)"`
LastTime time.Time `json:"lastTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
SizeLimit int64 `json:"sizeLimit" gorm:"type:bigint(20) not null;default:-1"`
TotalSizeLimit int64 `json:"totalSizeLimit" gorm:"type:bigint(20) not null;default:-1"`
TotalSize int64 `json:"totalSize" gorm:"type:bigint(20) not null;default:0"`
Status string `json:"status" gorm:"type:varchar(45)"`
Uuid string `json:"uuid" gorm:"type:char(36);primary_key;unique"`
Sort int64 `json:"sort" gorm:"type:bigint(20) not null"`
UpdateTime time.Time `json:"updateTime" gorm:"type:timestamp not null;default:CURRENT_TIMESTAMP"`
CreateTime time.Time `json:"createTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
Role string `json:"role" gorm:"type:varchar(45)"`
Username string `json:"username" gorm:"type:varchar(45) not null;unique"`
Password string `json:"-" gorm:"type:varchar(255)"`
AvatarUrl string `json:"avatarUrl" gorm:"type:varchar(255)"`
LastIp string `json:"lastIp" gorm:"type:varchar(128)"`
LastTime time.Time `json:"lastTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
SpaceUuid string `json:"spaceUuid" gorm:"type:char(36);unique"`
Status string `json:"status" gorm:"type:varchar(45)"`
Space *Space `json:"space" gorm:"-"`
}
+84 -27
View File
@@ -1,6 +1,9 @@
package rest
import (
"sync"
"time"
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/cache"
"github.com/eyebluecn/tank/code/tool/result"
@@ -8,21 +11,26 @@ import (
"github.com/eyebluecn/tank/code/tool/uuid"
"net/http"
"os"
"time"
)
//@Service
// @Service
type UserService struct {
BaseBean
userDao *UserDao
sessionDao *SessionDao
//file lock
spaceService *SpaceService
//file lock (legacy, kept for compatibility)
locker *cache.Table
// matterLocker is used for atomic matter operations with retry support
matterLocker sync.Map
matterDao *MatterDao
matterService *MatterService
imageCacheDao *ImageCacheDao
spaceDao *SpaceDao
spaceMemberDao *SpaceMemberDao
shareDao *ShareDao
shareService *ShareService
downloadTokenDao *DownloadTokenDao
@@ -43,6 +51,11 @@ func (this *UserService) Init() {
this.sessionDao = b
}
b = core.CONTEXT.GetBean(this.spaceService)
if b, ok := b.(*SpaceService); ok {
this.spaceService = b
}
b = core.CONTEXT.GetBean(this.matterDao)
if b, ok := b.(*MatterDao); ok {
this.matterDao = b
@@ -58,6 +71,15 @@ func (this *UserService) Init() {
this.imageCacheDao = b
}
b = core.CONTEXT.GetBean(this.spaceDao)
if b, ok := b.(*SpaceDao); ok {
this.spaceDao = b
}
b = core.CONTEXT.GetBean(this.spaceMemberDao)
if b, ok := b.(*SpaceMemberDao); ok {
this.spaceMemberDao = b
}
b = core.CONTEXT.GetBean(this.shareService)
if b, ok := b.(*ShareService); ok {
this.shareService = b
@@ -82,36 +104,37 @@ func (this *UserService) Init() {
this.locker = cache.NewTable()
}
//lock a user's operation. If lock, user cannot operate file.
// lock a user's operation with retry mechanism.
// This allows concurrent operations to wait for each other instead of immediately failing.
// Uses sync.Map for thread-safe atomic operations.
func (this *UserService) MatterLock(userUuid string) {
maxRetries := 30 // Maximum retry attempts (30 * 200ms = 6 seconds max wait)
retryInterval := 200 // Retry interval in milliseconds
cacheItem, err := this.locker.Value(userUuid)
if err != nil {
this.logger.Error("error while get cache" + err.Error())
for i := 0; i < maxRetries; i++ {
// Try to acquire lock atomically
if _, loaded := this.matterLocker.LoadOrStore(userUuid, true); !loaded {
// Successfully acquired the lock
return
}
// Lock is held by another operation, wait and retry
if i < maxRetries-1 {
time.Sleep(time.Duration(retryInterval) * time.Millisecond)
}
}
if cacheItem != nil && cacheItem.Data() != nil {
panic(result.BadRequest("file is being operating, retry later"))
}
duration := 12 * time.Hour
this.locker.Add(userUuid, duration, true)
// After all retries, still cannot acquire lock
panic(result.BadRequest("file is being operating, retry later"))
}
//unlock
// unlock
func (this *UserService) MatterUnlock(userUuid string) {
exist := this.locker.Exists(userUuid)
if exist {
_, err := this.locker.Delete(userUuid)
this.PanicError(err)
} else {
this.logger.Error("unlock error. %s has no matter lock ", userUuid)
}
this.matterLocker.Delete(userUuid)
}
//load session to SessionCache. This method will be invoked in every request.
//authorize by 1. cookie 2. username and password in request form. 3. Basic Auth
// load session to SessionCache. This method will be invoked in every request.
// authorize by 1. cookie 2. username and password in request form. 3. Basic Auth
func (this *UserService) PreHandle(writer http.ResponseWriter, request *http.Request) {
sessionId := util.GetSessionUuidFromRequest(request, core.COOKIE_AUTH_KEY)
@@ -182,7 +205,7 @@ func (this *UserService) PreHandle(writer http.ResponseWriter, request *http.Req
}
//find a cache user by its userUuid
// find a cache user by its userUuid
func (this *UserService) FindCacheUsersByUuid(userUuid string) []*User {
var users []*User
@@ -204,7 +227,7 @@ func (this *UserService) FindCacheUsersByUuid(userUuid string) []*User {
return users
}
//remove cache user by its userUuid
// remove cache user by its userUuid
func (this *UserService) RemoveCacheUserByUuid(userUuid string) {
var sessionId interface{}
@@ -233,7 +256,32 @@ func (this *UserService) RemoveCacheUserByUuid(userUuid string) {
}
}
//delete user
// create user
func (this *UserService) CreateUser(request *http.Request, username string, sizeLimit int64, totalSizeLimit int64, password string, role string) *User {
user := &User{
Username: username,
Password: util.GetBcrypt(password),
Role: role,
Status: USER_STATUS_OK,
}
user = this.userDao.Create(user)
//create space.
space := this.spaceService.CreateSpace(request, username, user, sizeLimit, totalSizeLimit, SPACE_TYPE_PRIVATE)
//update user's space.
user.SpaceUuid = space.Uuid
this.userDao.Save(user)
user.Space = space
return user
}
// delete user
func (this *UserService) DeleteUser(request *http.Request, currentUser *User) {
//delete from cache
@@ -268,6 +316,15 @@ func (this *UserService) DeleteUser(request *http.Request, currentUser *User) {
this.logger.Info("delete matters")
this.matterDao.DeleteByUserUuid(currentUser.Uuid)
//delete space members
space := this.spaceDao.CheckByUuid(currentUser.SpaceUuid)
this.logger.Info("delete space members")
this.spaceMemberDao.DeleteBySpaceUuid(space.Uuid)
//delete spaces
this.logger.Info("delete spaces")
this.spaceDao.DeleteByUserUuid(currentUser.Uuid)
//delete this user
this.logger.Info("delete this user.")
this.userDao.Delete(currentUser)
+22 -6
View File
@@ -69,7 +69,7 @@ func (this *TankContext) Cleanup() {
}
}
//can serve as http server.
// can serve as http server.
func (this *TankContext) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
this.Router.ServeHTTP(writer, request)
}
@@ -80,10 +80,10 @@ func (this *TankContext) OpenDb() {
dbLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // slow SQL 1s
LogLevel: logger.Silent, // log level. open when debug.
IgnoreRecordNotFoundError: true, // ignore ErrRecordNotFound
Colorful: false, // colorful print
SlowThreshold: time.Second, // slow SQL 1s
LogLevel: logger.Warn, // log level. open when debug.
IgnoreRecordNotFoundError: true, // ignore ErrRecordNotFound
Colorful: false, // colorful print
},
)
@@ -204,9 +204,25 @@ func (this *TankContext) registerBeans() {
this.registerBean(new(rest.ShareDao))
this.registerBean(new(rest.ShareService))
//space
this.registerBean(new(rest.SpaceController))
this.registerBean(new(rest.SpaceDao))
this.registerBean(new(rest.SpaceService))
//space member
this.registerBean(new(rest.SpaceMemberController))
this.registerBean(new(rest.SpaceMemberDao))
this.registerBean(new(rest.SpaceMemberService))
//uploadToken
this.registerBean(new(rest.UploadTokenDao))
//chunk upload
this.registerBean(new(rest.ChunkController))
this.registerBean(new(rest.ChunkService))
this.registerBean(new(rest.UploadSessionDao))
this.registerBean(new(rest.UploadChunkDao))
//task
this.registerBean(new(rest.TaskService))
@@ -241,7 +257,7 @@ func (this *TankContext) initBeans() {
}
}
//if application installed. invoke this method.
// if application installed. invoke this method.
func (this *TankContext) InstallOk() {
if core.CONFIG.Installed() {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
func TestHello(t *testing.T) {
split := strings.Split("good", "/")
split := strings.Split("", "")
fmt.Printf("%v", split)
var i int
+5 -2
View File
@@ -3,8 +3,6 @@ package download
import (
"errors"
"fmt"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
"io"
"mime/multipart"
"net/http"
@@ -14,6 +12,9 @@ import (
"strconv"
"strings"
"time"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
)
// HttpRange specifies the byte range to be sent to the client.
@@ -313,6 +314,8 @@ func DownloadFile(
sendSize = ra.length
code = http.StatusPartialContent
writer.Header().Set("Content-Range", ra.contentRange(size))
// 把文件放进去
io.CopyN(writer, diskFile, int64(ra.length))
case len(ranges) > 1:
sendSize = RangesMIMESize(ranges, ctype, size)
code = http.StatusPartialContent
+7
View File
@@ -27,12 +27,14 @@ var (
UsernameNotExist = &Item{English: `username "%s" not exists`, Chinese: `用户名"%s"不存在`}
UsernameIsNotAdmin = &Item{English: `username "%s" is not admin user`, Chinese: `用户名"%s"不是管理员账号`}
UsernameError = &Item{English: `username can only be letters, numbers or _`, Chinese: `用户名必填,且只能包含中文,字母,数字和'_'`}
UserRoleError = &Item{English: `user role is not correct`, Chinese: `用户角色设置错误`}
UserRegisterNotAllowd = &Item{English: `admin has banned register`, Chinese: `管理员已禁用自主注册`}
UserPasswordLengthError = &Item{English: `password at least 6 chars`, Chinese: `密码长度至少为6位`}
UserOldPasswordError = &Item{English: `old password error`, Chinese: `旧密码不正确`}
UserDisabled = &Item{English: `user has been disabled`, Chinese: `用户已经被禁用了`}
MatterDestinationMustDirectory = &Item{English: `destination must be directory'`, Chinese: `目标对象只能是文件夹。`}
MatterExist = &Item{English: `"%s" already exists, invalid operation`, Chinese: `"%s" 已经存在了,操作无效`}
MatterRecycleBinExist = &Item{English: `"%s" already exists in recycle bin, invalid operation`, Chinese: `"%s" 已经存在于回收站,请彻底删除后再操作`}
MatterDepthExceedLimit = &Item{English: `directory's depth exceed the limit %d > %d`, Chinese: `文件加层数超过限制 %d > %d `}
MatterNameLengthExceedLimit = &Item{English: `filename's length exceed the limit %d > %d`, Chinese: `文件名称长度超过限制 %d > %d `}
MatterSelectNumExceedLimit = &Item{English: `selected files' num exceed the limit %d > %d`, Chinese: `选择的文件数量超出限制了 %d > %d `}
@@ -46,6 +48,11 @@ var (
ShareCodeRequired = &Item{English: `share code required`, Chinese: `提取码必填`}
ShareCodeError = &Item{English: `share code error`, Chinese: `提取码错误`}
CronValidateError = &Item{English: `cron error. five fields needed. eg: 1 * * * *`, Chinese: `Cron表达式错误,必须为5位。例如:1 * * * *`}
SpaceNameError = &Item{English: `space's name can only be letters, numbers or _`, Chinese: `空间名称必填,且只能包含中文,字母,数字和'_'`}
SpaceNameExist = &Item{English: `space's name "%s" exists`, Chinese: `空间名称"%s"已被占用,请使用其他名字`}
SpaceExclusive = &Item{English: `user can only own ONE space`, Chinese: `一个用户只能拥有一个私有空间`}
SpaceMemberExist = &Item{English: `space member %s exists`, Chinese: `用户 %s 已经是空间的成员`}
PermissionDenied = &Item{English: `permission denied.`, Chinese: `没有操作权限`}
)
func (this *Item) Message(request *http.Request) string {
+4 -4
View File
@@ -6,7 +6,7 @@ import (
"strings"
)
//whether windows develop environment
// whether windows develop environment
func EnvWinDevelopment() bool {
ex, err := os.Executable()
@@ -18,7 +18,7 @@ func EnvWinDevelopment() bool {
systemUser, err := user.Current()
if systemUser != nil {
return strings.HasPrefix(ex, systemUser.HomeDir+"\\AppData\\Local\\Temp")
return strings.HasPrefix(ex, systemUser.HomeDir+"\\AppData\\Local")
}
@@ -26,7 +26,7 @@ func EnvWinDevelopment() bool {
}
//whether mac develop environment
// whether mac develop environment
func EnvMacDevelopment() bool {
ex, err := os.Executable()
@@ -38,7 +38,7 @@ func EnvMacDevelopment() bool {
}
//whether develop environment (whether run in IDE)
// whether develop environment (whether run in IDE)
func EnvDevelopment() bool {
return EnvWinDevelopment() || EnvMacDevelopment()
+154
View File
@@ -0,0 +1,154 @@
package util
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
// param is required. when missing, panic error.
func ExtractRequestString(request *http.Request, key string) string {
str := request.FormValue(key)
if str == "" {
panic(fmt.Sprintf("%s is required", key))
} else {
return str
}
}
// param is required. when missing, panic error.
func ExtractRequestStringWithErrorHint(request *http.Request, key string, errorHint string) string {
str := request.FormValue(key)
if str == "" {
panic(errorHint)
} else {
return str
}
}
// split by comma , param is required. when missing, panic error.
func ExtractRequestArray(request *http.Request, key string) []string {
str := request.FormValue(key)
if str == "" {
panic(fmt.Sprintf("%s is required", key))
} else {
arr := strings.Split(str, ",")
if len(arr) == 0 {
panic(fmt.Sprintf("%s cannot be empty", key))
}
return arr
}
}
// param is required. when missing, panic error.
func ExtractRequestInt64(request *http.Request, key string) int64 {
keyStr := request.FormValue(key)
if keyStr == "" {
panic(fmt.Sprintf("%s is required", key))
} else {
intVal, err := strconv.ParseInt(keyStr, 10, 64)
if err != nil {
panic(err)
}
return intVal
}
}
// param is required. when missing, panic error.
func ExtractRequestInt64WithErrorHint(request *http.Request, key string, errorHint string) int64 {
keyStr := request.FormValue(key)
var num int64 = 0
if keyStr == "" {
panic(errorHint)
} else {
intVal, err := strconv.Atoi(keyStr)
if err != nil {
panic(err)
}
num = int64(intVal)
return num
}
}
// param is optional. when missing, use default.
func ExtractRequestOptionalInt64(request *http.Request, key string, defaultValue int64) int64 {
str := request.FormValue(key)
if str == "" {
return defaultValue
} else {
intVal, err := strconv.ParseInt(str, 10, 64)
if err != nil {
panic(err)
}
return intVal
}
}
// param is required. when missing, panic error.
func ExtractRequestInt(request *http.Request, key string) int {
keyStr := request.FormValue(key)
if keyStr == "" {
panic(fmt.Sprintf("%s is required", key))
} else {
intVal, err := strconv.Atoi(keyStr)
if err != nil {
panic(err)
}
return intVal
}
}
// param is optional. when missing, use default.
func ExtractRequestOptionalInt(request *http.Request, key string, defaultValue int) int {
str := request.FormValue(key)
if str == "" {
return defaultValue
} else {
intVal, err := strconv.Atoi(str)
if err != nil {
panic(err)
}
return intVal
}
}
// param is required. when missing, panic error.
func ExtractRequestOptionalString(request *http.Request, key string, defaultValue string) string {
str := request.FormValue(key)
if str == "" {
return defaultValue
} else {
return str
}
}
// param is required. when missing, panic error.
func ExtractRequestBool(request *http.Request, key string) bool {
str := request.FormValue(key)
if str == "true" {
return true
} else {
return false
}
}
// param is required. when missing, panic error.
func ExtractRequestTime(request *http.Request, key string) time.Time {
str := request.FormValue(key)
return ConvertDateTimeStringToTime(str)
}
// param is optional. when missing, use default.
func ExtractRequestOptionalBool(request *http.Request, key string, defaultValue bool) bool {
str := request.FormValue(key)
if str == "" {
return defaultValue
} else {
return str == "true"
}
}
+29 -7
View File
@@ -1,14 +1,36 @@
module github.com/eyebluecn/tank
go 1.14
go 1.22.5
require (
github.com/disintegration/imaging v1.6.2
github.com/glebarez/sqlite v1.4.0
github.com/json-iterator/go v1.1.10
github.com/glebarez/sqlite v1.11.0
github.com/json-iterator/go v1.1.12
github.com/robfig/cron/v3 v3.0.1
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
golang.org/x/text v0.3.3
gorm.io/driver/mysql v1.3.2
gorm.io/gorm v1.23.2
golang.org/x/crypto v0.25.0
golang.org/x/text v0.16.0
gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.25.11
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
modernc.org/libc v1.55.4 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.31.1 // indirect
)
+84 -185
View File
@@ -1,202 +1,101 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/glebarez/go-sqlite v1.14.8 h1:30RsIS/olgfOMr7SxiCaYhpq50BTteA/CUKaWVOOHYg=
github.com/glebarez/go-sqlite v1.14.8/go.mod h1:gf9QVsKCYMcu+7nd+ZbDqvXnEXEb22qLcqRUQ9XEI34=
github.com/glebarez/sqlite v1.4.0 h1:TvSCuOjSxIwY/bGyo2Yk5NvTy5nwUbirYM/eaq+yUfA=
github.com/glebarez/sqlite v1.4.0/go.mod h1:xIxEsgI8j1uWS9RghOpxGje8MvygoFVBAByhlh/Nu64=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.3.2 h1:QJryWiqQ91EvZ0jZL48NOpdlPdMjdip1hQ8bTgo4H7I=
gorm.io/driver/mysql v1.3.2/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.2 h1:xmq9QRMWL8HTJyhAUBXy8FqIIQCYESeKfJL4DoGKiWQ=
gorm.io/gorm v1.23.2/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.20/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.22/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU=
modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko=
modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA=
modernc.org/ccgo/v3 v3.13.1/go.mod h1:aBYVOUfIlcSnrsRVU8VRS35y2DIfpgkmVkYZ0tpIXi4=
modernc.org/ccgo/v3 v3.15.1/go.mod h1:md59wBwDT2LznX/OTCPoVS6KIsdRgY8xqQwBV+hkTH0=
modernc.org/ccgo/v3 v3.15.9/go.mod h1:md59wBwDT2LznX/OTCPoVS6KIsdRgY8xqQwBV+hkTH0=
modernc.org/ccgo/v3 v3.15.10/go.mod h1:wQKxoFn0ynxMuCLfFD09c8XPUCc8obfchoVR9Cn0fI8=
modernc.org/ccgo/v3 v3.15.12/go.mod h1:VFePOWoCd8uDGRJpq/zfJ29D0EVzMSyID8LCMWYbX6I=
modernc.org/ccgo/v3 v3.15.13/go.mod h1:QHtvdpeODlXjdK3tsbpyK+7U9JV4PQsrPGIbtmc0KfY=
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/ccorpus v1.11.4/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ=
modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
modernc.org/libc v1.12.0/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ=
modernc.org/libc v1.14.1/go.mod h1:npFeGWjmZTjFeWALQLrvklVmAxv4m80jnG3+xI8FdJk=
modernc.org/libc v1.14.2/go.mod h1:MX1GBLnRLNdvmK9azU9LCxZ5lMyhrbEMK8rG3X/Fe34=
modernc.org/libc v1.14.3/go.mod h1:GPIvQVOVPizzlqyRX3l756/3ppsAgg1QgPxjr5Q4agQ=
modernc.org/libc v1.14.5 h1:DAHvwGoVRDZs5iJXnX9RJrgXSsorupCWmJ2ac964Owk=
modernc.org/libc v1.14.5/go.mod h1:2PJHINagVxO4QW/5OQdRrvMYo+bm5ClpUFfyXCYl9ak=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.14.7 h1:A+6rGjtRQbt9SORXfV+hUyXOP3mDf7J5uz+EES/CNPE=
modernc.org/sqlite v1.14.7/go.mod h1:yiCvMv3HblGmzENNIaNtFhfaNIwcla4u2JQEwJPzfEc=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.11.0/go.mod h1:zsTUpbQ+NxQEjOjCUlImDLPv1sG8Ww0qp66ZvyOxCgw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.3.0/go.mod h1:+mvgLH814oDjtATDdT3rs84JnUIpkvAF5B8AVkNlE2g=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/ccgo/v4 v4.20.4 h1:3pPOlMcblnu5CBU3w1BFtepwBnLezGjPYTH8xBeYZM8=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v2 v2.4.3 h1:Ik4ZcMbC7aY4ZDPUhzXVXi7GMub9QcXLTfXn3mWpNw8=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/libc v1.55.4 h1:iWzZ96v1Iut2Sm4lEp0yzFde20M9zpcI0wY3DFcb+8g=
modernc.org/libc v1.55.4/go.mod h1:GPuVtbWvXUo590z/xfVIQcOqnugb1WovSfMJWMe0ZfA=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.30.2 h1:IPVVkhLu5mMVnS1dQgh3h0SAACRWcVk7aoLP9Us3UCk=
modernc.org/sqlite v1.30.2/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
modernc.org/sqlite v1.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs=
modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=