Compare commits

...
116 Commits
Author SHA1 Message Date
lishuang f779053040 Release v3.1.6 2022-05-01 17:55:46 +08:00
lishuang 6becac74d0 Upgrade the front to 3.1.6 2022-05-01 17:51:18 +08:00
lishuang e64a73eb67 Fix the bug of share auth. 2022-05-01 17:38:19 +08:00
lishuang cffa82226d Fix the bug of time-zone. 2022-05-01 16:50:35 +08:00
lishuang 09d6db68ff Refine the format. 2022-04-28 18:35:44 +08:00
lishuang f33145d02a Refine the format for png. 2022-04-28 18:35:14 +08:00
lishuang a74e4f1a13 Add support for jetbrains. 2022-04-28 18:27:52 +08:00
lishuang 9344a831be Fix the demo tank website. 2022-04-04 21:43:37 +08:00
lishuang 4fd7e89ff8 Ready to release v3.1.5 2022-03-21 15:30:39 +08:00
eyeblue d0a2774f65 Update README.md 2022-03-20 20:34:24 +08:00
lishuang fb563396fe Add security.md. 2022-03-20 20:16:17 +08:00
lishuang 73ad9f49ef Fix some frontend bugs. 2022-03-20 18:52:26 +08:00
lishuang 10e6bf81f9 Fix a bug in frontend. 2022-03-20 18:18:59 +08:00
lishuang 274b49d66f Fix the sqlite folder bug2. 2022-03-20 17:24:57 +08:00
lishuang ab6749056a Fix the sqlite folder bug. 2022-03-20 17:21:40 +08:00
lishuang 3be2e354c4 Fix a potential bug. 2022-03-20 17:15:16 +08:00
lishuang 0c825c65d4 Change the default goproxy. 2022-03-20 16:18:43 +08:00
lishuang c6836f3654 Release 3.1.4 2022-03-20 16:16:15 +08:00
lishuang c6a5db7740 Finish the sqlite feature. 2022-03-18 22:17:32 +08:00
lishuang 179f34fad2 Refine some comments. 2022-03-15 02:21:41 +08:00
lishuang 161096fbb2 Finish upgrade the gorm framework. 2022-03-15 02:17:26 +08:00
lishuang 69412300b6 Upgrade half or gorm. 2022-03-15 01:19:44 +08:00
lishuang 329751350d release v 3.1.3 2022-03-10 09:54:57 +08:00
lishuang 90675e3ae8 Fix the total size bug. 2022-03-10 09:46:23 +08:00
zicla 238125eb8d Refine the dingding code. 2022-03-09 21:20:27 +08:00
eyeblue d30f02b1c0 Merge pull request #149 from biuaxia/master
Feat 预览功能适配kkFileView,请将预览引擎的 {url} 修改为 {b64Url}.
2021-11-26 17:39:48 +08:00
biuaxia d9ec47f8ea Feat 预览功能适配kkFileView,请将预览引擎的 {url} 修改为 {b64Url}.
tips: 完整的引擎格式如 -> `http://ddns.biuaxia.cn:10002/onlinePreview?url={b64Url}`
2021-11-19 09:56:44 +08:00
lishuang 6020915993 Release 3.1.2 2021-01-02 23:17:51 +08:00
lishuang 07ed4f7922 Support chinese username. 2021-01-02 18:13:20 +08:00
lishuang 13b6d7f98d Fix the hard delete feature. 2021-01-02 17:31:09 +08:00
lishuang 3752d42de8 Fix the 0 day bin feature. 2021-01-02 17:18:20 +08:00
lishuang 847bcd746c Add the keep 0 days feature. 2021-01-02 17:10:49 +08:00
lishuang e1e46457e7 Add the scan once api. 2021-01-02 16:15:50 +08:00
lishuang a2751deac6 Merge remote-tracking branch 'origin/master' 2020-09-08 00:41:21 +08:00
lishuang 74c11e7539 Release 3.1.1 2020-09-08 00:41:00 +08:00
Simba cb0ee30c6b release 3.1.1 2020-09-08 00:25:16 +08:00
eyeblue c518c0c1e6 Merge pull request #110 from user1121114685/patch-1
fix debian user can not start tank bug
2020-09-07 10:34:51 +08:00
联盟少侠 8827c45df1 fix debian user can not start tank bug 2020-09-06 23:40:58 +08:00
Simba b91518136b upgrade version to 3.1.0 2020-07-24 08:57:08 +08:00
lishuang b46cb2501b Release 3.1.0 2020-07-18 16:26:08 +08:00
lishuang c1823e7a83 Fix the docker command. 2020-07-18 15:54:14 +08:00
lishuang 84d5f4e5e2 Add the schema-3.1.0.sql 2020-07-18 15:31:26 +08:00
lishuang ff7416f858 Fix the index syntax. 2020-07-18 15:28:31 +08:00
lishuang b6b4cb1ce4 Release 3.1.0 2020-07-18 15:19:49 +08:00
lishuang 617d5df294 Fix the hard delete bug. 2020-07-18 14:38:47 +08:00
lishuang ff87ea8e39 Add the sort for delete time. 2020-07-18 14:02:36 +08:00
lishuang 36581ded16 Fix the soft delete bug. 2020-07-18 13:59:26 +08:00
lishuang 333682325c Add a todo case. 2020-07-16 02:01:18 +08:00
lishuang 1a1d9e8adf Add pre cast. 2020-07-12 23:41:29 +08:00
lishuang 020e7b0944 Next week release 3.1.0 2020-07-12 23:40:36 +08:00
lishuang e15bd00524 Refine the README.md file. 2020-07-12 23:08:06 +08:00
lishuang de1e2dbcbb Update the frontend resources. 2020-07-12 22:11:48 +08:00
lishuang 7b24f1dedf Add the recycle bin note. 2020-07-12 21:19:45 +08:00
lishuang ce94a35ed7 Add the recovery files. 2020-07-12 21:17:58 +08:00
lishuang 8a0c66ceff Add the recovery feature. 2020-07-12 21:03:01 +08:00
lishuang a92e172d26 Fix the soft delete bug. 2020-07-12 20:52:20 +08:00
lishuang a7bcc4d4fb Finish the clean expired deleted files feature. 2020-07-12 20:42:01 +08:00
lishuang 19cfd3daf4 Add the field deletedKeepDays. 2020-07-12 17:55:40 +08:00
lishuang b82bac40fc Add the field deletedKeepDays. 2020-07-12 17:53:33 +08:00
lishuang 21d61d9cd2 Fix the delete . 2020-07-12 17:47:07 +08:00
lishuang 76e7359c69 Add the deleted time. 2020-07-12 17:46:17 +08:00
lishuang 82047f4504 Add the deleted feature. 2020-07-12 17:26:15 +08:00
lishuang c970acbe8b Refine the frontend codes. 2020-07-12 16:43:42 +08:00
lishuang 988d2caced Ready to release 3.1.0 2020-07-12 00:14:22 +08:00
lishuang c5111929a3 Update html resources. 2020-07-12 00:11:30 +08:00
lishuang b6b72e0273 Remove lots of useless files. 2020-07-12 00:05:31 +08:00
lishuang 0d43439200 Add the visit time feature. 2020-07-11 22:29:56 +08:00
lishuang da0e68f95c Finish the details for the scan task. 2020-07-11 21:57:37 +08:00
lishuang fbea823ba1 Fix some bug for scan task. 2020-07-11 21:36:24 +08:00
lishuang 7adf56ce0a Fix some bug in the dashboard etl work. 2020-07-11 21:06:12 +08:00
lishuang 7f394b327b Add the task service for scan task. 2020-07-11 20:46:47 +08:00
lishuang 7fe673068b Finish the delete and sync by physics. 2020-07-11 19:56:13 +08:00
lishuang f16d48b432 Finish the PageHandle for matter. 2020-07-11 17:42:09 +08:00
lishuang 192df21da4 Use the new cron lib. 2020-07-11 16:30:28 +08:00
lishuang 02e0f10016 Migrate guuid. 2020-07-11 15:36:21 +08:00
lishuang 24e8dc4e9f Ready to upgrade the dependencies. 2020-07-11 15:28:12 +08:00
lishuang dbeef1cee4 Add the task service. Ready to tidy the go modules. 2020-07-11 15:25:31 +08:00
lishuang 096eb7c73e Add two preference edit api. 2020-07-11 14:40:03 +08:00
lishuang 69820bfcb9 Fix the share bug. 2020-06-21 21:45:55 +08:00
lishuang 3c3b7c4389 Use new front end assets. 2020-06-21 21:28:20 +08:00
lishuang 822ebf4888 Try to use go build. 2020-06-21 20:41:28 +08:00
lishuang 3b6c43ee23 Add the tank31 schema. 2020-06-21 20:08:06 +08:00
lishuang c711423ef9 Add tank30 schema. 2020-06-21 18:22:00 +08:00
lishuang 948c37c039 Add the tank30 schema. 2020-06-21 18:18:06 +08:00
lishuang 1f8e3cfb7a Finish the version of tank-3.1.0.beta1 2020-06-21 18:12:23 +08:00
lishuang 7b875c81d1 Add the preview config. 2020-06-21 02:41:13 +08:00
lishuang 3fb1d874d5 Finish the delete feature. 2020-06-21 00:04:11 +08:00
lishuang 601c60fca7 Finish the cache refresh things. 2020-06-20 22:17:01 +08:00
lishuang d052f75a1c Move to inner. 2020-06-19 23:17:56 +08:00
lishuang 51c81e19af Fix the move to root dir feature. 2020-06-19 23:15:18 +08:00
lishuang 480cbb73ea Add Info. 2020-06-06 02:49:25 +08:00
lishuang 4a1e4c1d68 Finish the preference. 2020-06-05 21:47:02 +08:00
lishuang 76c763b84d Finish the Lock and Unlock feature for webdav. 2020-05-06 01:10:17 +08:00
lishuang ed0aa017db Refine some capital letters. Finish the Lock method. 2020-05-05 22:29:16 +08:00
lishuang a314d4dc54 Add mysqlCharset prop. Fix some dav bug. 2020-05-05 19:33:22 +08:00
zicla 4bfae38176 Finish the prop set feature. 2020-03-26 02:19:43 +08:00
zicla 1f047e25c7 Refine some webdav methods. 2020-03-26 01:11:16 +08:00
zicla ff81448611 Ready to use x's webdav components. 2020-03-25 01:55:03 +08:00
zicla 83cf140fa1 Add the webdav from x 2020-03-25 01:48:33 +08:00
zicla 8fc1cd0430 Read to finish the webdav's props. 2020-03-16 01:50:09 +08:00
zicla 7ff7907e11 Add new fields into matter. Remove migrate20To30. 2020-03-16 01:44:27 +08:00
zicla 4611defe36 Pass all the move tests. 2020-03-12 01:57:08 +08:00
zicla eb89807cf1 Fix the error of copy. 2020-03-12 01:29:22 +08:00
zicla bea9a0a56d Fix some copy warning. 2020-03-12 00:56:54 +08:00
zicla fc2df7e501 Pass the litmus basic tests. 2020-03-11 02:11:26 +08:00
zicla bab3e80aec Upgrade the dependencies. 2020-03-11 00:33:45 +08:00
zicla a556813b01 Show by type. 2020-02-24 13:28:40 +08:00
zicla e90f922b58 Add a todo item. 2020-02-12 03:22:16 +08:00
zicla 7671564ea3 ADD some todo things. 2020-01-08 16:06:51 +08:00
zicla 402dc5d970 Add a todo list. 2020-01-03 14:36:00 +08:00
zicla 51b0668b5d Update the info for dingding. 2019-12-24 15:00:06 +08:00
zicla 8686551e71 Release 3.0.6 2019-10-13 16:37:20 +08:00
simba 437e2c602a fix: a user register bug 2019-09-26 23:26:34 +08:00
zicla fe00d39bc1 Release docker version. 2019-08-14 02:04:39 +08:00
zicla 5cb7998f12 Fix the Bug of sharing. [#56](https://github.com/eyebluecn/tank/issues/56) 2019-08-13 02:49:38 +08:00
eyeblue 6cec43bee6 Update README.md 2019-07-24 11:11:13 +08:00
151 changed files with 18775 additions and 5165 deletions
+92 -6
View File
@@ -12,10 +12,7 @@
3. core/config.go
VERSION = x.x.x
4. tank-front 修改 vuex/index.js
versionName: 'x.x.x'
5. readme.md 修改 eyeblue/tank:x.x.x
4. readme.md 修改 eyeblue/tank:x.x.x
eyeblue/tank:x.x.x
@@ -30,8 +27,8 @@ docker container prune
# 构建新版本 (最后的.不能少)
docker build -t eyeblue/tank:x.x.x .
# 本地测试
docker run --name dockermysql -p 13306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=tank -e MYSQL_USER=tank -e MYSQL_PASSWORD=tank123 -v ~/data/dockermysql:/var/lib/mysql -d mysql:5.7
docker run --name tank -p 6010:6010 --link dockermysql:mysql -v ~/data/dockermatter:/data/matter -d eyeblue/tank:x.x.x
docker run --name dockermysql -p 13306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=tank -e MYSQL_USER=tank -e MYSQL_PASSWORD=tank123 -v ~/data/dockermysql:/var/lib/mysql -e TZ=Asia/Shanghai -d mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
docker run --name tank -p 6010:6010 --link dockermysql:mysql -v ~/data/dockermatter:/data/build/matter -d eyeblue/tank:x.x.x
# 没问题后,推送到远程
docker push eyeblue/tank:x.x.x
```
@@ -50,6 +47,95 @@ c. 在github上发布新版本。
3.更新tank-doc 下载内容
## useful commands
go get xxx/xxx
go mod tidy
tank-3.1.5 2022-03-21
1. Support Sqlite.
2. Upgrade webpack.
1. 支持Sqlite
2. 升级了前端的webpack
tank-3.1.4 2022-03-20
1. Support Sqlite.
2. Upgrade webpack.
1. 支持Sqlite
2. 升级了前端的webpack
tank-3.1.3 2022-03-10
1. Add kkfileview 3.x support
2. Fix search bug https://github.com/eyebluecn/tank/issues/148
3. Fix bug of totalSize in dashboard
1. 添加了kkfileview 3.x的预览支持
2. 修复了一个搜索的bug https://github.com/eyebluecn/tank/issues/148
3. 修复了文件大小统计的bug
tank-3.1.2 2021-01-02
1. Add scan manually once feature
2. Add disable recycle bin feature
3. WebDAV use hard delete now
4. Support Chinese Username
5. Normal user cannot change itself space limit
6. Support the order sort by filename, size and update time
7. Hide the side menu on phone by default
8. Friendly hint for large file upload.
9. Cancel during uploading.
1. 添加了手动扫描一次的功能
2. 添加了禁用回收站功能
3. WebDAV中的删除采用硬删除
4. 支持中文的用户名
5. 普通用户不能再修改自己的空间限制
6. 文件支持按照名称排序,按照大小排序,按照修改日期排序
7. 手机端默认隐藏左边菜单
8. 大文件上传友好性提示
9. 可以取消文件上传
tank-3.1.1 2020-09-08
1. Add upload folder feature.
2. Fix the share auth bug.
1. 添加了文件夹上传功能
2. 修复了分享时需要登陆的问题
tank-3.1.0
1. Reconstruct frontend by typescript+react+antd.
2. Enhance the webdav ability.
3. Add charset in mysql config.
4. Add preview_config.
5. Add delete user feature.
6. Add Scan config.
7. Add recycle bin feature.
1. 前后进行了一次完全的重构。 采用Typescript+React+Antd
2. 增强了WebDav的能力,现在能够适配更多的WebDav客户端了
3. 数据库配置项中添加了编码支持,可以支持emoji了
4. 添加了预览引擎配置,用户可以使用自己的引擎来预览特定格式文件
5. 添加了删除用户的功能,删除用户后会清空用户所有的文件和痕迹
6. 添加了扫描功能,可以定时同步物理文件到蓝眼云盘中
7. 增加回收站功能
2019-10-13
tank-3.0.6
1. Fix the Bug of limit size.
2. Add the drag feature for uploading files.
3. Fix the page bug of list.
2019-08-13
tank-3.0.5
1. Fix the Bug of sharing. [#56](https://github.com/eyebluecn/tank/issues/56)
2019-06-11
tank-3.0.4
1. Use relative assets in tank-front.
+8 -4
View File
@@ -1,5 +1,5 @@
# use golang 1.12
FROM golang:1.12
# use golang 1.17
FROM golang:1.17
# maintainer. Author's name and email.
MAINTAINER eyeblue "eyebluecn@126.com"
@@ -14,10 +14,14 @@ COPY . /data
VOLUME /data/build/matter
# proxy
ENV GOPROXY=https://athens.azurefd.net
ENV GOPROXY=https://goproxy.cn
# Handle time-zone
ENV TZ=Asia/Shanghai
RUN apt-get install -y tzdata
# prepare the config file
RUN go build -mod=readonly \
RUN go build \
&& cp -r /data/tank /data/build
# use 6010 as default.
+27 -22
View File
@@ -1,12 +1,23 @@
[![蓝眼云盘logo](https://raw.githubusercontent.com/eyebluecn/tank/master/build/doc/img/logo.png)](https://github.com/eyebluecn/tank)
[![蓝眼云盘logo](./build/doc/img/logo.png)](https://github.com/eyebluecn/tank)
[English Version](./README_EN.md)
[English Version](https://tank-doc.eyeblue.cn/en)
# 蓝眼云盘(3.0.4)
# 蓝眼云盘(3.1.6)
[在线Demo](https://tanker.eyeblue.cn) (体验账号: demo 密码:123456)
[在线Demo](https://tank.eyeblue.cn) (体验账号: demo 密码:123456)
蓝眼云盘是蓝眼开源系列代表作品之一,致力于打造精致,优雅,简约的云盘。核心功能如下:
1. 文件列表
2. 文件/文件夹上传 + 拖拽上传
3. 文件分享
4. 文件监控统计
5. 回收站
6. 多用户
7. [WebDav](https://tank-doc.eyeblue.cn/advance/webdav.html)
8. [扫描磁盘任务](https://tank-doc.eyeblue.cn/advance/scan.html)
9. [在线预览及自定义配置预览引擎](https://tank-doc.eyeblue.cn/advance/preview.html)
10. 支持Sqlite和Mysql数据库用以存储文件元信息,Sqlite可以做到开箱即用。
[文档](https://tank-doc.eyeblue.cn/)
[更多内容请移步至文档](https://tank-doc.eyeblue.cn/)
### 软件截图
@@ -18,27 +29,12 @@
![](./build/doc/img/tank2.png)
![](./build/doc/img/tank3.png)
![](./build/doc/img/tank4.png)
#### 手机端截图
![](./build/doc/img/mobile.png)
#### [安装文档](https://tank-doc.eyeblue.cn/zh/basic/install.html)
### 相关文档
[蓝眼云盘后端api](https://github.com/eyebluecn/tank/blob/master/build/doc/api_zh.md)
[蓝眼云盘编程接口](https://github.com/eyebluecn/tank/blob/master/build/doc/alien_zh.md)
[快速使用Let's Encrypt开启个人网站的https](https://blog.eyeblue.cn/home/article/9f580b3f-5679-4a9d-be6f-4d9f0dd417af)
[Docker 化你的开源项目](https://blog.eyeblue.cn/home/article/510f9316-9ca1-40fe-b1b3-5285505a527d)
## [安装文档](https://tank-doc.eyeblue.cn/basic/install.html)
### Contribution
@@ -46,10 +42,19 @@
如果您也想参与进来,请尽情的fork, star, post issue, pull requests
当然你可以加入钉钉群一起直接交流
当然你可以加入钉钉群一起直接交流,在钉钉群中可以获取最新beta版本。
![](./build/doc/img/dingding.jpg)
群号:23156361
### Support
IDE for this project is supported by [Jetbrains](https://jb.gg/OpenSourceSupport).
[![](./build/doc/img/jb_beam.png)](https://jb.gg/OpenSourceSupport)
### License
[MIT](http://opensource.org/licenses/MIT)
-134
View File
@@ -1,134 +0,0 @@
[![蓝眼云盘logo](https://raw.githubusercontent.com/eyebluecn/tank/master/build/doc/img/logo.png)](https://github.com/eyebluecn/tank)
[中文版](./README_EN.md)
# Eyeblue Cloud Disk(2.0.0)
[Online Demo](https://tank.eyeblue.cn) (demo@tank.eyeblue.cn 123456)
![](https://tank.eyeblue.cn/api/alien/download/df372827-ba56-415e-42d1-0e3a34fdb2a1/github20x20.png "github20x20.png") [tank backend](https://github.com/eyebluecn/tank)
![](https://tank.eyeblue.cn/api/alien/download/df372827-ba56-415e-42d1-0e3a34fdb2a1/github20x20.png "github20x20.png") [tank-front frontend](https://github.com/eyebluecn/tank-front)
### Description
Eyeblue Cloud Disk is one of [Eyeblue Softwares](https://github.com/eyebluecn), and it is the basic software for other eyeblue softwares.Eyeblue Cloud Disk has the following features.
**1. Easy to use**
- Docker available
- [Install package](https://github.com/eyebluecn/tank/releases) is about 10M. Cross platform.
- Unpack the install zip, and run it with one command.
- Installation Guide
- Dashboard to view UV and PV.
- Preview usual files (doc,ppt,xls,pdf,mp3,mp4,jpg,png)
**2. Core features all in readiness**
- Manage your files on cloud.
- Responsive on PC and mobile.
- Normal user and administrator
**3. Custom Api supported**
- Support [Custom Api](https://github.com/eyebluecn/tank/blob/master/build/doc/alien_zh.md)
- Manage files by api.
If this project is helpful for you, star ⭐ it.
### Screen Shot
#### PC
![](./build/doc/img/tank0.png)
![](./build/doc/img/tank1.png)
![](./build/doc/img/tank2.png)
![](./build/doc/img/tank3.png)
![](./build/doc/img/tank4.png)
#### Mobile
![](./build/doc/img/mobile.png)
### Installation by Docker
##### a) Run
```
docker run --name tank -p 6010:6010 -d eyeblue/tank:2.0.0
```
If you'd like to start you mysql by docker, use the following command.
```
docker run --name mysql4tank -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=tank -e MYSQL_USER=tank -e MYSQL_PASSWORD=tank123 -v ~/data/mysqldemo1:/var/lib/mysql -d mysql:5.7
```
##### b) Validation
Open http://127.0.0.1:6010 it succeed if you see the installation guide.
### Installation by installation package (windows/linux almost the same)
##### a) Prepare
1. [Mysql](https://www.mysql.com/downloads/) (Use utf8 charset)
2. [Here](https://github.com/eyebluecn/tank/releases) download installation package.
3. Unzip on server
##### b) Run
- Double click `tank.exe` on windows
- Run the following command on linux
```
cd ApplicationDirectory
./tank
```
If you'd like to run in daemon mode.
```shell
# Run the application
cd ApplicationDirectory/service
./startup.sh
# Stop the application
cd ApplicationDirectory/service
./shutdown.sh
```
##### c) Validation
Open http://127.0.0.1:6010 it succeed if you see the installation guide.
### Reference
[api](https://github.com/eyebluecn/tank/blob/master/build/doc/api_zh.md)
[Custom API](https://github.com/eyebluecn/tank/blob/master/build/doc/alien_zh.md)
### Contribution
Thanks these authors [@zicla](https://github.com/zicla),[@seaheart](https://github.com/seaheart),[@yemuhe](https://github.com/yemuhe),[@hxsherry](https://github.com/hxsherry)
You can join us by scanning the following qcode.
![](./build/doc/img/dingding.jpg)
### License
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2017-present, eyeblue.cn
+16
View File
@@ -0,0 +1,16 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| master | :white_check_mark: |
## Reporting a Vulnerability
We strongly encourage you to report security vulnerabilities to
our private security mailing list: security@cilium.io - first, before
disclosing them in any public forums.
This is a private mailing list where only members of the Cilium internal
security team are subscribed to, and is treated as top priority.
-13
View File
@@ -1,13 +0,0 @@
# Eyeblue Tank
> The document is comming...
go build -mod=readonly
### Program Arguments
-mode=mirror -username=admin -password=123456 -src=/Users/fusu/d/temp -dest=/morning
-3
View File
@@ -1,3 +0,0 @@
# 蓝眼云盘
> 详细文档正在撰写中,敬请期待...
-132
View File
@@ -1,132 +0,0 @@
# 蓝眼云盘编程接口
- 蓝眼云盘提供了[编程接口](https://github.com/eyebluecn/tank/blob/master/build/doc/alien_zh.md),实现了云存储(如:[七牛云](https://www.qiniu.com),[阿里云OSS](https://www.aliyun.com/product/oss))的核心功能,可以使用编程接口上传文件,作为其他网站、系统、app的资源存储器。可以在下载图片时对图片做缩放裁剪处理,可以有效地节省客户端流量。
- 蓝眼系列开源软件之二的[《蓝眼博客》](https://github.com/eyebluecn/blog)正是使用蓝眼博客作为第三方资源存储器。蓝眼博客中的所有图片,附件均是存储在蓝眼云盘中。
所有的编程接口均定义在`alien_controller`中,主要包括以下几个接口:
`/api/alien/fetch/upload/token` 获取上传token
`/api/alien/fetch/download/token` 获取下载token
`/api/alien/confirm` 来蓝眼云盘确认文件
`/api/alien/upload` 使用form表单上传文件
`/api/alien/download/{uuid}/{filename}` 下载文件
### 上传时序图
![上传时序图](https://raw.githubusercontent.com/eyebluecn/tank/master/build/doc/img/upload-time-line.png)
### 下载时序图
![下载时序图](https://raw.githubusercontent.com/eyebluecn/tank/master/build/doc/img/download-time-line.png)
### 接口详情
#### /api/alien/fetch/upload/token
功能:一个蓝眼云盘受信任的用户请求一个`UploadToken`,用于给另一个用户向蓝眼云盘上传文件。
一般的使用场景是`应用服务器`向`蓝眼云盘`请求`UploadToken`,然后将此`UploadToken`交由`浏览器`去向`蓝眼云盘`上传文件。
参数 | 类型 | 描述
--------- | ---- | -----------
email | `string` | 【必填】邮箱,用于确定请求者身份
password | `string` | 【必填】密码,用于确定请求者身份
filename | `string` | 【必填】即将上传的文件名,不能包含以下特殊符号:`< > \| * ? / \`
expire | `int` | 【选填】UploadToken过期时间,单位:s。默认 86400s 即24h
privacy | `bool` | 【必填】文件的共有性。`true`表示文件私有,下载时必须要DownloadToken. `false`表示文件公有,任何人可以通过下载链接直接下载
size | `int` | 【必填】文件的大小。单位:byte
dir | `string` | 【必填】文件存放的路径。不能为空,必须以`/`开头,不能出现连续的`//`,不能包含以下特殊符号:`< > \| * ? \`。举例:`/app/blog/20180101121212001`
#### /api/alien/upload
功能:浏览器拿着`UploadToken`通过FormData向蓝眼云盘上传文件。
一般的使用场景是`应用服务器`向`蓝眼云盘`请求`UploadToken`,然后将此`UploadToken`交由`浏览器`去向`蓝眼云盘`上传文件。由于在请求`UploadToken`的时候已经传入了文件元信息,因此这里的文件信息必须要和`/api/alien/fetch/upload/token`传入的参数信息一致。
参数 | 类型 | 描述
--------- | ---- | -----------
uploadTokenUuid | `string` | 【必填】uploadToken标识,`/api/alien/fetch/upload/token`请求返回对象中的`uuid`
file | `file` | 【必填】文件,在浏览器中是通过`<input type="file" name="file"/>`来选择的
#### /api/alien/confirm
功能:`应用服务器`向蓝眼云盘确认某个文件是否确实已经上传好了。
参数 | 类型 | 描述
--------- | ---- | -----------
email | `string` | 【必填】邮箱,用于确定请求者身份
password | `string` | 【必填】密码,用于确定请求者身份
matterUuid | `string` | 【必填】浏览器上传完毕后,蓝眼云盘返回给浏览器的`uuid`
#### /api/alien/fetch/download/token
功能:一个蓝眼云盘受信任的用户请求一个`DownloadToken`,用于给另一个用户下载蓝眼云盘上的私有文件。
一般的使用场景是`应用服务器`向`蓝眼云盘`请求`DownloadToken`,然后将此`DownloadToken`交由`浏览器`去向`蓝眼云盘`下载文件。
参数 | 类型 | 描述
--------- | ---- | -----------
email | `string` | 【必填】邮箱,用于确定请求者身份
password | `string` | 【必填】密码,用于确定请求者身份
matterUuid | `string` | 【必填】文件uuid,要想下载的文件`uuid`
expire | `int` | 【选填】UploadToken过期时间,单位:s。默认 86400s 即24h
#### /api/alien/download/{uuid}/{filename}
功能:在浏览器中下载文件
这个接口既可以下载公有文件,又可以下载私有文件。同时对于图片文件还可以做裁剪缩放等操作。
参数 | 类型 | 描述
--------- | ---- | -----------
uuid | `string` | 【必填】文件的uuid,该参数放在url的路径中
filename | `string` | 【必填】文件的名称,该参数放在url的路径中
downloadTokenUuid | `string` | 【选填】download的uuid,如果是私有文件该参数必须,公有文件无需填写。
该接口同时还可以对图片进行缩放预处理
> 图片缩放支持的格式有:".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp", ".gif"
##### 额外参数
| 参数 | 类型 | 描述 | 取值范围 |
| ------------ | ---- | ------------ | ------------ |
| imageProcess | `string` | 指定图片处理的方式,对于图片缩放固定为`resize` | 固定为`resize` |
| imageResizeM | `string` | 指定图片缩放的策略,有三种策略,`fit` 表示固定一边,另一边按比例缩放;`fill`表示先将图片延伸出指定W与H的矩形框外,然后进行居中裁剪;`fixed`表示直接按照指定的W和H缩放图片,这种方式可能导致图片变形 | [`fit`,`fill`,`fixed`] 不填默认`fit` |
| imageResizeW | `int` | 指定的宽度,对于`fit`可以不指定 | 1 ~ 4096 |
| imageResizeH | `int` | 指定的高度,对于`fit`可以不指定 | 1 ~ 4096 |
##### 示例
原图:
![将宽度指定为200,高度等比例缩放](http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg)
1. 将宽度指定为200,高度等比例缩放
![将宽度指定为200,高度等比例缩放](http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fit&imageResizeW=200)
[http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fit&imageResizeW=200](http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fit&imageResizeW=200)
2. 将高度指定为200,宽度等比例缩放
![将高度指定为200,宽度等比例缩放](http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fit&imageResizeH=200)
[http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fit&imageResizeH=200](http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fit&imageResizeH=200)
3. 图片自动填充在200*200的大小中 (这种情况用得最多)
![图片自动填充在200*200的大小中](http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fill&imageResizeW=200&imageResizeH=200)
[http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fill&imageResizeW=200&imageResizeH=200](http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fill&imageResizeW=200&imageResizeH=200)
4. 图片固定大小200*200 (一般会导致变形)
![图片自动填充在200*200的大小中](http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fixed&imageResizeW=200&imageResizeH=200)
[http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fixed&imageResizeW=200&imageResizeH=200](http://tank.eyeblue.cn/api/alien/download/3f4b3090-e688-4d63-7705-93a120690505/horse.jpg?imageProcess=resize&imageResizeM=fixed&imageResizeW=200&imageResizeH=200)
-798
View File
@@ -1,798 +0,0 @@
# 蓝眼云盘api接口
## 一、实体
在详细介绍各controller中的接口前,有必要先介绍蓝眼云盘中的各实体,所有的实体基类均为`Base`
#### Base
`Base`定义如下,所有会在数据库中持久化的实体均会继承`Base`,`Controller`在返回实体给前端时,会将字段和值序列化成json字符串,其中键就和每个实体字段后面的`json`标签一致,下文也会有详细例子介绍
```
type Base struct {
//唯一标识
Uuid string `gorm:"primary_key" json:"uuid"`
//排序用的字段,一般是时间戳来表示序号先后
Sort int64 `json:"sort"`
//修改时间
ModifyTime time.Time `json:"modifyTime"`
//创建时间
CreateTime time.Time `json:"createTime"`
}
```
#### Preference
`Preference`是整个网站的偏好设置,网站的名称,logo,favicon,版权,备案号等信息均由这个实体负责。定义如下:
```
type Preference struct {
//继承Base,也就是说Base中的Uuid,Sort,ModifyTime,CreateTime这里也会有
Base
//网站名称
Name string `json:"name"`
//网站的logo url
LogoUrl string `json:"logoUrl"`
//网站的favicon.ico url
FaviconUrl string `json:"faviconUrl"`
//网站下方第一行信息,一般是版权信息
FooterLine1 string `json:"footerLine1"`
//网站下方的第二行信息,一般是本案信息
FooterLine2 string `json:"footerLine2"`
//当前运行的蓝眼博客版本,这个字段不可修改,每次发版时硬编码
Version string `json:"version"`
}
```
#### Matter
`Matter`是代表文件(文件夹是一种特殊的文件),为了和系统的`file`重复,这里使用`matter`,这个实体是蓝眼云盘最重要的实体:
```
type Matter struct {
//继承Base,也就是说Base中的Uuid,Sort,ModifyTime,CreateTime这里也会有
Base
//所在的文件夹的uuid,如果在根目录下,这个字段为 root
Puuid string `json:"puuid"`
//创建这个文件的用户uuid
UserUuid string `json:"userUuid"`
//该文件是否是文件夹
Dir bool `json:"dir"`
//该文件是否是应用文件,比如用户上传的头像这个字段就为true. 一般上传的文件就为false
Alien bool `json:"alien"`
//文件名,带后缀名。例如:avatar.jpg
Name string `json:"name"`
//文件的md5值,目前该功能尚未实现,作为保留字段
Md5 string `json:"md5"`
//文件大小,单位 byte。比如某个文件1M大,那么这里就为: 1048576
Size int64 `json:"size"`
//文件是否为私有,如果true则该文件只能作者或超级管理员可以下载,如果false所有人均可以通过下载链接下载。
Privacy bool `json:"privacy"`
//文件在磁盘中的路径,前端无需关心这个字段。但是后端在寻找文件时这个字段非常关键。
Path string `json:"path"`
//该文件的父级matter,该字段不会持久化到数据集,属于获取matter详情时临时组装出来的。
Parent *Matter `gorm:"-" json:"parent"`
}
```
#### User
`User`是代表用户:
```
type User struct {
//继承Base,也就是说Base中的Uuid,Sort,ModifyTime,CreateTime这里也会有
Base
//角色,有以下枚举值:GUEST(游客,不会持久化到数据库),USER(普通用户),ADMINISTRATOR(超级管理员)
Role string `json:"role"`
//用户名,在Matter的path字段中很有用。
Username string `json:"username"`
//密码,默认不会返回给前端
Password string `json:"-"`
//邮箱,作为登录时的账户
Email string `json:"email"`
//手机
Phone string `json:"phone"`
//性别,有以下枚举值:MALE(男性),FEMALE(女性),UNKNOWN(未知)
Gender string `json:"gender"`
//城市
City string `json:"city"`
//头像Url
AvatarUrl string `json:"avatarUrl"`
//上次登录时的ip
LastIp string `json:"lastIp"`
//上次登录的时间
LastTime time.Time `json:"lastTime"`
//该用户允许上传的单文件最大大小
SizeLimit int64 `json:"sizeLimit"`
//状态,有以下枚举值:OK(正常),DISABLED(被禁用)
Status string `json:"status"`
}
```
### UploadToken
用于给陌生人上传的token
```
type UploadToken struct {
//继承Base,也就是说Base中的Uuid,Sort,ModifyTime,CreateTime这里也会有
Base
//颁发该token的用户,系统中任何用户都能颁发token
UserUuid string `json:"userUuid"`
//使用这个token上传文件就必须上传在这个文件夹下
FolderUuid string `json:"folderUuid"`
//陌生人上传好了的文件uuid
MatterUuid string `json:"matterUuid"`
//过期时间
ExpireTime time.Time `json:"expireTime"`
//使用这个token上传文件就必须是这个文件名
Filename string `json:"filename"`
//使用这个token上传文件就必须是这个公私有性
Privacy bool `json:"privacy"`
//使用这个token上传文件就必须这个大小
Size int64 `json:"size"`
//使用这个token上传文件陌生人的ip
Ip string `json:"ip"`
}
```
### DownloadToken
用于给陌生人下载的token,一个matter如果Privacy=true,那么就意味着只有自己或者超级管理员可以下载,如果让某些自己信任的用户也能下载,那么就需要生成`DownloadToken`给这些用户来下载。
```
type DownloadToken struct {
//继承Base,也就是说Base中的Uuid,Sort,ModifyTime,CreateTime这里也会有
Base
//颁发该token的用户
UserUuid string `json:"userUuid"`
//该token只能下载这个文件
MatterUuid string `json:"matterUuid"`
//有效期截止
ExpireTime time.Time `json:"expireTime"`
//下载者的ip
Ip string `json:"ip"`
}
```
### Pager
在前端请求一个列表时,通常返回的都是一个`Pager`,`Pager`中就是装的各个实体的列表。
```
type Pager struct {
//当前页数,0基
Page int `json:"page"`
//每页的大小
PageSize int `json:"pageSize"`
//总的条目数
TotalItems int `json:"totalItems"`
//总的页数
TotalPages int `json:"totalPages"`
//实体的数组
Data interface{} `json:"data"`
}
```
### WebResult
`WebResult`并不是会持久化到数据库中实体,`WebResult`是在`controller`返回数据给前端时包装的一层,有了`WebResult`后每个接口返回的数据会更加统一,方便了前端的统一处理。
```
type WebResult struct {
//状态码,具体每个码的意义参考下文
Code int `json:"code"`
//一句话描述请求结果,通常会是出错时指明出错原因,或者修改权限等小操作时提示的`操作成功`
Msg string `json:"msg"`
//内容可能是一个实体,也可能是一个 Pager.
Data interface{} `json:"data"`
}
```
状态码对应关系如下:
```
const (
//正常
RESULT_CODE_OK = 200
//未登录
RESULT_CODE_LOGIN = -400
//没有权限
RESULT_CODE_UNAUTHORIZED = -401
//请求错误
RESULT_CODE_BAD_REQUEST = -402
//没有找到
RESULT_CODE_NOT_FOUND = -404
//登录过期
RESULT_CODE_LOGIN_EXPIRED = -405
//该登录用户不是有效用户
RESULT_CODE_LOGIN_INVALID = -406
//提交的表单验证不通过
RESULT_CODE_FORM_INVALID = -410
//请求太频繁
RESULT_CODE_FREQUENCY = -420
//服务器出错。
RESULT_CODE_SERVER_ERROR = -500
//远程服务不可用
RESULT_CODE_NOT_AVAILABLE = -501
//并发异常
RESULT_CODE_CONCURRENCY = -511
//远程微服务没有找到
RESULT_CODE_SERVICE_NOT_FOUND = -600
//远程微服务连接超时
RESULT_CODE_SERVICE_TIME_OUT = -610
//通用的异常
RESULT_CODE_UTIL_EXCEPTION = -700
)
```
## 二、返回规范
蓝眼云盘采用前后端分离的模式,前端调用后端接口时,url均以`/api`开头,返回均是json字符串。
- 返回的json字符串的key均为小写开头的驼峰法,具体参考实体类中`json`标签
- 返回的时间格式均为 `YYYY-MM-dd HH:mm:ss`(例如:2018-01-06 17:57:00)
返回内容均是由`WebResult`进行包装,因此具有高度的统一性,在这里我们约定一些说法,后面介绍`Controller`时便不再赘述。
1. 返回一个`XX`实体
指的是`WebResult`的`Code=200`, `Data=一个XX实体对象`。
例:返回一个User,则前端会收到以下json字符串:
```
{
"code": 200,
"msg": "",
"data": {
"uuid": "eed2c66d-1de6-47ff-645e-b67beaa10365",
"sort": 1514803034507,
"modifyTime": "2018-01-06 18:00:58",
"createTime": "2018-01-01 18:37:15",
"role": "USER",
"username": "demo",
"email": "demo@tank.eyeblue.cn",
"phone": "",
"gender": "UNKNOWN",
"city": "",
"avatarUrl": "/api/alien/download/ea490cb6-368e-436d-71c0-fcfb08854c80/1180472.png",
"lastIp": "124.78.220.82",
"lastTime": "2018-01-06 18:00:58",
"sizeLimit": 1048576,
"status": "OK"
}
}
```
2. 返回`XX`的`Pager`
指的是`WebResult`的`Code=200`, `Data=XX的Pager`。
例:返回`User`的`Pager`,则前端会收到以下json字符串:
```
{
"code": 200,
"msg": "",
"data": {
"page": 0,
"pageSize": 10,
"totalItems": 2,
"totalPages": 1,
"data": [
{
"uuid": "6a661938-8289-4957-4096-5a1b584bf371",
"sort": 1515057859613,
"modifyTime": "2018-01-04 17:26:01",
"createTime": "2018-01-04 17:24:20",
"role": "ADMINISTRATOR",
"username": "simba",
"email": "y******5@163.com",
"phone": "",
"gender": "MALE",
"city": "",
"avatarUrl": "/api/alien/download/d1e453cb-3170-4bdb-73f2-fa0372ee017b/1180480.png",
"lastIp": "180.173.103.207",
"lastTime": "2018-01-04 17:26:01",
"sizeLimit": -1,
"status": "OK"
},
{
"uuid": "e59be6a3-f806-463e-553a-4c5892eedf78",
"sort": 1514881002975,
"modifyTime": "2018-01-02 16:16:43",
"createTime": "2018-01-02 16:16:43",
"role": "USER",
"username": "blog_dev",
"email": "b*****@tank.eyeblue.cn",
"phone": "",
"gender": "MALE",
"city": "",
"avatarUrl": "/api/alien/download/fdca6eee-d009-4eb3-5ad4-15ba3701cb2e/jump.jpg",
"lastIp": "",
"lastTime": "2018-01-02 16:16:43",
"sizeLimit": 1048576,
"status": "OK"
}
]
}
}
```
3. 返回错误信息:yyy
指的是`WebResult`的`Code=-400`, `Msg=yyy`。(这里的Code具体值参考上文的code表)
例:返回错误信息:"【新建文件夹】已经存在了,请使用其他名称。",则前端会收到以下json字符串:
```
{
"code": -700,
"msg": "【新建文件夹】已经存在了,请使用其他名称。",
"data": null
}
```
4. 返回成功信息:zzz
指的是`WebResult`的`Code=200`, `Msg=zzz`。(这里的Code具体值参考上文的code表)
例:返回成功信息:"删除成功。",则前端会收到以下json字符串:
```
{
"code": 200,
"msg": "删除成功。",
"data": null
}
```
## 三、接口
蓝眼云盘所有的接口均定义在`controller`中,总共定义了以下`controller`:
名称 | 所在文件 | 描述
--------- | ---- | -----------
PreferenceController | `preference_controller.go` | 网站标题,logo,版权说明等信息的增删改查
MatterController | `matter_controller.go` | 站内创建文件夹,上传文件,删除文件,修改权限等
UserController | `user_controller.go` | 登录,管理操作站内用户
AlienController | `alien_controller.go` | 第三方授权上传,下载,预处理
每个接口都有不同的访问级别,系统中定义了三种访问级别,分别是:
`游客` < `注册用户` < `管理员`
### PreferenceController
该Controller负责网站中的偏好设置,主要操作`Preference`实体
----------
#### /api/preference/fetch
**功能**:读取网站偏好设置,网站名称,logo,版权,备案信息均从此接口读取。
**访问级别**:`游客`,`注册用户`,`管理员`
**请求参数**:无
**返回**: 一个`Preference`实体
----------
#### /api/preference/edit
**功能**:编辑网站偏好设置,修改网站名称,logo,版权,备案信息。
**访问级别**:`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
name | `string` | 必填 | 网站名称
logoUrl | `string` | 选填 | 网站logoUrl,如果不填默认使用蓝眼云盘logo
faviconUrl | `string` | 选填 | 网站faviconUrl,如果不填默认使用蓝眼云盘favicon.ico
footerLine1 | `string` | 选填 | 网站版权所有信息
footerLine2 | `string` | 选填 | 网站备案信息
**返回**: 一个`Preference`实体
----------
### MatterController
该Controller负责站内创建文件夹,上传文件,删除文件,修改权限等,主要操作`Matter`实体
----------
#### /api/matter/create/directory
**功能**:创建文件夹
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
puuid | `string` | 必填 | 准备创建的目录所在的目录,如果在根目录下创建传`root`
name | `string` | 必填 | 文件夹名称, 不能包含以下特殊符号:`< > \| * ? / \`
**返回**: 新建的这个文件夹的`Matter`实体
----------
#### /api/matter/upload
**功能**:上传文件
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
userUuid | `string` | 选填 | 该文件的归属人,如果不填则使用当前登录的用户uuid;如果是管理员要给用户A上传文件,那么这里应该填A的uuid.
alien | `bool` | 选填 | 是否是应用数据,true表示应用数据,这时会自动放置在`应用数据`这个文件夹下,比如上传头像。false表示非应用数据,这时`puuid`字段为必填
puuid | `bool` | 选填 | 文件上传到哪个目录下。 如果`alien`=false,这个字段必填。
privacy | `bool` | 选填 | 文件的私有性,默认`false`
file | `file` | 必填 | 文件,在浏览器中是通过`<input type="file" name="file"/>`来选择的
**返回**: 刚上传的这个文件的`Matter`实体
----------
#### /api/matter/delete
**功能**:删除文件或者文件夹
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
uuid | `string` | 必填 | 待删除的文件或文件夹的uuid
**返回**: 成功信息“删除成功”
----------
#### /api/matter/delete/batch
**功能**:批量删除文件或文件夹
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
uuids | `string` | 必填 | 待删除的文件或文件夹的uuids,用逗号(,)分隔。
**返回**: 成功信息“删除成功”
----------
#### /api/matter/rename
**功能**:重命名文件或文件夹
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
uuid | `string` | 必填 | 文件的uuid
name | `string` | 必填 | 新名字,不能包含以下特殊符号:`< > \| * ? / \`
**返回**: 刚重命名的这个文件的`Matter`实体
----------
#### /api/matter/change/privacy
**功能**:改变文件的公私有属性
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
uuid | `string` | 必填 | 文件的uuid
privacy | `bool` | 选填 | 文件的私有性,默认`false`
**返回**: 成功信息“设置成功”
----------
#### /api/matter/move
**功能**:将一个文件夹或者文件移入到另一个文件夹下。
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
srcUuids | `string` | 必填 | 待移动的文件或文件夹的uuids,用逗号(,)分隔。
destUuid | `string` | 必填 | 目标文件夹,根目录用`root`
**返回**: 成功信息“设置成功”
----------
#### /api/matter/detail
**功能**:产看文件详情
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
uuid | `string` | 必填 | 该文件的uuid。
destUuid | `string` | 必填 | 目标文件夹,根目录用`root`
**返回**: 这个文件的`Matter`实体
----------
#### /api/matter/page
**功能**:按照分页的方式获取某个文件夹下文件和子文件夹的列表
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
puuid | `string` | 选填 | 文件夹uuid,如果根目录填`root`
page | `int` | 选填 | 当前页数,0基,默认0
pageSize | `int` | 选填 | 每页条目数,默认200
userUuid | `string` | 选填 | 筛选文件拥有者,对于普通用户使用当前登录的用户uuid.
name | `string` | 选填 | 模糊筛选文件名
dir | `bool` | 选填 | 筛选是否为文件夹
orderDir | `DESC`或`ASC` | 选填 | 按文件夹排序,`DESC`降序排,`ASC`升序排
orderCreateTime | `DESC`或`ASC` | 选填 | 按创建时间排序,`DESC`降序排,`ASC`升序排
orderSize | `DESC`或`ASC` | 选填 | 按文件大小排序,`DESC`降序排,`ASC`升序排
orderName | `DESC`或`ASC` | 选填 | 按名称排序,`DESC`降序排,`ASC`升序排
extensions | `string` | 选填 | 按文件后缀名筛选,逗号(,)分隔。例:`jpg,png,pdf`
**返回**: `Matter`的`Pager`
----------
#### /api/alien/download/{uuid}/{filename}
这个接口的定义在在`AlienController`中的,具体使用请参考[蓝眼云盘编程接口](https://github.com/eyebluecn/tank/blob/master/build/doc/alien_zh.md) 这里只做简单的下载使用。
**功能**:下载文件。
**访问级别**:`游客`,`注册用户`,`管理员`
**请求参数**: 均是放置在url中
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
uuid | `string` | 必填 | 要下载的文件uuid
filename | `string` | 必填 | 要下载的文件名
**返回**: 二进制的文件
----------
### UserController
该Controller负责站内创建文件夹,上传文件,删除文件,修改权限等,主要操作`Matter`实体
----------
#### /api/user/create
**功能**:创建用户
**访问级别**:`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
username | `string` | 必填 | 用户名,且只能包含字母,数字和'_'
password | `string` | 必填 | 密码,密码长度至少为6位
email | `string` | 必填 | 邮箱,作为用户登录的凭证
avatarUrl | `string` | 选填 | 头像
phone | `string` | 选填 | 电话
gender | `string` | 选填 | 性别
role | `string` | 选填 | 角色
city | `string` | 选填 | 城市
sizeLimit | `string` | 选填 | 用户上传单文件限制,单位byte. 如果负数表示无限制。
**返回**: 新建的`User`实体
----------
#### /api/user/edit
**功能**:编辑用户
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
uuid | `string` | 必填 | 待编辑的用户uuid
avatarUrl | `string` | 选填 | 头像
phone | `string` | 选填 | 电话
gender | `string` | 选填 | 性别
role | `string` | 选填 | 角色
city | `string` | 选填 | 城市
sizeLimit | `string` | 选填 | 用户上传单文件限制,单位byte. 如果负数表示无限制。
**返回**: 编辑的`User`实体
----------
#### /api/user/change/password
**功能**:用户修改密码
**访问级别**:`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
oldPassword | `string` | 必填 | 旧密码
newPassword | `string` | 必填 | 新密码
**返回**: 修改密码的`User`实体
----------
#### /api/user/change/password
**功能**:管理员重置用户密码
**访问级别**:`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
userUuid | `string` | 必填 | 待重置密码的用户uuid
password | `string` | 必填 | 密码
**返回**: 修改密码的`User`实体
----------
#### /api/user/login
**功能**:登录
**访问级别**:`游客`,`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
email | `string` | 必填 | 邮箱
password | `string` | 必填 | 密码
**返回**: 当前登录的`User`实体
----------
#### /api/user/logout
**功能**:登录
**访问级别**:`注册用户`,`管理员`
**请求参数**:无
**返回**: 成功信息"退出成功!"
----------
#### /api/user/detail
**功能**:查看用户详情
**访问级别**:`游客`,`注册用户`,`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
uuid | `string` | 必填 | 待查看的用户uuid
**返回**: `User`实体
----------
#### /api/user/page
**功能**:查看用户列表
**访问级别**:`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
page | `int` | 选填 | 当前页数,0基,默认0
pageSize | `int` | 选填 | 每页条目数,默认200
username | `string` | 选填 | 模糊筛选用户名
email | `string` | 选填 | 模糊筛选邮箱
phone | `string` | 选填 | 模糊筛选电话
orderLastTime | `DESC`或`ASC` | 选填 | 按上次登录时间排序,`DESC`降序排,`ASC`升序排
orderCreateTime | `DESC`或`ASC` | 选填 | 按创建时间排序,`DESC`降序排,`ASC`升序排
**返回**: `User`实体的`Pager`
----------
#### /api/user/disable
**功能**:禁用用户
**访问级别**:`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
uuid | `string` | 必填 | 待操作的用户
**返回**: 被操作的用户`User`实体
----------
#### /api/user/enable
**功能**:启用用户
**访问级别**:`管理员`
**请求参数**:
名称 | 类型 | 必填性 | 描述
--------- | ---- | ---- | -----------
uuid | `string` | 必填 | 待操作的用户
**返回**: 被操作的用户`User`实体
----------
### AlienController
这个Controller主要暴露给第三方使用,详情请参考:[蓝眼云盘编程接口](https://github.com/eyebluecn/tank/blob/master/build/doc/alien_zh.md)
-14
View File
@@ -1,14 +0,0 @@
# Package dependency
```
main
├── bin
├── pkg
├── src
│   ├── github.com
│   ├── golang.org
│   ├── tank
│   │   ├── build
│   │   │   ├── conf
│   │   │   ├── doc
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

+193
View File
@@ -0,0 +1,193 @@
CREATE TABLE `tank31_bridge` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`share_uuid` char(36) DEFAULT NULL,
`matter_uuid` char(36) DEFAULT NULL,
PRIMARY KEY (`uuid`),
UNIQUE KEY `uuid` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `tank31_dashboard` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`invoke_num` bigint(20) NOT NULL,
`total_invoke_num` bigint(20) NOT NULL DEFAULT '0',
`uv` bigint(20) NOT NULL DEFAULT '0',
`total_uv` bigint(20) NOT NULL DEFAULT '0',
`matter_num` bigint(20) NOT NULL DEFAULT '0',
`total_matter_num` bigint(20) NOT NULL DEFAULT '0',
`file_size` bigint(20) NOT NULL DEFAULT '0',
`total_file_size` bigint(20) NOT NULL DEFAULT '0',
`avg_cost` bigint(20) NOT NULL DEFAULT '0',
`dt` varchar(45) NOT NULL,
PRIMARY KEY (`uuid`),
UNIQUE KEY `uuid` (`uuid`),
KEY `idx_dt` (`dt`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `tank31_download_token` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`user_uuid` char(36) NOT NULL,
`matter_uuid` char(36) NOT NULL,
`expire_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`ip` varchar(128) NOT NULL,
PRIMARY KEY (`uuid`),
UNIQUE KEY `uuid` (`uuid`),
KEY `idx_mu` (`matter_uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `tank31_footprint` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`user_uuid` char(36) DEFAULT NULL,
`ip` varchar(128) NOT NULL,
`host` varchar(45) NOT NULL,
`uri` varchar(255) NOT NULL,
`params` text,
`cost` bigint(20) NOT NULL DEFAULT '0',
`success` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`uuid`),
UNIQUE KEY `uuid` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `tank31_image_cache` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`name` varchar(255) NOT NULL,
`user_uuid` char(36) DEFAULT NULL,
`username` varchar(45) NOT NULL,
`matter_uuid` char(36) DEFAULT NULL,
`matter_name` varchar(255) NOT NULL,
`mode` varchar(512) DEFAULT NULL,
`md5` varchar(45) DEFAULT NULL,
`size` bigint(20) NOT NULL DEFAULT '0',
`path` varchar(512) DEFAULT NULL,
PRIMARY KEY (`uuid`),
UNIQUE KEY `uuid` (`uuid`),
KEY `idx_mu` (`matter_uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `tank31_matter` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`puuid` char(36) DEFAULT NULL,
`user_uuid` char(36) DEFAULT NULL,
`username` varchar(45) NOT NULL,
`dir` tinyint(1) NOT NULL DEFAULT '0',
`name` varchar(255) NOT NULL,
`md5` varchar(45) DEFAULT NULL,
`size` bigint(20) NOT NULL DEFAULT '0',
`privacy` tinyint(1) NOT NULL DEFAULT '0',
`path` varchar(1024) DEFAULT NULL,
`times` bigint(20) NOT NULL DEFAULT '0',
`prop` varchar(1024) NOT NULL DEFAULT '{}',
`visit_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`deleted` tinyint(1) NOT NULL DEFAULT '0',
`delete_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
PRIMARY KEY (`uuid`),
UNIQUE KEY `uuid` (`uuid`),
KEY `idx_puuid` (`puuid`),
KEY `idx_uu` (`user_uuid`),
KEY `idx_del` (`deleted`),
KEY `idx_delt` (`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `tank31_preference` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`name` varchar(45) DEFAULT NULL,
`logo_url` varchar(255) DEFAULT NULL,
`favicon_url` varchar(255) DEFAULT NULL,
`copyright` varchar(1024) DEFAULT NULL,
`record` varchar(1024) DEFAULT NULL,
`download_dir_max_size` bigint(20) NOT NULL DEFAULT '-1',
`download_dir_max_num` bigint(20) NOT NULL DEFAULT '-1',
`default_total_size_limit` bigint(20) NOT NULL DEFAULT '-1',
`allow_register` tinyint(1) NOT NULL DEFAULT '0',
`preview_config` text,
`scan_config` text,
`deleted_keep_days` bigint(20) NOT NULL DEFAULT '7',
PRIMARY KEY (`uuid`),
UNIQUE KEY `uuid` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `tank31_session` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`user_uuid` char(36) DEFAULT NULL,
`ip` varchar(128) NOT NULL,
`expire_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
PRIMARY KEY (`uuid`),
UNIQUE KEY `uuid` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `tank31_share` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`name` varchar(255) DEFAULT NULL,
`share_type` varchar(45) DEFAULT NULL,
`username` varchar(45) DEFAULT NULL,
`user_uuid` char(36) DEFAULT NULL,
`download_times` bigint(20) NOT NULL DEFAULT '0',
`code` varchar(45) NOT NULL,
`expire_infinity` tinyint(1) NOT NULL DEFAULT '0',
`expire_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
PRIMARY KEY (`uuid`),
UNIQUE KEY `uuid` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `tank31_upload_token` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`user_uuid` char(36) NOT NULL,
`folder_uuid` char(36) NOT NULL,
`matter_uuid` char(36) NOT NULL,
`expire_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`filename` varchar(255) NOT NULL,
`privacy` tinyint(1) NOT NULL DEFAULT '0',
`size` bigint(20) NOT NULL DEFAULT '0',
`ip` varchar(128) NOT NULL,
PRIMARY KEY (`uuid`),
UNIQUE KEY `uuid` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `tank31_user` (
`uuid` char(36) NOT NULL DEFAULT '',
`sort` bigint(20) NOT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`create_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`role` varchar(45) DEFAULT NULL,
`username` varchar(45) NOT NULL,
`password` varchar(255) DEFAULT NULL,
`avatar_url` varchar(255) DEFAULT NULL,
`last_ip` varchar(128) DEFAULT NULL,
`last_time` timestamp NOT NULL DEFAULT '2018-01-01 00:00:00',
`size_limit` bigint(20) NOT NULL DEFAULT '-1',
`total_size_limit` bigint(20) NOT NULL DEFAULT '-1',
`total_size` bigint(20) NOT NULL DEFAULT '0',
`status` varchar(45) DEFAULT NULL,
PRIMARY KEY (`uuid`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `uuid` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-267
View File
@@ -1,267 +0,0 @@
#### PROPFIND 列出目录情况
request method
```
PROPFIND
```
request header
```
Authorization=Basic YWRtaW46YWRtaW4=
Content-Type=text/xml
Accept-Encoding=gzip
Depth=infinity
```
request body
```
<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:resourcetype />
<D:getcontentlength />
<D:creationdate />
<D:getlastmodified />
</D:prop>
</D:propfind>
```
response body
```
<?xml version="1.0" encoding="UTF-8"?>
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>/api/dav/</D:href>
<D:propstat>
<D:prop>
<D:displayname>dav</D:displayname>
<D:getlastmodified>Tue, 16 Apr 2019 17:50:59 GMT</D:getlastmodified>
<D:supportedlock>
<D:lockentry xmlns:D="DAV:">
<D:lockscope>
<D:exclusive/>
</D:lockscope>
<D:locktype>
<D:write/>
</D:locktype>
</D:lockentry>
</D:supportedlock>
<D:resourcetype>
<D:collection xmlns:D="DAV:"/>
</D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/api/dav/api/</D:href>
<D:propstat>
<D:prop>
<D:displayname>api</D:displayname>
<D:getlastmodified>Tue, 16 Apr 2019 17:51:03 GMT</D:getlastmodified>
<D:supportedlock>
<D:lockentry xmlns:D="DAV:">
<D:lockscope>
<D:exclusive/>
</D:lockscope>
<D:locktype>
<D:write/>
</D:locktype>
</D:lockentry>
</D:supportedlock>
<D:resourcetype>
<D:collection xmlns:D="DAV:"/>
</D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/api/dav/api/dav/</D:href>
<D:propstat>
<D:prop>
<D:displayname>dav</D:displayname>
<D:getlastmodified>Tue, 16 Apr 2019 17:51:38 GMT</D:getlastmodified>
<D:supportedlock>
<D:lockentry xmlns:D="DAV:">
<D:lockscope>
<D:exclusive/>
</D:lockscope>
<D:locktype>
<D:write/>
</D:locktype>
</D:lockentry>
</D:supportedlock>
<D:resourcetype>
<D:collection xmlns:D="DAV:"/>
</D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/api/dav/api/dav/body.txt</D:href>
<D:propstat>
<D:prop>
<D:displayname>body.txt</D:displayname>
<D:getlastmodified>Tue, 16 Apr 2019 17:51:38 GMT</D:getlastmodified>
<D:getcontenttype>text/plain; charset=utf-8</D:getcontenttype>
<D:getetag>"159605ccc1d0f3c410"</D:getetag>
<D:supportedlock>
<D:lockentry xmlns:D="DAV:">
<D:lockscope>
<D:exclusive/>
</D:lockscope>
<D:locktype>
<D:write/>
</D:locktype>
</D:lockentry>
</D:supportedlock>
<D:resourcetype></D:resourcetype>
<D:getcontentlength>16</D:getcontentlength>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/api/dav/api/dav/cat.txt</D:href>
<D:propstat>
<D:prop>
<D:resourcetype></D:resourcetype>
<D:getcontentlength>24</D:getcontentlength>
<D:getlastmodified>Tue, 16 Apr 2019 17:51:19 GMT</D:getlastmodified>
<D:getcontenttype>text/plain; charset=utf-8</D:getcontenttype>
<D:getetag>"159605c862b0d64c18"</D:getetag>
<D:supportedlock>
<D:lockentry xmlns:D="DAV:">
<D:lockscope>
<D:exclusive/>
</D:lockscope>
<D:locktype>
<D:write/>
</D:locktype>
</D:lockentry>
</D:supportedlock>
<D:displayname>cat.txt</D:displayname>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/api/dav/cat/</D:href>
<D:propstat>
<D:prop>
<D:getlastmodified>Sat, 13 Apr 2019 16:55:54 GMT</D:getlastmodified>
<D:supportedlock>
<D:lockentry xmlns:D="DAV:">
<D:lockscope>
<D:exclusive/>
</D:lockscope>
<D:locktype>
<D:write/>
</D:locktype>
</D:lockentry>
</D:supportedlock>
<D:displayname>cat</D:displayname>
<D:resourcetype>
<D:collection xmlns:D="DAV:"/>
</D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/api/dav/cat/dog/</D:href>
<D:propstat>
<D:prop>
<D:getlastmodified>Sat, 13 Apr 2019 16:55:58 GMT</D:getlastmodified>
<D:supportedlock>
<D:lockentry xmlns:D="DAV:">
<D:lockscope>
<D:exclusive/>
</D:lockscope>
<D:locktype>
<D:write/>
</D:locktype>
</D:lockentry>
</D:supportedlock>
<D:displayname>dog</D:displayname>
<D:resourcetype>
<D:collection xmlns:D="DAV:"/>
</D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/api/dav/cat/dog/pig/</D:href>
<D:propstat>
<D:prop>
<D:resourcetype>
<D:collection xmlns:D="DAV:"/>
</D:resourcetype>
<D:getlastmodified>Sat, 13 Apr 2019 16:56:08 GMT</D:getlastmodified>
<D:supportedlock>
<D:lockentry xmlns:D="DAV:">
<D:lockscope>
<D:exclusive/>
</D:lockscope>
<D:locktype>
<D:write/>
</D:locktype>
</D:lockentry>
</D:supportedlock>
<D:displayname>pig</D:displayname>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/api/dav/cat/dog/pig/hi.txt</D:href>
<D:propstat>
<D:prop>
<D:getlastmodified>Sat, 13 Apr 2019 16:56:08 GMT</D:getlastmodified>
<D:getcontenttype>text/plain; charset=utf-8</D:getcontenttype>
<D:getetag>"15951707dc1116d87"</D:getetag>
<D:supportedlock>
<D:lockentry xmlns:D="DAV:">
<D:lockscope>
<D:exclusive/>
</D:lockscope>
<D:locktype>
<D:write/>
</D:locktype>
</D:lockentry>
</D:supportedlock>
<D:displayname>hi.txt</D:displayname>
<D:resourcetype></D:resourcetype>
<D:getcontentlength>7</D:getcontentlength>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<D:response>
<D:href>/api/dav/morning.txt</D:href>
<D:propstat>
<D:prop>
<D:resourcetype></D:resourcetype>
<D:getcontentlength>13</D:getcontentlength>
<D:displayname>morning.txt</D:displayname>
<D:getlastmodified>Sat, 13 Apr 2019 16:52:08 GMT</D:getlastmodified>
<D:getcontenttype>text/plain; charset=utf-8</D:getcontenttype>
<D:getetag>"159516cfe790beecd"</D:getetag>
<D:supportedlock>
<D:lockentry xmlns:D="DAV:">
<D:lockscope>
<D:exclusive/>
</D:lockscope>
<D:locktype>
<D:write/>
</D:locktype>
</D:lockentry>
</D:supportedlock>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>
```
+28
View File
@@ -0,0 +1,28 @@
{
"files": {
"main.css": "/static/css/main.b03c51fb.css",
"main.js": "/static/js/main.b94b1947.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",
"index.html": "/index.html",
"main.b03c51fb.css.map": "/static/css/main.b03c51fb.css.map",
"main.b94b1947.js.map": "/static/js/main.b94b1947.js.map"
},
"entrypoints": [
"static/css/main.b03c51fb.css",
"static/js/main.b94b1947.js"
]
}
+1 -1
View File
@@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>EyeblueTank</title><link href=/static/css/app.a7719e8c.css rel=preload as=style><link href=/static/css/chunk-vendors.fc2d671c.css rel=preload as=style><link href=/static/js/app.e7fcae17.js rel=preload as=script><link href=/static/js/chunk-vendors.220ccae9.js rel=preload as=script><link href=/static/css/chunk-vendors.fc2d671c.css rel=stylesheet><link href=/static/css/app.a7719e8c.css rel=stylesheet></head><body><noscript><strong>We're sorry but tank-front-tmp doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/static/js/chunk-vendors.220ccae9.js></script><script src=/static/js/app.e7fcae17.js></script></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.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>
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
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 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="1544085034574" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1828" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M0 512A512 512 0 1 0 512 0 512 512 0 0 0 0 512" fill="#36D49F" p-id="1829"></path><path d="M450.37037 552.580741l-115.105185-87.798519-50.820741 39.822222L483.365926 701.62963C517.499259 616.296296 625.777778 449.232593 758.518519 330.714074L746.382222 303.407407a1264.071111 1264.071111 0 0 0-296.011852 249.173334" fill="#FFFFFF" p-id="1830"></path></svg>

Before

Width:  |  Height:  |  Size: 743 B

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
@@ -0,0 +1,70 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
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. */
/** @license React v0.20.2
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
//! moment.js
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 828 B

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 943 B

After

Width:  |  Height:  |  Size: 943 B

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

+2 -2
View File
@@ -11,10 +11,10 @@
@REM prepare the variables.
@REM version name
SET VERSION_NAME=tank-3.0.4
SET VERSION_NAME=tank-3.1.6
ECHO VERSION_NAME: %VERSION_NAME%
@REM golang proxy
SET GOPROXY=https://athens.azurefd.net
SET GOPROXY=https://goproxy.cn
ECHO GOPROXY: %GOPROXY%
@REM assign variable like Linux GOARCH=$(go env GOARCH) eg. amd64
FOR /f %%i IN ('go env GOARCH') DO SET GOARCH=%%i
+4 -4
View File
@@ -9,10 +9,10 @@
#prepare the variables.
# version name
VERSION_NAME=tank-3.0.4
VERSION_NAME=tank-3.1.6
echo "VERSION_NAME: ${VERSION_NAME}"
# golang proxy
GOPROXY=https://athens.azurefd.net
GOPROXY=https://goproxy.cn
echo "GOPROXY: ${GOPROXY}"
# eg. amd64
GOARCH=$(go env GOARCH)
@@ -47,8 +47,8 @@ echo "DIST_PATH: ${DIST_PATH}"
cd ${PROJECT_DIR}
echo "go build -mod=readonly"
go build -mod=readonly
echo "go build"
go build
# if a directory
if [[ -d COMPONENT_DIR ]] ; then
+2 -2
View File
@@ -6,10 +6,10 @@ After=network.target
[Service]
Type=simple
DynamicUser=yes
#DynamicUser=yes
ExecStart=/data/program/tank/tank
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target
+12 -4
View File
@@ -1,5 +1,7 @@
package core
import "gorm.io/gorm/schema"
const (
//authentication key of cookie
COOKIE_AUTH_KEY = "_ak"
@@ -9,19 +11,25 @@ const (
DEFAULT_SERVER_PORT = 6010
//db table's prefix. tank30_ means current version is tank:3.0.x
TABLE_PREFIX = "tank30_"
//db table's prefix. tank31_ means current version is tank:3.1.x
TABLE_PREFIX = "tank31_"
VERSION = "3.0.4"
VERSION = "3.1.6"
)
type Config interface {
Installed() bool
ServerPort() int
//get the db type
DbType() string
//get the mysql url. eg. tank:tank123@tcp(127.0.0.1:3306)/tank?charset=utf8&parseTime=True&loc=Local
MysqlUrl() string
//get the sqlite path
SqliteFolder() string
//files storage location.
MatterPath() string
//table name strategy
NamingStrategy() schema.NamingStrategy
//when installed by user. Write configs to tank.json
FinishInstall(mysqlPort int, mysqlHost string, mysqlSchema string, mysqlUsername string, mysqlPassword string)
FinishInstall(dbType string, mysqlPort int, mysqlHost string, mysqlSchema string, mysqlUsername string, mysqlPassword string, mysqlCharset string)
}
+1 -1
View File
@@ -2,7 +2,7 @@ package core
import (
"github.com/eyebluecn/tank/code/tool/cache"
"github.com/jinzhu/gorm"
"gorm.io/gorm"
"net/http"
)
+3 -19
View File
@@ -2,7 +2,6 @@ package rest
import (
"math"
"time"
)
const (
@@ -11,26 +10,11 @@ const (
DIRECTION_ASC = "ASC"
DIRECTION_DESC = "DESC"
EMPTY_JSON_MAP = "{}"
EMPTY_JSON_ARRAY = "[]"
)
type IBase interface {
//name of db table
TableName() string
}
// Mysql 5.5 only support one CURRENT_TIMESTAMP
// so we use 2018-01-01 00:00:00 as default, which is the first release date of EyeblueTank
type Base 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'"`
}
func (this *Base) TableName() string {
panic("you should overwrite TableName()")
}
//pager
type Pager struct {
Page int `json:"page"`
+12 -5
View File
@@ -4,9 +4,8 @@ import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/jinzhu/gorm"
"github.com/nu7hatch/gouuid"
"github.com/eyebluecn/tank/code/tool/uuid"
"gorm.io/gorm"
"time"
)
@@ -59,7 +58,7 @@ func (this *BridgeDao) CheckByShareUuidAndMatterUuid(shareUuid string, matterUui
}
//get pager
func (this *BridgeDao) Page(page int, pageSize int, shareUuid string, sortArray []builder.OrderPair) *Pager {
func (this *BridgeDao) PlainPage(page int, pageSize int, shareUuid string, sortArray []builder.OrderPair) (int, []*Bridge) {
var wp = &builder.WherePair{}
@@ -70,13 +69,21 @@ func (this *BridgeDao) Page(page int, pageSize int, shareUuid string, sortArray
var conditionDB *gorm.DB
conditionDB = core.CONTEXT.GetDB().Model(&Bridge{}).Where(wp.Query, wp.Args...)
count := 0
var count int64 = 0
db := conditionDB.Count(&count)
this.PanicError(db.Error)
var bridges []*Bridge
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&bridges)
this.PanicError(db.Error)
return int(count), bridges
}
//get pager
func (this *BridgeDao) Page(page int, pageSize int, shareUuid string, sortArray []builder.OrderPair) *Pager {
count, bridges := this.PlainPage(page, pageSize, shareUuid, sortArray)
pager := NewPager(page, pageSize, count, bridges)
return pager
+7 -10
View File
@@ -1,18 +1,15 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
)
import "time"
/**
* the link table for Share and Matter.
*/
type Bridge struct {
Base
ShareUuid string `json:"shareUuid" gorm:"type:char(36)"`
MatterUuid string `json:"matterUuid" gorm:"type:char(36)"`
}
func (this *Bridge) TableName() string {
return core.TABLE_PREFIX + "bridge"
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'"`
ShareUuid string `json:"shareUuid" gorm:"type:char(36)"`
MatterUuid string `json:"matterUuid" gorm:"type:char(36)"`
}
+7
View File
@@ -34,6 +34,7 @@ func (this *DashboardController) RegisterRoutes() map[string]func(writer http.Re
routeMap["/api/dashboard/page"] = this.Wrap(this.Page, USER_ROLE_ADMINISTRATOR)
routeMap["/api/dashboard/active/ip/top10"] = this.Wrap(this.ActiveIpTop10, USER_ROLE_ADMINISTRATOR)
routeMap["/api/dashboard/etl"] = this.Wrap(this.Etl, USER_ROLE_ADMINISTRATOR)
return routeMap
}
@@ -89,3 +90,9 @@ func (this *DashboardController) ActiveIpTop10(writer http.ResponseWriter, reque
list := this.dashboardDao.ActiveIpTop10()
return this.Success(list)
}
func (this *DashboardController) Etl(writer http.ResponseWriter, request *http.Request) *result.WebResult {
this.dashboardService.Etl()
return this.Success("OK")
}
+4 -4
View File
@@ -3,8 +3,8 @@ package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/jinzhu/gorm"
"github.com/nu7hatch/gouuid"
"github.com/eyebluecn/tank/code/tool/uuid"
"gorm.io/gorm"
"time"
)
@@ -61,14 +61,14 @@ func (this *DashboardDao) Page(page int, pageSize int, dt string, sortArray []bu
var conditionDB *gorm.DB
conditionDB = core.CONTEXT.GetDB().Model(&Dashboard{}).Where(wp.Query, wp.Args...)
count := 0
var count int64 = 0
db := conditionDB.Count(&count)
this.PanicError(db.Error)
var dashboards []*Dashboard
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&dashboards)
this.PanicError(db.Error)
pager := NewPager(page, pageSize, count, dashboards)
pager := NewPager(page, pageSize, int(count), dashboards)
return pager
}
+15 -17
View File
@@ -1,27 +1,25 @@
package rest
import "github.com/eyebluecn/tank/code/core"
import "time"
/**
* application's dashboard.
*/
type Dashboard struct {
Base
InvokeNum int64 `json:"invokeNum" gorm:"type:bigint(20) not null"` //api invoke num.
TotalInvokeNum int64 `json:"totalInvokeNum" gorm:"type:bigint(20) not null;default:0"` //total invoke num up to now.
Uv int64 `json:"uv" gorm:"type:bigint(20) not null;default:0"` //today's uv
TotalUv int64 `json:"totalUv" gorm:"type:bigint(20) not null;default:0"` //total uv
MatterNum int64 `json:"matterNum" gorm:"type:bigint(20) not null;default:0"` //file's num
TotalMatterNum int64 `json:"totalMatterNum" gorm:"type:bigint(20) not null;default:0"` //file's total number
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_dt"` //date
}
// set File's table name to be `profiles`
func (this *Dashboard) TableName() string {
return core.TABLE_PREFIX + "dashboard"
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'"`
InvokeNum int64 `json:"invokeNum" gorm:"type:bigint(20) not null"` //api invoke num.
TotalInvokeNum int64 `json:"totalInvokeNum" gorm:"type:bigint(20) not null;default:0"` //total invoke num up to now.
Uv int64 `json:"uv" gorm:"type:bigint(20) not null;default:0"` //today's uv
TotalUv int64 `json:"totalUv" gorm:"type:bigint(20) not null;default:0"` //total uv
MatterNum int64 `json:"matterNum" gorm:"type:bigint(20) not null;default:0"` //file's num
TotalMatterNum int64 `json:"totalMatterNum" gorm:"type:bigint(20) not null;default:0"` //file's total number
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.
}
/**
+16 -18
View File
@@ -3,7 +3,6 @@ package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/util"
"github.com/robfig/cron"
"time"
)
@@ -49,25 +48,24 @@ func (this *DashboardService) Init() {
func (this *DashboardService) Bootstrap() {
this.logger.Info("[cron job] Everyday 00:05 ETL dashboard data.")
expression := "0 5 0 * * ?"
cronJob := cron.New()
err := cronJob.AddFunc(expression, this.etl)
core.PanicError(err)
cronJob.Start()
this.logger.Info("Immediately ETL dashboard data.")
//do the etl method now.
go core.RunWithRecovery(this.etl)
go core.RunWithRecovery(this.Etl)
}
func (this *DashboardService) Etl() {
this.etlOneDay(util.Yesterday())
this.etlOneDay(time.Now())
}
// handle the dashboard data.
func (this *DashboardService) etl() {
func (this *DashboardService) etlOneDay(thenTime time.Time) {
this.logger.Info("ETL dashboard data.")
startTime := util.FirstSecondOfDay(util.Yesterday())
endTime := util.LastSecondOfDay(util.Yesterday())
startTime := util.FirstSecondOfDay(thenTime)
endTime := util.LastSecondOfDay(thenTime)
dt := util.ConvertTimeToDateString(startTime)
now := time.Now()
longTimeAgo := time.Now()
longTimeAgo = longTimeAgo.AddDate(-20, 0, 0)
@@ -81,19 +79,19 @@ func (this *DashboardService) etl() {
}
invokeNum := this.footprintDao.CountBetweenTime(startTime, endTime)
totalInvokeNum := this.footprintDao.CountBetweenTime(longTimeAgo, endTime)
totalInvokeNum := this.footprintDao.CountBetweenTime(longTimeAgo, now)
uv := this.footprintDao.UvBetweenTime(startTime, endTime)
totalUv := this.footprintDao.UvBetweenTime(longTimeAgo, endTime)
totalUv := this.footprintDao.UvBetweenTime(longTimeAgo, now)
matterNum := this.matterDao.CountBetweenTime(startTime, endTime)
totalMatterNum := this.matterDao.CountBetweenTime(longTimeAgo, endTime)
totalMatterNum := this.matterDao.CountBetweenTime(longTimeAgo, now)
matterSize := this.matterDao.SizeBetweenTime(startTime, endTime)
totalMatterSize := this.matterDao.SizeBetweenTime(longTimeAgo, endTime)
totalMatterSize := this.matterDao.SizeBetweenTime(longTimeAgo, now)
cacheSize := this.imageCacheDao.SizeBetweenTime(startTime, endTime)
totalCacheSize := this.imageCacheDao.SizeBetweenTime(longTimeAgo, endTime)
totalCacheSize := this.imageCacheDao.SizeBetweenTime(longTimeAgo, now)
avgCost := this.footprintDao.AvgCostBetweenTime(startTime, endTime)
+3 -2
View File
@@ -18,7 +18,7 @@ import (
* WebDav document
* https://tools.ietf.org/html/rfc4918
* http://www.webdav.org/specs/rfc4918.html
*
* test machine: http://www.webdav.org/neon/litmus/
*/
type DavController struct {
@@ -145,7 +145,7 @@ func (this *DavController) debug(writer http.ResponseWriter, request *http.Reque
bodyBytes, err := ioutil.ReadAll(request.Body)
if err != nil {
fmt.Println("读取body时出错" + err.Error())
fmt.Println("occur error when reading body: " + err.Error())
}
fmt.Println(string(bodyBytes))
@@ -162,6 +162,7 @@ func (this *DavController) debug(writer http.ResponseWriter, request *http.Reque
func (this *DavController) Index(writer http.ResponseWriter, request *http.Request, subPath string) {
//when debugging. open it.
//this.debug(writer, request, subPath)
user := this.CheckCurrentUser(writer, request)
+424 -21
View File
@@ -7,11 +7,14 @@ import (
"github.com/eyebluecn/tank/code/tool/dav/xml"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
"github.com/eyebluecn/tank/code/tool/webdav"
"io/ioutil"
"net/http"
"net/url"
"path"
"regexp"
"strings"
"time"
)
/**
@@ -26,6 +29,7 @@ type DavService struct {
BaseBean
matterDao *MatterDao
matterService *MatterService
lockSystem webdav.LockSystem
}
func (this *DavService) Init() {
@@ -41,6 +45,8 @@ func (this *DavService) Init() {
this.matterService = b
}
// init the webdav lock system.
this.lockSystem = webdav.NewMemLS()
}
//get the depth in header. Not support infinity yet.
@@ -87,7 +93,8 @@ func (this *DavService) PropstatsFromXmlNames(user *User, matter *Matter, xmlNam
propstats := make([]dav.Propstat, 0)
var properties []dav.Property
var okProperties []dav.Property
var notFoundProperties []dav.Property
for _, xmlName := range xmlNames {
//TODO: deadprops not implement yet.
@@ -96,22 +103,48 @@ func (this *DavService) PropstatsFromXmlNames(user *User, matter *Matter, xmlNam
if liveProp := LivePropMap[xmlName]; liveProp.findFn != nil && (liveProp.dir || !matter.Dir) {
innerXML := liveProp.findFn(user, matter)
properties = append(properties, dav.Property{
okProperties = append(okProperties, dav.Property{
XMLName: xmlName,
InnerXML: []byte(innerXML),
})
} else {
this.logger.Info("%s %s cannot finish.", matter.Path, xmlName.Local)
this.logger.Info("handle props %s %s.", matter.Path, xmlName.Local)
propMap := matter.FetchPropMap()
if value, isPresent := propMap[xmlName.Local]; isPresent {
okProperties = append(okProperties, dav.Property{
XMLName: xmlName,
InnerXML: []byte(value),
})
} else {
//only accept Space not null.
if xmlName.Space != "" {
//collect not found props
notFoundProperties = append(notFoundProperties, dav.Property{
XMLName: xmlName,
InnerXML: []byte(""),
})
}
}
}
}
if len(properties) == 0 {
if len(okProperties) == 0 && len(notFoundProperties) == 0 {
panic(result.BadRequest("cannot parse request properties"))
}
okPropstat := dav.Propstat{Status: http.StatusOK, Props: properties}
if len(okProperties) != 0 {
okPropstat := dav.Propstat{Status: http.StatusOK, Props: okProperties}
propstats = append(propstats, okPropstat)
}
propstats = append(propstats, okPropstat)
if len(notFoundProperties) != 0 {
notFoundPropstat := dav.Propstat{Status: http.StatusNotFound, Props: notFoundProperties}
propstats = append(propstats, notFoundPropstat)
}
return propstats
@@ -154,6 +187,7 @@ func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http
fmt.Printf("PROPFIND %s\n", subPath)
// read depth
depth := this.ParseDepth(request)
propfind := dav.ReadPropfind(request.Body)
@@ -166,7 +200,7 @@ func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http
matters = []*Matter{matter}
} else {
// len(matters) == 0 means empty directory
matters = this.matterDao.FindByPuuidAndUserUuid(matter.Uuid, user.Uuid, nil)
matters = this.matterDao.FindByPuuidAndUserUuidAndDeleted(matter.Uuid, user.Uuid, FALSE, nil)
//add this matter to head.
matters = append([]*Matter{matter}, matters...)
@@ -194,6 +228,74 @@ func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http
}
//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)
// handle the lock feature.
reqPath, status, err := this.stripPrefix(request.URL.Path)
if err != nil {
panic(result.StatusCodeWebResult(status, err.Error()))
}
release, status, err := this.confirmLocks(request, reqPath, "")
if err != nil {
panic(result.StatusCodeWebResult(status, err.Error()))
}
if release != nil {
defer release()
}
matter := this.matterDao.checkByUserUuidAndPath(user.Uuid, subPath)
patches, status, err := webdav.ReadProppatch(request.Body)
this.PanicError(err)
fmt.Println("status:%v", status)
//prepare a multiStatusWriter.
multiStatusWriter := &dav.MultiStatusWriter{Writer: writer}
propstats := make([]dav.Propstat, 0)
propMap := matter.FetchPropMap()
for _, patch := range patches {
propStat := dav.Propstat{Status: http.StatusOK}
if patch.Remove {
if len(patch.Props) > 0 {
property := patch.Props[0]
if _, isPresent := propMap[property.XMLName.Local]; isPresent {
//delete the prop.
delete(propMap, property.XMLName.Local)
}
}
} else {
for _, prop := range patch.Props {
propMap[prop.XMLName.Local] = string(prop.InnerXML)
propStat.Props = append(propStat.Props, dav.Property{XMLName: xml.Name{Space: prop.XMLName.Space, Local: prop.XMLName.Local}})
}
}
propstats = append(propstats, propStat)
}
matter.SetPropMap(propMap)
// update the matter
this.matterDao.Save(matter)
visitPath := fmt.Sprintf("%s%s", WEBDAV_PREFIX, matter.Path)
response := this.makePropstatResponse(visitPath, propstats)
err1 := multiStatusWriter.Write(response)
this.PanicError(err1)
err2 := multiStatusWriter.Close()
this.PanicError(err2)
}
//handle download
func (this *DavService) HandleGetHeadPost(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
@@ -217,6 +319,23 @@ func (this *DavService) HandlePut(writer http.ResponseWriter, request *http.Requ
fmt.Printf("PUT %s\n", subPath)
// handle the lock feature.
reqPath, status, err := this.stripPrefix(request.URL.Path)
if err != nil {
panic(result.StatusCodeWebResult(status, err.Error()))
}
release, status, err := this.confirmLocks(request, reqPath, "")
if err != nil {
//if status == http.StatusLocked {
// status = http.StatusPreconditionFailed
//}
panic(result.StatusCodeWebResult(status, err.Error()))
}
if release != nil {
defer release()
}
filename := util.GetFilenameOfPath(subPath)
dirPath := util.GetDirOfPath(subPath)
@@ -230,16 +349,31 @@ func (this *DavService) HandlePut(writer http.ResponseWriter, request *http.Requ
this.matterService.Upload(request, request.Body, user, dirMatter, filename, true)
//set the status code 201
writer.WriteHeader(http.StatusCreated)
}
//delete file
func (this *DavService) HandleDelete(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
func (this *DavService) HandleDelete(w http.ResponseWriter, r *http.Request, user *User, subPath string) {
fmt.Printf("DELETE %s\n", subPath)
reqPath, status, err := this.stripPrefix(r.URL.Path)
if err != nil {
panic(result.StatusCodeWebResult(status, err.Error()))
}
release, status, err := this.confirmLocks(r, reqPath, "")
if err != nil {
panic(result.StatusCodeWebResult(status, err.Error()))
}
if release != nil {
defer release()
}
matter := this.matterDao.CheckWithRootByPath(subPath, user)
this.matterService.AtomicDelete(request, matter, user)
this.matterService.AtomicDelete(r, matter, user)
}
//crate a directory
@@ -247,10 +381,37 @@ func (this *DavService) HandleMkcol(writer http.ResponseWriter, request *http.Re
fmt.Printf("MKCOL %s\n", subPath)
//the body of MKCOL request MUST be empty. (RFC2518:8.3.1)
bodyBytes, err := ioutil.ReadAll(request.Body)
if err != nil {
fmt.Println("occur error when reading body: " + err.Error())
} else {
if len(bodyBytes) != 0 {
//throw conflict error
panic(result.CustomWebResult(result.UNSUPPORTED_MEDIA_TYPE, fmt.Sprintf("%s MKCOL should NO body", subPath)))
}
}
thisDirName := util.GetFilenameOfPath(subPath)
dirPath := util.GetDirOfPath(subPath)
dirMatter := this.matterDao.CheckWithRootByPath(dirPath, user)
dirMatter := this.matterDao.FindWithRootByPath(dirPath, user)
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)
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)
if fileMatter != nil {
panic(result.CustomWebResult(result.METHOD_NOT_ALLOWED, fmt.Sprintf("%s file already exists", dirPath)))
}
this.matterService.AtomicCreateDirectory(request, dirMatter, thisDirName, user)
@@ -352,7 +513,11 @@ func (this *DavService) prepareMoveCopy(
panic(result.BadRequest("you cannot move the root directory"))
}
destDirMatter = this.matterDao.CheckWithRootByPath(destinationDirPath, user)
destDirMatter = this.matterDao.FindWithRootByPath(destinationDirPath, user)
if destDirMatter == nil {
//throw conflict error
panic(result.CustomWebResult(result.CONFLICT, fmt.Sprintf("%s not exist", destinationDirPath)))
}
return srcMatter, destDirMatter, srcDirPath, destinationDirPath, destinationName, overwrite
@@ -363,16 +528,38 @@ func (this *DavService) HandleMove(writer http.ResponseWriter, request *http.Req
fmt.Printf("MOVE %s\n", subPath)
// handle the lock feature.
reqPath, status, err := this.stripPrefix(request.URL.Path)
if err != nil {
panic(result.StatusCodeWebResult(status, err.Error()))
}
release, status, err := this.confirmLocks(request, reqPath, "")
if err != nil {
panic(result.StatusCodeWebResult(status, err.Error()))
}
if release != nil {
defer release()
}
srcMatter, destDirMatter, srcDirPath, destinationDirPath, destinationName, overwrite := this.prepareMoveCopy(writer, request, user, subPath)
//move to the new directory
if destinationDirPath == srcDirPath {
//if destination path not change. it means rename.
this.matterService.AtomicRename(request, srcMatter, destinationName, user)
this.matterService.AtomicRename(request, srcMatter, destinationName, overwrite, user)
} else {
this.matterService.AtomicMove(request, srcMatter, destDirMatter, overwrite, user)
}
this.logger.Info("finish moving %s => %s", subPath, destDirMatter.Path)
if overwrite {
//overwrite old. set the status code 204
writer.WriteHeader(http.StatusNoContent)
} else {
//copy new. set the status code 201
writer.WriteHeader(http.StatusCreated)
}
}
//copy file/directory
@@ -382,29 +569,245 @@ func (this *DavService) HandleCopy(writer http.ResponseWriter, request *http.Req
srcMatter, destDirMatter, _, _, destinationName, overwrite := this.prepareMoveCopy(writer, request, user, subPath)
// handle the lock feature.
release, status, err := this.confirmLocks(request, destDirMatter.Path+"/"+destinationName, "")
if err != nil {
panic(result.StatusCodeWebResult(status, err.Error()))
}
if release != nil {
defer release()
}
//copy to the new directory
this.matterService.AtomicCopy(request, srcMatter, destDirMatter, destinationName, overwrite, user)
this.logger.Info("finish copying %s => %s", subPath, destDirMatter.Path)
if overwrite {
//overwrite old. set the status code 204
writer.WriteHeader(http.StatusNoContent)
} else {
//copy new. set the status code 201
writer.WriteHeader(http.StatusCreated)
}
}
func (h *DavService) stripPrefix(p string) (string, int, error) {
if r := strings.TrimPrefix(p, WEBDAV_PREFIX); len(r) < len(p) {
return r, http.StatusOK, nil
}
return p, http.StatusNotFound, webdav.ErrPrefixMismatch
}
func (h *DavService) lock(now time.Time, root string) (token string, status int, err error) {
token, err = h.lockSystem.Create(now, webdav.LockDetails{
Root: root,
Duration: webdav.InfiniteTimeout,
ZeroDepth: true,
})
if err != nil {
if err == webdav.ErrLocked {
return "", webdav.StatusLocked, err
}
return "", http.StatusInternalServerError, err
}
return token, 0, nil
}
func (h *DavService) confirmLocks(r *http.Request, src, dst string) (release func(), status int, err error) {
hdr := r.Header.Get("If")
if hdr == "" {
// An empty If header means that the client hasn't previously created locks.
// Even if this client doesn't care about locks, we still need to check that
// the resources aren't locked by another client, so we create temporary
// locks that would conflict with another client's locks. These temporary
// locks are unlocked at the end of the HTTP request.
now, srcToken, dstToken := time.Now(), "", ""
if src != "" {
srcToken, status, err = h.lock(now, src)
if err != nil {
return nil, status, err
}
}
if dst != "" {
dstToken, status, err = h.lock(now, dst)
if err != nil {
if srcToken != "" {
h.lockSystem.Unlock(now, srcToken)
}
return nil, status, err
}
}
return func() {
if dstToken != "" {
h.lockSystem.Unlock(now, dstToken)
}
if srcToken != "" {
h.lockSystem.Unlock(now, srcToken)
}
}, 0, nil
}
ih, ok := webdav.ParseIfHeader(hdr)
if !ok {
return nil, http.StatusBadRequest, webdav.ErrInvalidIfHeader
}
// ih is a disjunction (OR) of ifLists, so any IfList will do.
for _, l := range ih.Lists {
lsrc := l.ResourceTag
if lsrc == "" {
lsrc = src
} else {
u, err := url.Parse(lsrc)
if err != nil {
continue
}
if u.Host != r.Host {
continue
}
lsrc, status, err = h.stripPrefix(u.Path)
if err != nil {
return nil, status, err
}
}
release, err = h.lockSystem.Confirm(time.Now(), lsrc, dst, l.Conditions...)
if err == webdav.ErrConfirmationFailed {
continue
}
if err != nil {
return nil, http.StatusInternalServerError, err
}
return release, 0, nil
}
// Section 10.4.1 says that "If this header is evaluated and all state lists
// fail, then the request must fail with a 412 (Precondition Failed) status."
// We follow the spec even though the cond_put_corrupt_token test case from
// the litmus test warns on seeing a 412 instead of a 423 (Locked).
return nil, http.StatusLocked, webdav.ErrLocked
}
//lock.
func (this *DavService) HandleLock(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
func (this *DavService) HandleLock(w http.ResponseWriter, r *http.Request, user *User, subPath string) {
duration, err := webdav.ParseTimeout(r.Header.Get("Timeout"))
if err != nil {
panic(result.BadRequest(err.Error()))
}
li, status, err := webdav.ReadLockInfo(r.Body)
if err != nil {
panic(result.BadRequest(fmt.Sprintf("error:%s, status=%d", err.Error(), status)))
}
token, ld, now, created := "", webdav.LockDetails{}, time.Now(), false
if li == (webdav.LockInfo{}) {
// An empty LockInfo means to refresh the lock.
ih, ok := webdav.ParseIfHeader(r.Header.Get("If"))
if !ok {
panic(result.BadRequest(webdav.ErrInvalidIfHeader.Error()))
}
if len(ih.Lists) == 1 && len(ih.Lists[0].Conditions) == 1 {
token = ih.Lists[0].Conditions[0].Token
}
if token == "" {
panic(result.BadRequest(webdav.ErrInvalidLockToken.Error()))
}
ld, err = this.lockSystem.Refresh(now, token, duration)
if err != nil {
if err == webdav.ErrNoSuchLock {
panic(result.StatusCodeWebResult(http.StatusPreconditionFailed, err.Error()))
}
panic(result.StatusCodeWebResult(http.StatusInternalServerError, err.Error()))
}
} else {
// Section 9.10.3 says that "If no Depth header is submitted on a LOCK request,
// then the request MUST act as if a "Depth:infinity" had been submitted."
depth := webdav.InfiniteDepth
if hdr := r.Header.Get("Depth"); hdr != "" {
depth = webdav.ParseDepth(hdr)
if depth != 0 && depth != webdav.InfiniteDepth {
// Section 9.10.3 says that "Values other than 0 or infinity must not be
// used with the Depth header on a LOCK method".
panic(result.StatusCodeWebResult(http.StatusBadRequest, webdav.ErrInvalidDepth.Error()))
}
}
reqPath, status, err := this.stripPrefix(r.URL.Path)
if err != nil {
panic(result.StatusCodeWebResult(status, err.Error()))
}
ld = webdav.LockDetails{
Root: reqPath,
Duration: duration,
OwnerXML: li.Owner.InnerXML,
ZeroDepth: depth == 0,
}
token, err = this.lockSystem.Create(now, ld)
if err != nil {
if err == webdav.ErrLocked {
panic(result.StatusCodeWebResult(http.StatusLocked, err.Error()))
}
panic(result.StatusCodeWebResult(http.StatusInternalServerError, err.Error()))
}
defer func() {
//when error occur, rollback.
//this.lockSystem.Unlock(now, token)
}()
// Create the resource if it didn't previously exist.
// ctx := r.Context()
//if _, err := this.FileSystem.Stat(ctx, subPath); err != nil {
// f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
// if err != nil {
// // TODO: detect missing intermediate dirs and return http.StatusConflict?
// return http.StatusInternalServerError, err
// }
// f.Close()
// created = true
//}
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
// Lock-Token value is a Coded-URL. We add angle brackets.
w.Header().Set("Lock-Token", "<"+token+">")
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
if created {
// This is "w.WriteHeader(http.StatusCreated)" and not "return
// http.StatusCreated, nil" because we write our own (XML) response to w
// and Handler.ServeHTTP would otherwise write "Created".
w.WriteHeader(http.StatusCreated)
}
_, _ = webdav.WriteLockInfo(w, token, ld)
panic(result.BadRequest("not support LOCK yet."))
}
//unlock
func (this *DavService) HandleUnlock(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
func (this *DavService) HandleUnlock(w http.ResponseWriter, r *http.Request, user *User, subPath string) {
panic(result.BadRequest("not support UNLOCK yet."))
}
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
// Lock-Token value is a Coded-URL. We strip its angle brackets.
t := r.Header.Get("Lock-Token")
if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' {
panic(result.StatusCodeWebResult(http.StatusBadRequest, webdav.ErrInvalidLockToken.Error()))
}
t = t[1 : len(t)-1]
//change the file's property
func (this *DavService) HandleProppatch(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
panic(result.BadRequest("not support PROPPATCH yet."))
switch err := this.lockSystem.Unlock(time.Now(), t); err {
case nil:
panic(result.StatusCodeWebResult(http.StatusNoContent, ""))
case webdav.ErrForbidden:
panic(result.StatusCodeWebResult(http.StatusForbidden, err.Error()))
case webdav.ErrLocked:
panic(result.StatusCodeWebResult(http.StatusLocked, err.Error()))
case webdav.ErrNoSuchLock:
panic(result.StatusCodeWebResult(http.StatusConflict, err.Error()))
default:
panic(result.StatusCodeWebResult(http.StatusInternalServerError, err.Error()))
}
}
//hanle all the request.
+8 -1
View File
@@ -3,7 +3,7 @@ package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/nu7hatch/gouuid"
"github.com/eyebluecn/tank/code/tool/uuid"
"time"
)
@@ -58,6 +58,13 @@ func (this *DownloadTokenDao) Save(downloadToken *DownloadToken) *DownloadToken
return downloadToken
}
func (this *DownloadTokenDao) DeleteByUserUuid(userUuid string) {
db := core.CONTEXT.GetDB().Where("user_uuid = ?", userUuid).Delete(DownloadToken{})
this.PanicError(db.Error)
}
func (this *DownloadTokenDao) Cleanup() {
this.logger.Info("[DownloadTokenDao] clean up. Delete all DownloadToken")
db := core.CONTEXT.GetDB().Where("uuid is not null").Delete(DownloadToken{})
+5 -7
View File
@@ -1,18 +1,16 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
"time"
)
type DownloadToken struct {
Base
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"`
MatterUuid string `json:"matterUuid" gorm:"type:char(36) not null;index:idx_mu"`
MatterUuid string `json:"matterUuid" gorm:"type:char(36) not null;index:idx_download_token_mu"` //index should unique globally.
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"`
}
func (this *DownloadToken) TableName() string {
return core.TABLE_PREFIX + "download_token"
}
+11 -4
View File
@@ -4,9 +4,9 @@ import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/jinzhu/gorm"
"gorm.io/gorm"
"github.com/nu7hatch/gouuid"
"github.com/eyebluecn/tank/code/tool/uuid"
"time"
)
@@ -48,14 +48,14 @@ func (this *FootprintDao) Page(page int, pageSize int, userUuid string, sortArra
var conditionDB *gorm.DB
conditionDB = core.CONTEXT.GetDB().Model(&Footprint{}).Where(wp.Query, wp.Args...)
count := 0
var count int64 = 0
db := conditionDB.Count(&count)
this.PanicError(db.Error)
var footprints []*Footprint
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&footprints)
this.PanicError(db.Error)
pager := NewPager(page, pageSize, count, footprints)
pager := NewPager(page, pageSize, int(count), footprints)
return pager
}
@@ -137,6 +137,13 @@ func (this *FootprintDao) DeleteByCreateTimeBefore(createTime time.Time) {
this.PanicError(db.Error)
}
func (this *FootprintDao) DeleteByUserUuid(userUuid string) {
db := core.CONTEXT.GetDB().Where("user_uuid = ?", userUuid).Delete(Footprint{})
this.PanicError(db.Error)
}
//System cleanup.
func (this *FootprintDao) Cleanup() {
this.logger.Info("[FootprintDao][DownloadTokenDao] clean up. Delete all Footprint")
+15 -17
View File
@@ -1,22 +1,20 @@
package rest
import "github.com/eyebluecn/tank/code/core"
import "time"
/**
* visit record.
*/
// Footprint /**
// Mysql 5.5 only support one CURRENT_TIMESTAMP
// so we use 2018-01-01 00:00:00 as default, which is the first release date of EyeblueTank
type Footprint struct {
Base
UserUuid string `json:"userUuid" gorm:"type:char(36)"`
Ip string `json:"ip" gorm:"type:varchar(128) not null"`
Host string `json:"host" gorm:"type:varchar(45) not null"`
Uri string `json:"uri" gorm:"type:varchar(255) not null"`
Params string `json:"params" gorm:"type:text"`
Cost int64 `json:"cost" gorm:"type:bigint(20) not null;default:0"`
Success bool `json:"success" gorm:"type:tinyint(1) not null;default:0"`
}
// set File's table name to be `profiles`
func (this *Footprint) TableName() string {
return core.TABLE_PREFIX + "footprint"
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)"`
Ip string `json:"ip" gorm:"type:varchar(128) not null"`
Host string `json:"host" gorm:"type:varchar(45) not null"`
Uri string `json:"uri" gorm:"type:varchar(255) not null"`
Params string `json:"params" gorm:"type:text"`
Cost int64 `json:"cost" gorm:"type:bigint(20) not null;default:0"`
Success bool `json:"success" gorm:"type:tinyint(1) not null;default:0"`
}
+3 -10
View File
@@ -5,7 +5,6 @@ import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/util"
"github.com/robfig/cron"
"net/http"
"time"
)
@@ -94,18 +93,12 @@ func (this *FootprintService) Trace(request *http.Request, duration time.Duratio
func (this *FootprintService) Bootstrap() {
this.logger.Info("[cron job] Every day 00:10 delete Footprint data 8 days ago.")
expression := "0 10 0 * * ?"
cronJob := cron.New()
err := cronJob.AddFunc(expression, this.cleanOldData)
core.PanicError(err)
cronJob.Start()
go core.RunWithRecovery(this.cleanOldData)
this.logger.Info("Immediately delete Footprint data of 8 days ago.")
go core.RunWithRecovery(this.CleanOldData)
}
func (this *FootprintService) cleanOldData() {
func (this *FootprintService) CleanOldData() {
day8Ago := time.Now()
day8Ago = day8Ago.AddDate(0, 0, -8)
+10 -5
View File
@@ -6,8 +6,8 @@ import (
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
"github.com/jinzhu/gorm"
"github.com/nu7hatch/gouuid"
"github.com/eyebluecn/tank/code/tool/uuid"
"gorm.io/gorm"
"os"
"path/filepath"
"time"
@@ -67,7 +67,7 @@ func (this *ImageCacheDao) CheckByUuidAndUserUuid(uuid string, userUuid string)
// Read
var imageCache = &ImageCache{}
db := core.CONTEXT.GetDB().Where(&ImageCache{Base: Base{Uuid: uuid}, UserUuid: userUuid}).First(imageCache)
db := core.CONTEXT.GetDB().Where(&ImageCache{Uuid: uuid, UserUuid: userUuid}).First(imageCache)
this.PanicError(db.Error)
return imageCache
@@ -101,14 +101,14 @@ func (this *ImageCacheDao) Page(page int, pageSize int, userUuid string, matterU
var conditionDB *gorm.DB
conditionDB = core.CONTEXT.GetDB().Model(&ImageCache{}).Where(wp.Query, wp.Args...)
count := 0
var count int64 = 0
db := conditionDB.Count(&count)
this.PanicError(db.Error)
var imageCaches []*ImageCache
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&imageCaches)
this.PanicError(db.Error)
pager := NewPager(page, pageSize, count, imageCaches)
pager := NewPager(page, pageSize, int(count), imageCaches)
return pager
}
@@ -184,6 +184,11 @@ func (this *ImageCacheDao) DeleteByMatterUuid(matterUuid string) {
}
func (this *ImageCacheDao) DeleteByUserUuid(userUuid string) {
db := core.CONTEXT.GetDB().Where("user_uuid = ?", userUuid).Delete(ImageCache{})
this.PanicError(db.Error)
}
func (this *ImageCacheDao) SizeBetweenTime(startTime time.Time, endTime time.Time) int64 {
var wp = &builder.WherePair{Query: "create_time >= ? AND create_time <= ?", Args: []interface{}{startTime, endTime}}
+15 -19
View File
@@ -1,29 +1,25 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
)
import "time"
/**
* image cache.
*/
type ImageCache struct {
Base
Name string `json:"name" gorm:"type:varchar(255) not null"`
UserUuid string `json:"userUuid" gorm:"type:char(36)"`
Username string `json:"username" gorm:"type:varchar(45) not null"`
MatterUuid string `json:"matterUuid" gorm:"type:char(36);index:idx_mu"`
MatterName string `json:"matterName" gorm:"type:varchar(255) not null"`
Mode string `json:"mode" gorm:"type:varchar(512)"`
Md5 string `json:"md5" gorm:"type:varchar(45)"`
Size int64 `json:"size" gorm:"type:bigint(20) not null;default:0"`
Path string `json:"path" gorm:"type:varchar(512)"`
Matter *Matter `json:"matter" gorm:"-"`
}
// set File's table name to be `profiles`
func (this *ImageCache) TableName() string {
return core.TABLE_PREFIX + "image_cache"
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(255) not null"`
UserUuid string `json:"userUuid" gorm:"type:char(36)"`
Username string `json:"username" gorm:"type:varchar(45) not null"`
MatterUuid string `json:"matterUuid" gorm:"type:char(36);index:idx_image_cache_mu"` //index should unique globally.
MatterName string `json:"matterName" gorm:"type:varchar(255) not null"`
Mode string `json:"mode" gorm:"type:varchar(512)"`
Md5 string `json:"md5" gorm:"type:varchar(45)"`
Size int64 `json:"size" gorm:"type:bigint(20) not null;default:0"`
Path string `json:"path" gorm:"type:varchar(512)"`
Matter *Matter `json:"matter" gorm:"-"`
}
// get the absolute path. path in db means relative path.
+118 -36
View File
@@ -6,12 +6,20 @@ import (
"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/third"
"github.com/eyebluecn/tank/code/tool/util"
"github.com/jinzhu/gorm"
"github.com/nu7hatch/gouuid"
"github.com/eyebluecn/tank/code/tool/uuid"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"net/http"
"os"
"regexp"
"strconv"
"sync"
"time"
)
@@ -24,7 +32,7 @@ type InstallController struct {
matterService *MatterService
imageCacheDao *ImageCacheDao
imageCacheService *ImageCacheService
tableNames []IBase
tableNames []interface{}
}
func (this *InstallController) Init() {
@@ -60,7 +68,7 @@ func (this *InstallController) Init() {
this.imageCacheService = c
}
this.tableNames = []IBase{
this.tableNames = []interface{}{
&Dashboard{},
&Bridge{},
&DownloadToken{},
@@ -92,11 +100,13 @@ func (this *InstallController) RegisterRoutes() map[string]func(writer http.Resp
}
func (this *InstallController) openDbConnection(writer http.ResponseWriter, request *http.Request) *gorm.DB {
dbType := request.FormValue("dbType")
mysqlPortStr := request.FormValue("mysqlPort")
mysqlHost := request.FormValue("mysqlHost")
mysqlSchema := request.FormValue("mysqlSchema")
mysqlUsername := request.FormValue("mysqlUsername")
mysqlPassword := request.FormValue("mysqlPassword")
mysqlCharset := request.FormValue("mysqlCharset")
var mysqlPort int
if mysqlPortStr != "" {
@@ -105,55 +115,114 @@ func (this *InstallController) openDbConnection(writer http.ResponseWriter, requ
mysqlPort = tmp
}
mysqlUrl := util.GetMysqlUrl(mysqlPort, mysqlHost, mysqlSchema, mysqlUsername, mysqlPassword)
//log config
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
},
)
//table name strategy
namingStrategy := core.CONFIG.NamingStrategy()
this.logger.Info("Connect MySQL %s", mysqlUrl)
if dbType == "sqlite" {
var err error = nil
db, err := gorm.Open("mysql", mysqlUrl)
this.PanicError(err)
var err error = nil
sqliteFolder := core.CONFIG.SqliteFolder() + "/tank.sqlite"
this.logger.Info("Connect Sqlite %s", sqliteFolder)
db, err := gorm.Open(sqlite.Open(sqliteFolder), &gorm.Config{Logger: dbLogger, NamingStrategy: namingStrategy})
db.LogMode(false)
if err != nil {
core.LOGGER.Panic("failed to connect sqlite database")
}
return db
//sqlite lock issue. https://gist.github.com/mrnugget/0eda3b2b53a70fa4a894
phyDb, err := db.DB()
phyDb.SetMaxOpenConns(1)
return db
} else {
mysqlUrl := util.GetMysqlUrl(mysqlPort, mysqlHost, mysqlSchema, mysqlUsername, mysqlPassword, mysqlCharset)
this.logger.Info("Connect MySQL %s", mysqlUrl)
var err error = nil
db, err := gorm.Open(mysql.Open(mysqlUrl), &gorm.Config{Logger: dbLogger, NamingStrategy: namingStrategy})
this.PanicError(err)
return db
}
}
func (this *InstallController) closeDbConnection(db *gorm.DB) {
if db != nil {
err := db.Close()
sqlDB, err := db.DB()
if err != nil {
this.logger.Error("occur error when close db. %v", err)
core.LOGGER.Error("occur error when get *sql.DB %s", err.Error())
}
err = sqlDB.Close()
if err != nil {
core.LOGGER.Error("occur error when closing db %s", err.Error())
}
}
}
func (this *InstallController) getTableMeta(gormDb *gorm.DB, entity IBase) (bool, []*gorm.StructField, []*gorm.StructField) {
// (tableName, exists, allFields, missingFields)
func (this *InstallController) getTableMeta(gormDb *gorm.DB, entity interface{}) (string, bool, []*InstallFieldInfo, []*InstallFieldInfo) {
db := gormDb.Unscoped()
scope := db.NewScope(entity)
//get all useful fields from model.
entitySchema, err := schema.Parse(entity, &sync.Map{}, core.CONFIG.NamingStrategy())
this.PanicError(err)
tableName := scope.TableName()
modelStruct := scope.GetModelStruct()
allFields := modelStruct.StructFields
var missingFields = make([]*gorm.StructField, 0)
tableName := entitySchema.Table
if !scope.Dialect().HasTable(tableName) {
var allFields = make([]*InstallFieldInfo, 0)
for _, field := range entitySchema.Fields {
allFields = append(allFields, &InstallFieldInfo{
Name: field.DBName,
DataType: string(field.DataType),
})
}
var missingFields = make([]*InstallFieldInfo, 0)
if !gormDb.Migrator().HasTable(tableName) {
missingFields = append(missingFields, allFields...)
return false, allFields, missingFields
return tableName, false, allFields, missingFields
} else {
for _, field := range allFields {
if !scope.Dialect().HasColumn(tableName, field.DBName) {
if field.IsNormal {
missingFields = append(missingFields, field)
//tag with `gorm:"-"` will be ""
if field.Name != "" {
database := gormDb.Migrator().CurrentDatabase()
//if sqlite
if _, ok := gormDb.Dialector.(*sqlite.Dialector); ok {
if !gormDb.Migrator().HasColumn(tableName, field.Name) {
missingFields = append(missingFields, field)
}
} else {
if !third.MysqlMigratorHasColumn(gormDb, database, tableName, field.Name) {
missingFields = append(missingFields, field)
}
}
}
}
return true, allFields, missingFields
return tableName, true, allFields, missingFields
}
}
@@ -163,9 +232,10 @@ func (this *InstallController) getTableMetaList(db *gorm.DB) []*InstallTableInfo
var installTableInfos []*InstallTableInfo
for _, iBase := range this.tableNames {
exist, allFields, missingFields := this.getTableMeta(db, iBase)
tableName, exist, allFields, missingFields := this.getTableMeta(db, iBase)
installTableInfos = append(installTableInfos, &InstallTableInfo{
Name: iBase.TableName(),
Name: tableName,
TableExist: exist,
AllFields: allFields,
MissingFields: missingFields,
@@ -184,7 +254,7 @@ func (this *InstallController) validateTableMetaList(tableInfoList []*InstallTab
var strs []string
for _, v := range tableInfo.MissingFields {
strs = append(strs, v.DBName)
strs = append(strs, v.Name)
}
panic(result.BadRequest(fmt.Sprintf("table %s miss the following fields %v", tableInfo.Name, strs)))
@@ -203,7 +273,9 @@ func (this *InstallController) Verify(writer http.ResponseWriter, request *http.
defer this.closeDbConnection(db)
this.logger.Info("Ping DB")
err := db.DB().Ping()
phyDb, err := db.DB()
this.PanicError(err)
err = phyDb.Ping()
this.PanicError(err)
return this.Success("OK")
@@ -226,13 +298,21 @@ func (this *InstallController) CreateTable(writer http.ResponseWriter, request *
for _, iBase := range this.tableNames {
//complete the missing fields or create table. use utf8 charset
db1 := db.Set("gorm:table_options", "CHARSET=utf8").AutoMigrate(iBase)
this.PanicError(db1.Error)
if _, ok := db.Dialector.(*sqlite.Dialector); ok {
//if sqlite. no need to set CHARSET.
err := db.AutoMigrate(iBase)
this.PanicError(err)
} else {
//use utf8 charset
mysqlCharset := request.FormValue("mysqlCharset")
err := db.Set("gorm:table_options", fmt.Sprintf("CHARSET=%s", mysqlCharset)).AutoMigrate(iBase)
this.PanicError(err)
}
exist, allFields, missingFields := this.getTableMeta(db, iBase)
//complete the missing fields or create table.
tableName, exist, allFields, missingFields := this.getTableMeta(db, iBase)
installTableInfos = append(installTableInfos, &InstallTableInfo{
Name: iBase.TableName(),
Name: tableName,
TableExist: exist,
AllFields: allFields,
MissingFields: missingFields,
@@ -345,11 +425,13 @@ func (this *InstallController) ValidateAdmin(writer http.ResponseWriter, request
//Finish the installation
func (this *InstallController) Finish(writer http.ResponseWriter, request *http.Request) *result.WebResult {
dbType := request.FormValue("dbType")
mysqlPortStr := request.FormValue("mysqlPort")
mysqlHost := request.FormValue("mysqlHost")
mysqlSchema := request.FormValue("mysqlSchema")
mysqlUsername := request.FormValue("mysqlUsername")
mysqlPassword := request.FormValue("mysqlPassword")
mysqlCharset := request.FormValue("mysqlCharset")
var mysqlPort int
if mysqlPortStr != "" {
@@ -375,7 +457,7 @@ func (this *InstallController) Finish(writer http.ResponseWriter, request *http.
}
//announce the config to write config to tank.json
core.CONFIG.FinishInstall(mysqlPort, mysqlHost, mysqlSchema, mysqlUsername, mysqlPassword)
core.CONFIG.FinishInstall(dbType, mysqlPort, mysqlHost, mysqlSchema, mysqlUsername, mysqlPassword, mysqlCharset)
//announce the context to broadcast the installation news to bean.
core.CONTEXT.InstallOk()
+11 -6
View File
@@ -1,13 +1,18 @@
package rest
import "github.com/jinzhu/gorm"
// InstallTableInfo /**
// table meta info.
type InstallTableInfo struct {
Name string `json:"name"`
TableExist bool `json:"tableExist"`
AllFields []*InstallFieldInfo `json:"allFields"`
MissingFields []*InstallFieldInfo `json:"missingFields"`
}
/**
* table meta info.
*/
type InstallTableInfo struct {
Name string `json:"name"`
TableExist bool `json:"tableExist"`
AllFields []*gorm.StructField `json:"allFields"`
MissingFields []*gorm.StructField `json:"missingFields"`
type InstallFieldInfo struct {
Name string `json:"name"`
DataType string `json:"dataType"`
}
+156 -7
View File
@@ -14,6 +14,7 @@ type MatterController struct {
BaseController
matterDao *MatterDao
matterService *MatterService
preferenceService *PreferenceService
downloadTokenDao *DownloadTokenDao
imageCacheDao *ImageCacheDao
shareDao *ShareDao
@@ -73,8 +74,13 @@ func (this *MatterController) RegisterRoutes() map[string]func(writer http.Respo
routeMap["/api/matter/create/directory"] = this.Wrap(this.CreateDirectory, USER_ROLE_USER)
routeMap["/api/matter/upload"] = this.Wrap(this.Upload, USER_ROLE_USER)
routeMap["/api/matter/crawl"] = this.Wrap(this.Crawl, USER_ROLE_USER)
routeMap["/api/matter/soft/delete"] = this.Wrap(this.SoftDelete, USER_ROLE_USER)
routeMap["/api/matter/soft/delete/batch"] = this.Wrap(this.SoftDeleteBatch, USER_ROLE_USER)
routeMap["/api/matter/recovery"] = this.Wrap(this.Recovery, USER_ROLE_USER)
routeMap["/api/matter/recovery/batch"] = this.Wrap(this.RecoveryBatch, USER_ROLE_USER)
routeMap["/api/matter/delete"] = this.Wrap(this.Delete, USER_ROLE_USER)
routeMap["/api/matter/delete/batch"] = this.Wrap(this.DeleteBatch, USER_ROLE_USER)
routeMap["/api/matter/clean/expired/deleted/matters"] = this.Wrap(this.CleanExpiredDeletedMatters, USER_ROLE_ADMINISTRATOR)
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)
@@ -112,12 +118,14 @@ func (this *MatterController) Page(writer http.ResponseWriter, request *http.Req
pageSizeStr := request.FormValue("pageSize")
orderCreateTime := request.FormValue("orderCreateTime")
orderUpdateTime := request.FormValue("orderUpdateTime")
orderDeleteTime := request.FormValue("orderDeleteTime")
orderSort := request.FormValue("orderSort")
orderTimes := request.FormValue("orderTimes")
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")
@@ -143,7 +151,7 @@ func (this *MatterController) Page(writer http.ResponseWriter, request *http.Req
user := this.findUser(request)
this.shareService.ValidateMatter(request, shareUuid, shareCode, user, shareRootUuid, dirMatter)
userUuid = dirMatter.Uuid
puuid = dirMatter.Uuid
} else {
//if cannot auth by share. Then login is required.
@@ -183,6 +191,10 @@ func (this *MatterController) Page(writer http.ResponseWriter, request *http.Req
Key: "update_time",
Value: orderUpdateTime,
},
{
Key: "delete_time",
Value: orderDeleteTime,
},
{
Key: "sort",
Value: orderSort,
@@ -201,7 +213,7 @@ func (this *MatterController) Page(writer http.ResponseWriter, request *http.Req
},
}
pager := this.matterDao.Page(page, pageSize, puuid, userUuid, name, dir, extensions, sortArray)
pager := this.matterDao.Page(page, pageSize, puuid, userUuid, name, dir, deleted, extensions, sortArray)
return this.Success(pager)
}
@@ -280,6 +292,112 @@ func (this *MatterController) Crawl(writer http.ResponseWriter, request *http.Re
return this.Success(matter)
}
//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)
user := this.checkUser(request)
if matter.UserUuid != user.Uuid {
panic(result.UNAUTHORIZED)
}
this.matterService.AtomicSoftDelete(request, matter, user)
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"))
}
user := this.checkUser(request)
uuidArray := strings.Split(uuids, ",")
matters := make([]*Matter, 0)
for _, uuid := range uuidArray {
matter := this.matterDao.FindByUuid(uuid)
if matter == nil {
this.logger.Warn("%s not exist anymore", uuid)
continue
}
if matter.UserUuid != user.Uuid {
panic(result.UNAUTHORIZED)
}
matters = append(matters, matter)
}
for _, matter := range matters {
this.matterService.AtomicSoftDelete(request, matter, user)
}
return this.Success("OK")
}
//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"))
}
matter := this.matterDao.CheckByUuid(uuid)
user := this.checkUser(request)
if matter.UserUuid != user.Uuid {
panic(result.UNAUTHORIZED)
}
this.matterService.AtomicRecovery(request, matter, user)
return this.Success("OK")
}
//recovery batch.
func (this *MatterController) RecoveryBatch(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuids := request.FormValue("uuids")
if uuids == "" {
panic(result.BadRequest("uuids cannot be null"))
}
uuidArray := strings.Split(uuids, ",")
for _, uuid := range uuidArray {
matter := this.matterDao.FindByUuid(uuid)
if matter == nil {
this.logger.Warn("%s not exist anymore", uuid)
continue
}
user := this.checkUser(request)
if matter.UserUuid != user.Uuid {
panic(result.UNAUTHORIZED)
}
this.matterService.AtomicRecovery(request, matter, user)
}
return this.Success("OK")
}
//complete delete.
func (this *MatterController) Delete(writer http.ResponseWriter, request *http.Request) *result.WebResult {
uuid := request.FormValue("uuid")
@@ -307,7 +425,8 @@ func (this *MatterController) DeleteBatch(writer http.ResponseWriter, request *h
}
uuidArray := strings.Split(uuids, ",")
user := this.checkUser(request)
matters := make([]*Matter, 0)
for _, uuid := range uuidArray {
matter := this.matterDao.FindByUuid(uuid)
@@ -317,15 +436,26 @@ func (this *MatterController) DeleteBatch(writer http.ResponseWriter, request *h
continue
}
user := this.checkUser(request)
if matter.UserUuid != user.Uuid {
panic(result.UNAUTHORIZED)
}
this.matterService.AtomicDelete(request, matter, user)
matters = append(matters, matter)
}
for _, matter := range matters {
this.matterService.AtomicDelete(request, matter, user)
}
return this.Success("OK")
}
//manual clean expired deleted matters.
func (this *MatterController) CleanExpiredDeletedMatters(writer http.ResponseWriter, request *http.Request) *result.WebResult {
this.matterService.CleanExpiredDeletedMatters()
return this.Success("OK")
}
@@ -342,7 +472,7 @@ func (this *MatterController) Rename(writer http.ResponseWriter, request *http.R
panic(result.UNAUTHORIZED)
}
this.matterService.AtomicRename(request, matter, name, user)
this.matterService.AtomicRename(request, matter, name, false, user)
return this.Success(matter)
}
@@ -357,6 +487,10 @@ func (this *MatterController) ChangePrivacy(writer http.ResponseWriter, request
matter := this.matterDao.CheckByUuid(uuid)
if matter.Deleted {
panic(result.BadRequest("matter has been deleted. Cannot change privacy."))
}
if matter.Privacy == privacy {
panic(result.BadRequest("not changed. Invalid operation."))
}
@@ -395,6 +529,10 @@ func (this *MatterController) Move(writer http.ResponseWriter, request *http.Req
panic(result.UNAUTHORIZED)
}
if destMatter.Deleted {
panic(result.BadRequest("dest matter has been deleted. Cannot move."))
}
var srcMatters []*Matter
for _, uuid := range srcUuids {
srcMatter := this.matterDao.CheckByUuid(uuid)
@@ -403,6 +541,10 @@ func (this *MatterController) Move(writer http.ResponseWriter, request *http.Req
panic(result.BadRequest("no move, invalid operation"))
}
if srcMatter.Deleted {
panic(result.BadRequest("src matter has been deleted. Cannot move."))
}
//check whether there are files with the same name.
count := this.matterDao.CountByUserUuidAndPuuidAndDirAndName(user.Uuid, destMatter.Uuid, srcMatter.Dir, srcMatter.Name)
@@ -461,6 +603,13 @@ func (this *MatterController) Zip(writer http.ResponseWriter, request *http.Requ
if matters == nil || len(matters) == 0 {
panic(result.BadRequest("matters cannot be nil."))
}
for _, matter := range matters {
if matter.Deleted {
panic(result.BadRequest("matter has been deleted. Cannot download batch."))
}
}
user := this.checkUser(request)
puuid := matters[0].Puuid
+119 -33
View File
@@ -5,8 +5,9 @@ import (
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
"github.com/jinzhu/gorm"
"github.com/nu7hatch/gouuid"
"github.com/eyebluecn/tank/code/tool/uuid"
"gorm.io/gorm"
"math"
"os"
"time"
)
@@ -92,29 +93,19 @@ func (this *MatterDao) CheckWithRootByPath(path string, user *User) *Matter {
return matter
}
func (this *MatterDao) FindByUserUuidAndPuuidAndNameAndDirTrue(userUuid string, puuid string, name string) *Matter {
// find by path. if path=/, then return the Root Matter
func (this *MatterDao) FindWithRootByPath(path string, user *User) *Matter {
var wp = &builder.WherePair{}
var matter *Matter
if userUuid != "" {
wp = wp.And(&builder.WherePair{Query: "user_uuid = ?", Args: []interface{}{userUuid}})
if user == nil {
panic(result.BadRequest("user cannot be null."))
}
if puuid != "" {
wp = wp.And(&builder.WherePair{Query: "puuid = ?", Args: []interface{}{puuid}})
}
if name != "" {
wp = wp.And(&builder.WherePair{Query: "name = ?", Args: []interface{}{name}})
}
wp = wp.And(&builder.WherePair{Query: "dir = ?", Args: []interface{}{1}})
var matter = &Matter{}
db := core.CONTEXT.GetDB().Model(&Matter{}).Where(wp.Query, wp.Args...).First(matter)
if db.Error != nil {
return nil
if path == "" || path == "/" {
matter = NewRootMatter(user)
} else {
matter = this.findByUserUuidAndPath(user.Uuid, path)
}
return matter
@@ -147,7 +138,7 @@ func (this *MatterDao) FindByUserUuidAndPuuidAndDirTrue(userUuid string, puuid s
func (this *MatterDao) CheckByUuidAndUserUuid(uuid string, userUuid string) *Matter {
var matter = &Matter{}
db := core.CONTEXT.GetDB().Where(&Matter{Base: Base{Uuid: uuid}, UserUuid: userUuid}).First(matter)
db := core.CONTEXT.GetDB().Where(&Matter{Uuid: uuid, UserUuid: userUuid}).First(matter)
this.PanicError(db.Error)
return matter
@@ -157,7 +148,7 @@ func (this *MatterDao) CheckByUuidAndUserUuid(uuid string, userUuid string) *Mat
func (this *MatterDao) CountByUserUuidAndPuuidAndDirAndName(userUuid string, puuid string, dir bool, name string) int {
var matter Matter
var count int
var count int64
var wp = &builder.WherePair{}
@@ -181,10 +172,10 @@ func (this *MatterDao) CountByUserUuidAndPuuidAndDirAndName(userUuid string, puu
Count(&count)
this.PanicError(db.Error)
return count
return int(count)
}
func (this *MatterDao) FindByUserUuidAndPuuidAndDirAndName(userUuid string, puuid string, dir bool, name string) *Matter {
func (this *MatterDao) FindByUserUuidAndPuuidAndDirAndName(userUuid string, puuid string, dir string, name string) *Matter {
var matter = &Matter{}
@@ -202,7 +193,11 @@ func (this *MatterDao) FindByUserUuidAndPuuidAndDirAndName(userUuid string, puui
wp = wp.And(&builder.WherePair{Query: "name = ?", Args: []interface{}{name}})
}
wp = wp.And(&builder.WherePair{Query: "dir = ?", Args: []interface{}{dir}})
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)
@@ -218,10 +213,19 @@ func (this *MatterDao) FindByUserUuidAndPuuidAndDirAndName(userUuid string, puui
}
func (this *MatterDao) FindByPuuidAndUserUuid(puuid string, userUuid string, sortArray []builder.OrderPair) []*Matter {
return this.FindByPuuidAndUserUuidAndDeleted(puuid, userUuid, "", sortArray)
}
func (this *MatterDao) FindByPuuidAndUserUuidAndDeleted(puuid string, userUuid string, deleted string, sortArray []builder.OrderPair) []*Matter {
var matters []*Matter
var wp = &builder.WherePair{}
wp = wp.And(&builder.WherePair{Query: "puuid = ? AND user_uuid = ?", Args: []interface{}{puuid, userUuid}})
if deleted == TRUE {
wp = wp.And(&builder.WherePair{Query: "deleted = 1", Args: []interface{}{}})
} else if deleted == FALSE {
wp = wp.And(&builder.WherePair{Query: "deleted = 0", Args: []interface{}{}})
}
if sortArray == nil {
@@ -251,7 +255,17 @@ func (this *MatterDao) FindByUuids(uuids []string, sortArray []builder.OrderPair
return matters
}
func (this *MatterDao) PlainPage(page int, pageSize int, puuid string, userUuid string, name string, dir string, extensions []string, sortArray []builder.OrderPair) (int, []*Matter) {
func (this *MatterDao) PlainPage(
page int,
pageSize int,
puuid string,
userUuid string,
name string,
dir string,
deleted string,
deleteTimeBefore *time.Time,
extensions []string,
sortArray []builder.OrderPair) (int, []*Matter) {
var wp = &builder.WherePair{}
@@ -267,12 +281,22 @@ func (this *MatterDao) PlainPage(page int, pageSize int, puuid string, userUuid
wp = wp.And(&builder.WherePair{Query: "name LIKE ?", Args: []interface{}{"%" + name + "%"}})
}
if deleteTimeBefore != nil {
wp = wp.And(&builder.WherePair{Query: "delete_time < ?", Args: []interface{}{&deleteTimeBefore}})
}
if dir == TRUE {
wp = wp.And(&builder.WherePair{Query: "dir = ?", Args: []interface{}{1}})
} else if dir == FALSE {
wp = wp.And(&builder.WherePair{Query: "dir = ?", Args: []interface{}{0}})
}
if deleted == TRUE {
wp = wp.And(&builder.WherePair{Query: "deleted = ?", Args: []interface{}{1}})
} else if deleted == FALSE {
wp = wp.And(&builder.WherePair{Query: "deleted = ?", Args: []interface{}{0}})
}
var conditionDB *gorm.DB
if extensions != nil && len(extensions) > 0 {
var orWp = &builder.WherePair{}
@@ -286,7 +310,7 @@ func (this *MatterDao) PlainPage(page int, pageSize int, puuid string, userUuid
conditionDB = core.CONTEXT.GetDB().Model(&Matter{}).Where(wp.Query, wp.Args...)
}
count := 0
var count int64 = 0
db := conditionDB.Count(&count)
this.PanicError(db.Error)
@@ -294,16 +318,51 @@ func (this *MatterDao) PlainPage(page int, pageSize int, puuid string, userUuid
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&matters)
this.PanicError(db.Error)
return count, matters
return int(count), matters
}
func (this *MatterDao) Page(page int, pageSize int, puuid string, userUuid string, name string, dir string, extensions []string, sortArray []builder.OrderPair) *Pager {
func (this *MatterDao) Page(page int, pageSize int, puuid string, userUuid string, name string, dir string, deleted string, extensions []string, sortArray []builder.OrderPair) *Pager {
count, matters := this.PlainPage(page, pageSize, puuid, userUuid, name, dir, extensions, sortArray)
count, matters := this.PlainPage(page, pageSize, puuid, userUuid, name, dir, deleted, nil, extensions, sortArray)
pager := NewPager(page, pageSize, count, matters)
return pager
}
//handle matter page by page.
func (this *MatterDao) PageHandle(
puuid string,
userUuid string,
name string,
dir string,
deleted string,
deleteTimeBefore *time.Time,
sortArray []builder.OrderPair,
fun func(matter *Matter)) {
pageSize := 1000
if sortArray == nil || len(sortArray) == 0 {
sortArray = []builder.OrderPair{
{
Key: "uuid",
Value: DIRECTION_ASC,
},
}
}
count, _ := this.PlainPage(0, pageSize, puuid, userUuid, 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)
for _, matter := range matters {
fun(matter)
}
}
}
}
func (this *MatterDao) Create(matter *Matter) *Matter {
timeUUID, _ := uuid.NewV4()
@@ -328,7 +387,7 @@ func (this *MatterDao) Save(matter *Matter) *Matter {
//download time add 1
func (this *MatterDao) TimesIncrement(matterUuid string) {
db := core.CONTEXT.GetDB().Model(&Matter{}).Where("uuid = ?", matterUuid).Update("times", gorm.Expr("times + 1"))
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)
}
@@ -391,6 +450,31 @@ func (this *MatterDao) Delete(matter *Matter) {
}
}
//soft delete a file or dir
func (this *MatterDao) SoftDelete(matter *Matter) {
//soft delete from db.
db := core.CONTEXT.GetDB().Model(&Matter{}).Where("uuid = ?", matter.Uuid).Updates(map[string]interface{}{"deleted": true, "delete_time": time.Now()})
this.PanicError(db.Error)
}
//recovery a file
func (this *MatterDao) Recovery(matter *Matter) {
//recovery from db.
db := core.CONTEXT.GetDB().Model(&Matter{}).Where("uuid = ?", matter.Uuid).Updates(map[string]interface{}{"deleted": false, "delete_time": time.Now()})
this.PanicError(db.Error)
}
func (this *MatterDao) DeleteByUserUuid(userUuid string) {
db := core.CONTEXT.GetDB().Where("user_uuid = ?", userUuid).Delete(Matter{})
this.PanicError(db.Error)
}
func (this *MatterDao) CountBetweenTime(startTime time.Time, endTime time.Time) int64 {
var count int64
db := core.CONTEXT.GetDB().Model(&Matter{}).Where("create_time >= ? AND create_time <= ?", startTime, endTime).Count(&count)
@@ -400,7 +484,7 @@ func (this *MatterDao) CountBetweenTime(startTime time.Time, endTime time.Time)
func (this *MatterDao) SizeBetweenTime(startTime time.Time, endTime time.Time) int64 {
var wp = &builder.WherePair{Query: "create_time >= ? AND create_time <= ?", Args: []interface{}{startTime, endTime}}
var wp = &builder.WherePair{Query: "dir = 0 AND create_time >= ? AND create_time <= ?", Args: []interface{}{startTime, endTime}}
var count int64
db := core.CONTEXT.GetDB().Model(&Matter{}).Where(wp.Query, wp.Args...).Count(&count)
@@ -417,6 +501,7 @@ func (this *MatterDao) SizeBetweenTime(startTime time.Time, endTime time.Time) i
return size
}
//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}}
@@ -435,6 +520,7 @@ func (this *MatterDao) findByUserUuidAndPath(userUuid string, path string) *Matt
return matter
}
//find by userUuid and path. if not found, panic
func (this *MatterDao) checkByUserUuidAndPath(userUuid string, path string) *Matter {
if path == "" {
+59 -18
View File
@@ -6,9 +6,11 @@ import (
"github.com/eyebluecn/tank/code/tool/i18n"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/util"
jsoniter "github.com/json-iterator/go"
"net/http"
"regexp"
"strings"
"time"
)
const (
@@ -28,24 +30,26 @@ const (
* file is too common. so we use matter as file.
*/
type Matter struct {
Base
Puuid string `json:"puuid" gorm:"type:char(36);index:idx_puuid"`
UserUuid string `json:"userUuid" gorm:"type:char(36);index:idx_uu"`
Username string `json:"username" 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)"`
Size int64 `json:"size" gorm:"type:bigint(20) not null;default:0"`
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:"-"`
}
// set File's table name to be `profiles`
func (Matter) TableName() string {
return core.TABLE_PREFIX + "matter"
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'"`
Puuid string `json:"puuid" gorm:"type:char(36);index:idx_matter_puuid"` //index should unique globally.
UserUuid string `json:"userUuid" gorm:"type:char(36);index:idx_matter_uu"`
Username string `json:"username" 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)"`
Size int64 `json:"size" gorm:"type:bigint(20) not null;default:0"`
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'"`
}
// get matter's absolute path. the Path property is relative path in db.
@@ -67,10 +71,19 @@ func NewRootMatter(user *User) *Matter {
matter.Path = ""
matter.CreateTime = user.CreateTime
matter.UpdateTime = user.UpdateTime
matter.VisitTime = user.UpdateTime
return matter
}
//get user's space absolute path
func GetUserSpaceRootDir(username string) (rootDirPath string) {
rootDirPath = fmt.Sprintf("%s/%s", core.CONFIG.MatterPath(), username)
return rootDirPath
}
//get user's root absolute path
func GetUserMatterRootDir(username string) (rootDirPath string) {
@@ -113,3 +126,31 @@ func CheckMatterName(request *http.Request, name string) string {
}
return name
}
//fetch the props
func (this *Matter) FetchPropMap() map[string]string {
m := make(map[string]string)
json := this.Prop
if json == "" {
json = EMPTY_JSON_MAP
}
err := jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal([]byte(json), &m)
if err != nil {
panic(err)
}
return m
}
//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)
}
+356 -52
View File
@@ -4,6 +4,7 @@ import (
"archive/zip"
"fmt"
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/download"
"github.com/eyebluecn/tank/code/tool/i18n"
"github.com/eyebluecn/tank/code/tool/result"
@@ -13,6 +14,7 @@ import (
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
@@ -254,6 +256,36 @@ func (this *MatterService) Delete(request *http.Request, matter *Matter, user *U
this.ComputeRouteSize(matter.Puuid, user)
}
//soft delete files.
func (this *MatterService) SoftDelete(request *http.Request, matter *Matter, user *User) {
if matter == nil {
panic(result.BadRequest("matter cannot be nil"))
}
if matter.Deleted {
panic(result.BadRequest("matter has been deleted"))
}
this.matterDao.SoftDelete(matter)
//no need to recompute size.
}
//recovery delete files.
func (this *MatterService) Recovery(request *http.Request, matter *Matter, user *User) {
if matter == nil {
panic(result.BadRequest("matter cannot be nil"))
}
if !matter.Deleted {
panic(result.BadRequest("matter has not been deleted"))
}
this.matterDao.Recovery(matter)
//no need to recompute size.
}
//atomic delete files
func (this *MatterService) AtomicDelete(request *http.Request, matter *Matter, user *User) {
@@ -268,6 +300,49 @@ func (this *MatterService) AtomicDelete(request *http.Request, matter *Matter, u
this.Delete(request, matter, user)
}
//atomic soft delete files
func (this *MatterService) AtomicSoftDelete(request *http.Request, matter *Matter, user *User) {
if matter == nil {
panic(result.BadRequest("matter cannot be nil"))
}
if matter.Deleted {
panic(result.BadRequest("matter has been deleted"))
}
//lock
this.userService.MatterLock(matter.UserUuid)
defer this.userService.MatterUnlock(matter.UserUuid)
//if disabled the recycle feature. then we hard delete.
preference := this.preferenceService.Fetch()
if preference.DeletedKeepDays == 0 {
this.Delete(request, matter, user)
} else {
this.SoftDelete(request, matter, user)
}
}
//atomic recovery delete files
func (this *MatterService) AtomicRecovery(request *http.Request, matter *Matter, user *User) {
if matter == nil {
panic(result.BadRequest("matter cannot be nil"))
}
if !matter.Deleted {
panic(result.BadRequest("matter has not been deleted"))
}
//lock
this.userService.MatterLock(matter.UserUuid)
defer this.userService.MatterUnlock(matter.UserUuid)
this.Recovery(request, matter, user)
}
//upload files.
func (this *MatterService) Upload(request *http.Request, file io.Reader, user *User, dirMatter *Matter, filename string, privacy bool) *Matter {
@@ -279,12 +354,15 @@ func (this *MatterService) Upload(request *http.Request, file io.Reader, user *U
panic(result.BadRequest("dirMatter cannot be nil."))
}
if dirMatter.Deleted {
panic(result.BadRequest("Dir has been deleted. Cannot upload under it."))
}
if len(filename) > MATTER_NAME_MAX_LENGTH {
panic(result.BadRequestI18n(request, i18n.MatterNameLengthExceedLimit, len(filename), MATTER_NAME_MAX_LENGTH))
}
dirAbsolutePath := dirMatter.AbsolutePath()
dirRelativePath := dirMatter.Path
count := this.matterDao.CountByUserUuidAndPuuidAndDirAndName(user.Uuid, dirMatter.Uuid, false, filename)
if count > 0 {
@@ -292,7 +370,6 @@ func (this *MatterService) Upload(request *http.Request, file io.Reader, user *U
}
fileAbsolutePath := dirAbsolutePath + "/" + filename
fileRelativePath := dirRelativePath + "/" + filename
util.MakeDirAll(dirAbsolutePath)
@@ -340,17 +417,29 @@ func (this *MatterService) Upload(request *http.Request, file io.Reader, user *U
}
}
matter := this.createNonDirMatter(dirMatter, filename, fileSize, privacy, user)
return matter
}
// create a non dir matter.
func (this *MatterService) createNonDirMatter(dirMatter *Matter, filename string, fileSize int64, privacy bool, user *User) *Matter {
dirRelativePath := dirMatter.Path
fileRelativePath := dirRelativePath + "/" + filename
//write to db.
matter := &Matter{
Puuid: dirMatter.Uuid,
UserUuid: user.Uuid,
Username: user.Username,
Dir: false,
Name: filename,
Md5: "",
Size: fileSize,
Privacy: privacy,
Path: fileRelativePath,
Puuid: dirMatter.Uuid,
UserUuid: user.Uuid,
Username: user.Username,
Dir: false,
Name: filename,
Md5: "",
Size: fileSize,
Privacy: privacy,
Path: fileRelativePath,
Prop: EMPTY_JSON_MAP,
VisitTime: time.Now(),
}
matter = this.matterDao.Create(matter)
@@ -362,6 +451,21 @@ func (this *MatterService) Upload(request *http.Request, file io.Reader, user *U
return matter
}
// create a non dir matter.
func (this *MatterService) updateNonDirMatter(matter *Matter, fileSize int64, user *User) *Matter {
matter.Size = fileSize
matter = this.matterDao.Save(matter)
//compute the size of directory
go core.RunWithRecovery(func() {
this.ComputeRouteSize(matter.Puuid, user)
})
return matter
}
// compute route size. It will compute upward until root directory
func (this *MatterService) ComputeRouteSize(matterUuid string, user *User) {
@@ -453,6 +557,10 @@ func (this *MatterService) createDirectory(request *http.Request, dirMatter *Mat
panic(result.BadRequest("dirMatter must be directory"))
}
if dirMatter.Deleted {
panic(result.BadRequest("Dir has been deleted. Cannot create dir under it."))
}
if dirMatter.UserUuid != user.Uuid {
panic(result.BadRequest("file's user not the same"))
@@ -474,7 +582,7 @@ func (this *MatterService) createDirectory(request *http.Request, dirMatter *Mat
}
//if exist. return.
matter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, dirMatter.Uuid, true, name)
matter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, dirMatter.Uuid, TRUE, name)
if matter != nil {
return matter
}
@@ -495,12 +603,13 @@ func (this *MatterService) createDirectory(request *http.Request, dirMatter *Mat
//create in db
matter = &Matter{
Puuid: dirMatter.Uuid,
UserUuid: user.Uuid,
Username: user.Username,
Dir: true,
Name: name,
Path: relativePath,
Puuid: dirMatter.Uuid,
UserUuid: user.Uuid,
Username: user.Username,
Dir: true,
Name: name,
Path: relativePath,
VisitTime: time.Now(),
}
matter = this.matterDao.Create(matter)
@@ -510,6 +619,10 @@ func (this *MatterService) createDirectory(request *http.Request, dirMatter *Mat
func (this *MatterService) AtomicCreateDirectory(request *http.Request, dirMatter *Matter, name string, user *User) *Matter {
if dirMatter.Deleted {
panic(result.BadRequest("Dir has been deleted. Cannot create sub dir under it."))
}
this.userService.MatterLock(user.Uuid)
defer this.userService.MatterUnlock(user.Uuid)
@@ -528,7 +641,8 @@ func (this *MatterService) handleOverwrite(request *http.Request, user *User, de
//delete.
this.Delete(request, destMatter, user)
} else {
panic(result.BadRequestI18n(request, i18n.MatterExist, destMatter.Path))
//throw precondition failed. (RFC4918:10.6)
panic(result.CustomWebResult(result.PRECONDITION_FAILED, fmt.Sprintf("%s exists", destMatter.Path)))
}
}
@@ -670,22 +784,29 @@ func (this *MatterService) AtomicMoveBatch(request *http.Request, srcMatters []*
//copy srcMatter to destMatter. invoker must handled the overwrite and lock.
func (this *MatterService) copy(request *http.Request, srcMatter *Matter, destDirMatter *Matter, name string) {
this.logger.Info("copy srcPath = %s destPath = %s/%s", srcMatter.Path, destDirMatter.Path, name)
if srcMatter.Dir {
newMatter := &Matter{
Puuid: destDirMatter.Uuid,
UserUuid: srcMatter.UserUuid,
Username: srcMatter.Username,
Dir: srcMatter.Dir,
Name: name,
Md5: "",
Size: srcMatter.Size,
Privacy: srcMatter.Privacy,
Path: destDirMatter.Path + "/" + name,
Puuid: destDirMatter.Uuid,
UserUuid: srcMatter.UserUuid,
Username: srcMatter.Username,
Dir: srcMatter.Dir,
Name: name,
Md5: "",
Size: srcMatter.Size,
Privacy: srcMatter.Privacy,
Path: destDirMatter.Path + "/" + name,
Prop: EMPTY_JSON_MAP,
VisitTime: time.Now(),
}
newMatter = this.matterDao.Create(newMatter)
//make the dir
util.MakeDirAll(newMatter.AbsolutePath())
//copy children
matters := this.matterDao.FindByPuuidAndUserUuid(srcMatter.Uuid, srcMatter.UserUuid, nil)
for _, m := range matters {
@@ -701,15 +822,17 @@ func (this *MatterService) copy(request *http.Request, srcMatter *Matter, destDi
util.CopyFile(srcAbsolutePath, destAbsolutePath)
newMatter := &Matter{
Puuid: destDirMatter.Uuid,
UserUuid: srcMatter.UserUuid,
Username: srcMatter.Username,
Dir: srcMatter.Dir,
Name: name,
Md5: "",
Size: srcMatter.Size,
Privacy: srcMatter.Privacy,
Path: destDirMatter.Path + "/" + name,
Puuid: destDirMatter.Uuid,
UserUuid: srcMatter.UserUuid,
Username: srcMatter.Username,
Dir: srcMatter.Dir,
Name: name,
Md5: "",
Size: srcMatter.Size,
Privacy: srcMatter.Privacy,
Path: destDirMatter.Path + "/" + name,
Prop: EMPTY_JSON_MAP,
VisitTime: time.Now(),
}
newMatter = this.matterDao.Create(newMatter)
@@ -737,12 +860,18 @@ func (this *MatterService) AtomicCopy(request *http.Request, srcMatter *Matter,
}
//rename matter to name
func (this *MatterService) AtomicRename(request *http.Request, matter *Matter, name string, user *User) {
func (this *MatterService) AtomicRename(request *http.Request, matter *Matter, name string, overwrite bool, user *User) {
this.logger.Info("Try to rename srcPath = %s to name = %s", matter.Path, name)
if user == nil {
panic(result.BadRequest("user cannot be nil"))
}
if matter.Deleted {
panic(result.BadRequest("matter has been deleted. Cannot rename."))
}
this.userService.MatterLock(user.Uuid)
defer this.userService.MatterUnlock(user.Uuid)
@@ -752,12 +881,16 @@ func (this *MatterService) AtomicRename(request *http.Request, matter *Matter, n
panic(result.BadRequestI18n(request, i18n.MatterNameNoChange))
}
//判断同级文件夹中是否有同名的文件
count := this.matterDao.CountByUserUuidAndPuuidAndDirAndName(user.Uuid, matter.Puuid, matter.Dir, name)
//check whether the name used by another matter.
oldMatter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, matter.Puuid, "", name)
if oldMatter != nil {
if overwrite {
//delete this one.
this.Delete(request, oldMatter, user)
} else {
panic(result.CustomWebResult(result.PRECONDITION_FAILED, fmt.Sprintf("%s already exists", name)))
}
if count > 0 {
panic(result.BadRequestI18n(request, i18n.MatterExist, name))
}
if matter.Dir {
@@ -826,6 +959,10 @@ func (this *MatterService) AtomicMirror(request *http.Request, srcPath string, d
destDirMatter := this.CreateDirectories(request, user, destPath)
if destDirMatter.Deleted {
panic(result.BadRequest("dest matter has been deleted. Cannot mirror."))
}
this.mirror(request, srcPath, destDirMatter, overwrite, user)
}
@@ -853,7 +990,7 @@ func (this *MatterService) mirror(request *http.Request, srcPath string, destDir
if fileStat.IsDir() {
//判断当前文件夹下,文件是否已经存在了。
srcDirMatter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, destDirMatter.Uuid, true, fileStat.Name())
srcDirMatter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, destDirMatter.Uuid, TRUE, fileStat.Name())
if srcDirMatter == nil {
srcDirMatter = this.createDirectory(request, destDirMatter, fileStat.Name(), user)
@@ -872,7 +1009,7 @@ func (this *MatterService) mirror(request *http.Request, srcPath string, destDir
} else {
//判断当前文件夹下,文件是否已经存在了。
matter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, destDirMatter.Uuid, false, fileStat.Name())
matter := this.matterDao.FindByUserUuidAndPuuidAndDirAndName(user.Uuid, destDirMatter.Uuid, FALSE, fileStat.Name())
if matter != nil {
//如果是覆盖,那么删除之前的文件
if overwrite {
@@ -945,13 +1082,17 @@ func (this *MatterService) WrapParentDetail(request *http.Request, matter *Matte
panic(result.BadRequest("matter cannot be nil."))
}
puuid := matter.Puuid
tmpMatter := matter
for puuid != MATTER_ROOT {
pFile := this.matterDao.CheckByUuid(puuid)
tmpMatter.Parent = pFile
tmpMatter = pFile
puuid = pFile.Puuid
//when self not root.
if matter.Uuid != MATTER_ROOT {
puuid := matter.Puuid
tmpMatter := matter
for puuid != MATTER_ROOT {
pFile := this.matterDao.CheckByUuid(puuid)
tmpMatter.Parent = pFile
tmpMatter = pFile
puuid = pFile.Puuid
}
}
return matter
@@ -989,6 +1130,10 @@ func (this *MatterService) AtomicCrawl(request *http.Request, url string, filena
panic(result.BadRequest("user cannot be nil."))
}
if dirMatter.Deleted {
panic(result.BadRequest("Dir has been deleted. Cannot crawl under it."))
}
this.userService.MatterLock(user.Uuid)
defer this.userService.MatterUnlock(user.Uuid)
@@ -1030,3 +1175,162 @@ func (this *MatterService) adjustPath(matter *Matter, parentMatter *Matter) {
}
}
//delete someone's EyeblueTank files according to physics files.
func (this *MatterService) DeleteByPhysics(request *http.Request, user *User) {
if user == nil {
panic(result.BadRequest("user cannot be nil."))
}
//scan user's file. scan level by level.
rootMatter := NewRootMatter(user)
this.deleteFolderByPhysics(request, rootMatter, user)
}
func (this *MatterService) deleteFolderByPhysics(request *http.Request, dirMatter *Matter, user *User) {
//scan user's file. scan level by level.
this.matterDao.PageHandle(dirMatter.Uuid, user.Uuid, "", "", "", nil, nil, func(matter *Matter) {
if matter.Dir {
//delete children first.
this.deleteFolderByPhysics(request, matter, user)
}
if !util.PathExists(matter.AbsolutePath()) {
this.logger.Info("physics file not exist. delete from tank. %s", matter.Name)
this.AtomicDelete(nil, matter, user)
}
})
}
//scan someone's physics files to EyeblueTank
func (this *MatterService) ScanPhysics(request *http.Request, user *User) {
if user == nil {
panic(result.BadRequest("user cannot be nil."))
}
rootDirPath := GetUserMatterRootDir(user.Username)
this.logger.Info("scan %s's root dir %s", user.Username, rootDirPath)
rootExists := util.PathExists(rootDirPath)
if !rootExists {
util.MakeDirAll(rootDirPath)
}
rootFileInfo, err := os.Lstat(rootDirPath)
if err != nil {
panic(result.BadRequest("cannot get root file info."))
}
rootMatter := NewRootMatter(user)
this.scanPhysicsFolder(request, rootFileInfo, rootMatter, user)
}
func (this *MatterService) scanPhysicsFolder(request *http.Request, dirInfo os.FileInfo, dirMatter *Matter, user *User) {
if !dirInfo.IsDir() {
return
}
//fetch all matters under this folder.
_, matters := this.matterDao.PlainPage(0, 1000, dirMatter.Uuid, user.Uuid, "", "", "", nil, nil, nil)
nameMatterMap := make(map[string]*Matter)
for _, m := range matters {
nameMatterMap[m.Name] = m
}
dirPath := dirMatter.AbsolutePath()
names, err := util.ReadDirNames(dirPath)
if err != nil {
this.logger.Error("occur error when ReadDirNames %s %s", dirPath, err.Error())
return
}
for _, name := range names {
fileFullPath := filepath.Join(dirPath, name)
fileInfo, err := os.Lstat(fileFullPath)
if err != nil {
this.logger.Error("occur error when Lstat %s %s", name, err.Error())
continue
}
//find ther matter
var matter *Matter
_, ok := nameMatterMap[name]
if ok {
//exits. check the basic info.
matter = nameMatterMap[name]
//only check the fileSize.
if !matter.Dir {
if matter.Size != fileInfo.Size() {
this.logger.Info("update matter: %s size:%d -> %d", name, matter.Size, fileInfo.Size())
this.updateNonDirMatter(matter, fileInfo.Size(), user)
}
} else {
//recursive scan this folder.
this.scanPhysicsFolder(request, fileInfo, matter, user)
}
} else {
if fileInfo.IsDir() {
//create folder.
matter = this.createDirectory(request, dirMatter, name, user)
//recursive scan this folder.
this.scanPhysicsFolder(request, fileInfo, matter, user)
} else {
//not exist. add basic info.
this.logger.Info("Create matter: %s size:%d", name, fileInfo.Size())
matter = this.createNonDirMatter(dirMatter, name, fileInfo.Size(), true, user)
}
}
}
}
//clean all the expired deleted matters
func (this *MatterService) CleanExpiredDeletedMatters() {
//mock a request.
request := &http.Request{}
preference := this.preferenceService.Fetch()
this.userDao.PageHandle("", "", func(user *User) {
this.logger.Info("Clean %s 's deleted matters", user.Username)
thenDate := time.Now()
thenDate = thenDate.AddDate(0, 0, int(-preference.DeletedKeepDays))
if preference.DeletedKeepDays != 0 {
thenDate = util.FirstSecondOfDay(thenDate)
}
//first remove all the matter(not dir).
this.matterDao.PageHandle("", "", "", FALSE, TRUE, &thenDate, nil, func(matter *Matter) {
this.Delete(request, matter, user)
})
sortArray := []builder.OrderPair{
{
Key: "path",
Value: DIRECTION_DESC,
},
}
//remove all the deleted directories. sort by path.
this.matterDao.PageHandle("", "", "", TRUE, TRUE, &thenDate, sortArray, func(matter *Matter) {
this.Delete(request, matter, user)
})
})
}
+101 -13
View File
@@ -2,8 +2,10 @@ package rest
import (
"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"
jsoniter "github.com/json-iterator/go"
"net/http"
"strconv"
)
@@ -12,7 +14,9 @@ type PreferenceController struct {
BaseController
preferenceDao *PreferenceDao
matterDao *MatterDao
matterService *MatterService
preferenceService *PreferenceService
taskService *TaskService
}
func (this *PreferenceController) Init() {
@@ -27,11 +31,20 @@ func (this *PreferenceController) Init() {
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.preferenceService)
if b, ok := b.(*PreferenceService); ok {
this.preferenceService = b
}
b = core.CONTEXT.GetBean(this.taskService)
if b, ok := b.(*TaskService); ok {
this.taskService = b
}
}
func (this *PreferenceController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
@@ -41,8 +54,10 @@ func (this *PreferenceController) RegisterRoutes() map[string]func(writer http.R
routeMap["/api/preference/ping"] = this.Wrap(this.Ping, USER_ROLE_GUEST)
routeMap["/api/preference/fetch"] = this.Wrap(this.Fetch, USER_ROLE_GUEST)
routeMap["/api/preference/edit"] = this.Wrap(this.Edit, USER_ROLE_ADMINISTRATOR)
routeMap["/api/preference/edit/preview/config"] = this.Wrap(this.EditPreviewConfig, USER_ROLE_ADMINISTRATOR)
routeMap["/api/preference/edit/scan/config"] = this.Wrap(this.EditScanConfig, USER_ROLE_ADMINISTRATOR)
routeMap["/api/preference/scan/once"] = this.Wrap(this.ScanOnce, USER_ROLE_ADMINISTRATOR)
routeMap["/api/preference/system/cleanup"] = this.Wrap(this.SystemCleanup, USER_ROLE_ADMINISTRATOR)
routeMap["/api/preference/migrate20to30"] = this.Wrap(this.Migrate20to30, USER_ROLE_ADMINISTRATOR)
return routeMap
}
@@ -61,6 +76,7 @@ func (this *PreferenceController) Fetch(writer http.ResponseWriter, request *htt
return this.Success(preference)
}
//edit basic info.
func (this *PreferenceController) Edit(writer http.ResponseWriter, request *http.Request) *result.WebResult {
name := request.FormValue("name")
@@ -73,6 +89,7 @@ func (this *PreferenceController) Edit(writer http.ResponseWriter, request *http
downloadDirMaxNumStr := request.FormValue("downloadDirMaxNum")
defaultTotalSizeLimitStr := request.FormValue("defaultTotalSizeLimit")
allowRegisterStr := request.FormValue("allowRegister")
deletedKeepDaysStr := request.FormValue("deletedKeepDays")
if name == "" {
panic(result.BadRequest("name cannot be null"))
@@ -105,12 +122,26 @@ func (this *PreferenceController) Edit(writer http.ResponseWriter, request *http
defaultTotalSizeLimit = int64(intDefaultTotalSizeLimit)
}
var deletedKeepDays int64 = 0
if deletedKeepDaysStr == "" {
panic(result.BadRequest("deletedKeepDays cannot be null"))
} else {
intDeletedKeepDays, err := strconv.Atoi(deletedKeepDaysStr)
this.PanicError(err)
deletedKeepDays = int64(intDeletedKeepDays)
if deletedKeepDays < 0 {
panic(result.BadRequest("deletedKeepDays cannot less than 0"))
}
}
var allowRegister = false
if allowRegisterStr == TRUE {
allowRegister = true
}
preference := this.preferenceDao.Fetch()
oldDeletedKeepDays := preference.DeletedKeepDays
preference.Name = name
preference.LogoUrl = logoUrl
preference.FaviconUrl = faviconUrl
@@ -120,15 +151,81 @@ func (this *PreferenceController) Edit(writer http.ResponseWriter, request *http
preference.DownloadDirMaxNum = downloadDirMaxNum
preference.DefaultTotalSizeLimit = defaultTotalSizeLimit
preference.AllowRegister = allowRegister
preference.DeletedKeepDays = deletedKeepDays
preference = this.preferenceDao.Save(preference)
preference = this.preferenceService.Save(preference)
//reset the preference cache
this.preferenceService.Reset()
//if changed the bin strategy. then trigger once.
if oldDeletedKeepDays != deletedKeepDays {
this.matterService.CleanExpiredDeletedMatters()
}
return this.Success(preference)
}
//edit preview config.
func (this *PreferenceController) EditPreviewConfig(writer http.ResponseWriter, request *http.Request) *result.WebResult {
previewConfig := request.FormValue("previewConfig")
preference := this.preferenceDao.Fetch()
preference.PreviewConfig = previewConfig
preference = this.preferenceService.Save(preference)
return this.Success(preference)
}
func (this *PreferenceController) EditScanConfig(writer http.ResponseWriter, request *http.Request) *result.WebResult {
scanConfigStr := request.FormValue("scanConfig")
if scanConfigStr == "" {
panic(result.BadRequest("scanConfig cannot be null"))
}
preference := this.preferenceDao.Fetch()
scanConfig := &ScanConfig{}
err := jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal([]byte(scanConfigStr), &scanConfig)
if err != nil {
panic(err)
}
//validate the scan config.
if scanConfig.Enable {
//validate cron.
if !util.ValidateCron(scanConfig.Cron) {
panic(result.CustomWebResultI18n(request, result.SHARE_CODE_ERROR, i18n.CronValidateError))
}
//validate scope.
if scanConfig.Scope == SCAN_SCOPE_CUSTOM {
if len(scanConfig.Usernames) == 0 {
panic(result.BadRequest("scope cannot be null"))
}
} else if scanConfig.Scope == SCAN_SCOPE_ALL {
} else {
panic(result.BadRequest("cannot recognize scope %s", scanConfig.Scope))
}
}
preference.ScanConfig = scanConfigStr
preference = this.preferenceService.Save(preference)
//reinit the scan task.
this.taskService.InitScanTask()
return this.Success(preference)
}
//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.
func (this *PreferenceController) SystemCleanup(writer http.ResponseWriter, request *http.Request) *result.WebResult {
@@ -144,12 +241,3 @@ func (this *PreferenceController) SystemCleanup(writer http.ResponseWriter, requ
return this.Success("OK")
}
//migrate 2.0's db data and file data to 3.0
func (this *PreferenceController) Migrate20to30(writer http.ResponseWriter, request *http.Request) *result.WebResult {
this.logger.Info("start migrating from 2.0 to 3.0")
this.preferenceService.Migrate20to30(writer, request)
return this.Success("OK")
}
+3 -1
View File
@@ -3,7 +3,7 @@ package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/nu7hatch/gouuid"
"github.com/eyebluecn/tank/code/tool/uuid"
"time"
)
@@ -22,6 +22,8 @@ func (this *PreferenceDao) Fetch() *Preference {
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
preference.Name = "EyeblueTank"
preference.Version = core.VERSION
preference.PreviewConfig = "{}"
preference.ScanConfig = "{}"
this.Create(preference)
return preference
} else {
+58 -15
View File
@@ -1,22 +1,65 @@
package rest
import "github.com/eyebluecn/tank/code/core"
import (
jsoniter "github.com/json-iterator/go"
"time"
)
type Preference struct {
Base
Name string `json:"name" gorm:"type:varchar(45)"`
LogoUrl string `json:"logoUrl" gorm:"type:varchar(255)"`
FaviconUrl string `json:"faviconUrl" gorm:"type:varchar(255)"`
Copyright string `json:"copyright" gorm:"type:varchar(1024)"`
Record string `json:"record" gorm:"type:varchar(1024)"`
DownloadDirMaxSize int64 `json:"downloadDirMaxSize" gorm:"type:bigint(20) not null;default:-1"`
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"`
Version string `json:"version" gorm:"-"`
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(45)"`
LogoUrl string `json:"logoUrl" gorm:"type:varchar(255)"`
FaviconUrl string `json:"faviconUrl" gorm:"type:varchar(255)"`
Copyright string `json:"copyright" gorm:"type:varchar(1024)"`
Record string `json:"record" gorm:"type:varchar(1024)"`
DownloadDirMaxSize int64 `json:"downloadDirMaxSize" gorm:"type:bigint(20) not null;default:-1"`
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"`
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"`
Version string `json:"version" gorm:"-"`
}
// set File's table name to be `profiles`
func (this *Preference) TableName() string {
return core.TABLE_PREFIX + "preference"
const (
//scan scope all.
SCAN_SCOPE_ALL = "ALL"
//scan scope custom.
SCAN_SCOPE_CUSTOM = "CUSTOM"
)
//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"`
//scan scope. see SCAN_SCOPE
Scope string `json:"scope"`
}
//fetch the scan config
func (this *Preference) FetchScanConfig() *ScanConfig {
json := this.ScanConfig
if json == "" || json == EMPTY_JSON_MAP {
return &ScanConfig{
Enable: false,
}
} else {
m := &ScanConfig{}
err := jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal([]byte(json), &m)
if err != nil {
panic(err)
}
return m
}
}
+11 -196
View File
@@ -1,15 +1,7 @@
package rest
import (
"fmt"
"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"
"net/http"
"os"
"regexp"
"strings"
)
//@Service
@@ -64,6 +56,17 @@ func (this *PreferenceService) Reset() {
}
//清空单例配置。
func (this *PreferenceService) Save(preference *Preference) *Preference {
preference = this.preferenceDao.Save(preference)
//clean cache.
this.Reset()
return preference
}
//System cleanup.
func (this *PreferenceService) Cleanup() {
@@ -71,191 +74,3 @@ func (this *PreferenceService) Cleanup() {
this.Reset()
}
//migrate 2.0's db data and file data to 3.0
func (this *PreferenceService) Migrate20to30(writer http.ResponseWriter, request *http.Request) {
matterPath := request.FormValue("matterPath")
if matterPath == "" {
panic(result.BadRequest("matterPath required"))
}
this.logger.Info("start migrating from 2.0 to 3.0")
//lock
if this.migrating {
panic(result.BadRequest("migrating work is processing"))
} else {
this.migrating = true
}
defer func() {
this.migrating = false
}()
//delete all users with _20
this.userDao.DeleteUsers20()
migrateDashboardSql := "INSERT INTO `tank`.`tank30_download_token` ( `uuid`, `sort`, `update_time`, `create_time`, `user_uuid`, `matter_uuid`, `expire_time`, `ip` ) ( SELECT `uuid`, `sort`, `update_time`, `create_time`, `user_uuid`, `matter_uuid`, `expire_time`, `ip` FROM `tank`.`tank20_download_token`)"
this.logger.Info(migrateDashboardSql)
db := core.CONTEXT.GetDB().Exec(migrateDashboardSql)
if db.Error != nil {
this.logger.Error("%v", db.Error)
}
migrateDownloadTokenSql := "INSERT INTO `tank`.`tank30_dashboard` ( `uuid`, `sort`, `update_time`, `create_time`, `invoke_num`, `total_invoke_num`, `uv`, `total_uv`, `matter_num`, `total_matter_num`, `file_size`, `total_file_size`, `avg_cost`, `dt` ) ( SELECT `uuid`, `sort`, `update_time`, `create_time`, `invoke_num`, `total_invoke_num`, `uv`, `total_uv`, `matter_num`, `total_matter_num`, `file_size`, `total_file_size`, `avg_cost`, `dt` FROM `tank`.`tank20_dashboard` )"
this.logger.Info(migrateDownloadTokenSql)
db = core.CONTEXT.GetDB().Exec(migrateDownloadTokenSql)
if db.Error != nil {
this.logger.Error("%v", db.Error)
}
migrateMatterSql := "INSERT INTO `tank`.`tank30_matter` ( `uuid`, `sort`, `update_time`, `create_time`, `puuid`, `user_uuid`, `username`, `dir`, `name`, `md5`, `size`, `privacy`, `path`, `times` ) ( SELECT `uuid`, `sort`, `update_time`, `create_time`, `puuid`, `user_uuid`, '', `dir`, `name`, `md5`, `size`, `privacy`, `path`, `times` FROM `tank`.`tank20_matter` ) "
this.logger.Info(migrateMatterSql)
db = core.CONTEXT.GetDB().Exec(migrateMatterSql)
if db.Error != nil {
this.logger.Error("%v", db.Error)
}
migrateUploadTokenSql := "INSERT INTO `tank`.`tank30_upload_token` ( `uuid`, `sort`, `update_time`, `create_time`, `user_uuid`, `folder_uuid`, `matter_uuid`, `expire_time`, `filename`, `privacy`, `size`, `ip` ) ( SELECT `uuid`, `sort`, `update_time`, `create_time`, `user_uuid`, `folder_uuid`, `matter_uuid`, `expire_time`, `filename`, `privacy`, `size`, `ip` FROM `tank`.`tank20_upload_token` ) "
this.logger.Info(migrateUploadTokenSql)
db = core.CONTEXT.GetDB().Exec(migrateUploadTokenSql)
if db.Error != nil {
this.logger.Error("%v", db.Error)
}
//username in tank2.0 add _20.
migrateUserSql := "INSERT INTO `tank`.`tank30_user` ( `uuid`, `sort`, `update_time`, `create_time`, `role`, `username`, `password`, `avatar_url`, `last_ip`, `last_time`, `size_limit`, `total_size_limit`, `total_size`, `status` ) ( SELECT `uuid`, `sort`, `update_time`, `create_time`, `role`, CONCAT(`username`,'_20') as `username`, `password`, `avatar_url`, `last_ip`, `last_time`, `size_limit`, -1, 0, `status` FROM `tank`.`tank20_user` )"
this.logger.Info(migrateUserSql)
db = core.CONTEXT.GetDB().Exec(migrateUserSql)
if db.Error != nil {
this.logger.Error("%v", db.Error)
}
//find all 2.0 users.
users := this.userDao.FindUsers20()
for _, user := range users {
this.logger.Info("start handling matters for user %s %s", user.Uuid, user.Username)
rootMatter := NewRootMatter(user)
firstLevelMatters := this.matterDao.FindByPuuidAndUserUuid(MATTER_ROOT, user.Uuid, nil)
for _, firstLevelMatter := range firstLevelMatters {
this.HandleMatter20(request, matterPath, rootMatter, firstLevelMatter, user)
}
//adjust all the size.
this.matterService.ComputeAllDirSize(user)
}
}
//handle matter from 2.0
func (this *PreferenceService) HandleMatter20(request *http.Request, matterPath string, dirMatter *Matter, matter *Matter, user *User) {
defer func() {
if err := recover(); err != nil {
this.logger.Warn("HandleMatter20 occur error %v when handle matter %s %s. Ignore the error and continue. \r\n", err, matter.Uuid, matter.Name)
}
}()
this.logger.Info("start handling matter %s", matter.Name)
if matter == nil {
panic(result.BadRequest("matter cannot be nil."))
}
if user == nil {
panic(result.BadRequest("user cannot be nil."))
}
if dirMatter == nil {
panic(result.BadRequest("dirMatter cannot be nil"))
}
if !dirMatter.Dir {
panic(result.BadRequest("dirMatter must be directory"))
}
if dirMatter.UserUuid != user.Uuid {
panic(result.BadRequest("file's user not the same"))
}
name := matter.Name
filename := name
if name == "" {
panic(result.BadRequest("name cannot be blank"))
}
if len(name) > MATTER_NAME_MAX_LENGTH {
panic(result.BadRequestI18n(request, i18n.MatterNameLengthExceedLimit, len(name), MATTER_NAME_MAX_LENGTH))
}
//if directory. Create it.
if matter.Dir {
if m, _ := regexp.MatchString(MATTER_NAME_PATTERN, name); m {
panic(result.BadRequestI18n(request, i18n.MatterNameContainSpecialChars))
}
parts := strings.Split(dirMatter.Path, "/")
if len(parts) > MATTER_NAME_MAX_DEPTH {
panic(result.BadRequestI18n(request, i18n.MatterDepthExceedLimit, len(parts), MATTER_NAME_MAX_DEPTH))
}
absolutePath := GetUserMatterRootDir(user.Username) + dirMatter.Path + "/" + name
relativePath := dirMatter.Path + "/" + name
//crate directory on disk.
dirPath := util.MakeDirAll(absolutePath)
this.logger.Info("Create Directory: %s", dirPath)
//change matter info.
matter.Username = user.Username
matter.Path = relativePath
matter = this.matterDao.Save(matter)
//handle its children.
children := this.matterDao.FindByPuuidAndUserUuid(matter.Uuid, user.Uuid, nil)
for _, child := range children {
this.HandleMatter20(request, matterPath, matter, child, user)
}
} else {
//if file. copy and adjust it.
dirAbsolutePath := dirMatter.AbsolutePath()
dirRelativePath := dirMatter.Path
fileAbsolutePath := dirAbsolutePath + "/" + filename
fileRelativePath := dirRelativePath + "/" + filename
util.MakeDirAll(dirAbsolutePath)
//if exist, panic it.
exist := util.PathExists(fileAbsolutePath)
if exist {
this.logger.Error("%s exits, overwrite it.", fileAbsolutePath)
removeError := os.Remove(fileAbsolutePath)
this.PanicError(removeError)
}
srcAbsolutePath := fmt.Sprintf("%s%s", matterPath, matter.Path)
//find the 2.0 disk file.
fileSize := util.CopyFile(srcAbsolutePath, fileAbsolutePath)
this.logger.Info("copy %s %v ", filename, util.HumanFileSize(fileSize))
//update info.
matter.Path = fileRelativePath
matter.Username = user.Username
matter = this.matterDao.Save(matter)
}
}
+8 -1
View File
@@ -3,7 +3,7 @@ package rest
import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/nu7hatch/gouuid"
"github.com/eyebluecn/tank/code/tool/uuid"
"time"
)
@@ -67,6 +67,13 @@ func (this *SessionDao) Delete(uuid string) {
}
func (this *SessionDao) DeleteByUserUuid(userUuid string) {
db := core.CONTEXT.GetDB().Where("user_uuid = ?", userUuid).Delete(Session{})
this.PanicError(db.Error)
}
//System cleanup.
func (this *SessionDao) Cleanup() {
this.logger.Info("[SessionDao] clean up. Delete all Session")
+4 -7
View File
@@ -1,18 +1,15 @@
package rest
import (
"github.com/eyebluecn/tank/code/core"
"time"
)
type Session struct {
Base
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)"`
Ip string `json:"ip" gorm:"type:varchar(128) not null"`
ExpireTime time.Time `json:"expireTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
}
// set User's table name to be `profiles`
func (this *Session) TableName() string {
return core.TABLE_PREFIX + "session"
}
+4
View File
@@ -278,6 +278,10 @@ func (this *ShareController) Browse(writer http.ResponseWriter, request *http.Re
user := this.findUser(request)
share := this.shareService.CheckShare(request, shareUuid, code, user)
bridges := this.bridgeDao.FindByShareUuid(share.Uuid)
shareOwner := this.userDao.FindByUuid(share.UserUuid)
if shareOwner.Status == USER_STATUS_DISABLED {
panic(result.BadRequestI18n(request, i18n.UserDisabled))
}
if puuid == MATTER_ROOT {
+12 -5
View File
@@ -4,9 +4,9 @@ import (
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/jinzhu/gorm"
"gorm.io/gorm"
"github.com/nu7hatch/gouuid"
"github.com/eyebluecn/tank/code/tool/uuid"
"time"
)
@@ -39,6 +39,14 @@ func (this *ShareDao) CheckByUuid(uuid string) *Share {
func (this *ShareDao) Page(page int, pageSize int, userUuid string, sortArray []builder.OrderPair) *Pager {
count, shares := this.PlainPage(page, pageSize, userUuid, sortArray)
pager := NewPager(page, pageSize, count, shares)
return pager
}
func (this *ShareDao) PlainPage(page int, pageSize int, userUuid string, sortArray []builder.OrderPair) (int, []*Share) {
var wp = &builder.WherePair{}
if userUuid != "" {
@@ -48,16 +56,15 @@ func (this *ShareDao) Page(page int, pageSize int, userUuid string, sortArray []
var conditionDB *gorm.DB
conditionDB = core.CONTEXT.GetDB().Model(&Share{}).Where(wp.Query, wp.Args...)
count := 0
var count int64 = 0
db := conditionDB.Count(&count)
this.PanicError(db.Error)
var shares []*Share
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&shares)
this.PanicError(db.Error)
pager := NewPager(page, pageSize, count, shares)
return pager
return int(count), shares
}
func (this *ShareDao) Create(share *Share) *Share {

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