├── .env.example
├── .github
└── workflows
│ └── go.yml
├── .gitignore
├── .travis.yml
├── Dockerfile.server
├── Dockerfile.worker
├── LICENSE
├── Makefile
├── README.md
├── cmd
├── cli
│ ├── command
│ │ ├── command.go
│ │ └── example.go
│ └── main.go
├── server
│ └── main.go
└── worker
│ └── main.go
├── config
├── config.go
└── config.yml
├── demo.go
├── docs
└── docs.go
├── errors
└── errors.go
├── example.go
├── go.mod
├── go.sum
├── handler
├── auth.go
├── download_file.go
├── file.go
├── folder.go
├── group.go
├── handler.go
├── me.go
├── middleware
│ ├── auth.go
│ ├── db_middleware.go
│ ├── handle_error.go
│ ├── pub_middleware.go
│ └── service_middleware.go
├── upload_file.go
├── upload_image.go
└── user.go
├── model
├── certificate.go
├── file.go
├── folder.go
├── folder_file.go
├── group.go
├── share.go
├── ticket.go
└── user.go
├── pkg
├── bytesize
│ └── bytesize.go
├── hasher
│ ├── argon2.go
│ ├── bcypt.go
│ └── hasher.go
├── pubsub
│ ├── context.go
│ ├── pub.go
│ └── sub.go
└── storage_capacity
│ ├── capacity.go
│ └── capacity_test.go
├── queue
├── context.go
├── pub.go
└── subscribe
│ ├── queue.go
│ └── wrapper
│ ├── db.go
│ └── service.go
├── screenshots
├── download.png
├── home.png
├── login.png
├── queue.png
├── success.png
└── upload.png
├── server
├── server.go
└── setup.go
├── service
├── certificate.go
├── context.go
├── file.go
├── folder.go
├── folder_file.go
├── group.go
├── service.go
├── share.go
├── ticket.go
└── user.go
├── store
├── db_store
│ ├── certificate.go
│ ├── context.go
│ ├── file.go
│ ├── folder.go
│ ├── folder_file.go
│ ├── group.go
│ ├── share.go
│ ├── ticket.go
│ └── user.go
├── redis_store
│ └── ticket.go
└── store.go
└── tests
├── bytesize_test.go
└── example_test.go
/.env.example:
--------------------------------------------------------------------------------
1 | DEBUG=true
2 | CLOUD_APPSALT=cloud_disk
3 | CLOUD_FILESYSTEM_ROOT=data
4 |
5 | # database
6 | CLOUD_DATABASE_HOST=127.0.0.1
7 | CLOUD_DATABASE_USER=root
8 | CLOUD_DATABASE_PASSWORD=sunlong0717
9 | CLOUD_DATABASE_DBNAME=cloud_disk
10 |
11 | # redis
12 | CLOUD_REDIS_ADDRESS=127.0.0.1
13 |
14 | # minio
15 | CLOUD_MINIO_SSL=false
16 | CLOUD_MINIO_ACCESSKEY=zm2018
17 | CLOUD_MINIO_SECRETKEY=zhiming2018
18 | CLOUD_MINIO_BUCKETNAME=cloud-disk
19 | CLOUD_MINIO_HOST=127.0.0.1:9000
20 |
21 | CLOUD_IMAGE-PROXY_HOST=http://127.0.0.1:9001
22 | CLOUD_IMAGE-PROXY_OMITBASEURL=true
23 |
24 | # nos
25 | CLOUD_NOS_ACCESSKEY=9a03191ef6dc45b18d56957b2c3f5e85
26 | CLOUD_NOS_SECRETKEY=726c2e1e789141519b8744602f0db0b8
27 | CLOUD_NOS_BUCKETNAME=cloud-disk
28 | CLOUD_NOS_ENDPOINT=nos-eastchina1-i.netease.com
29 | CLOUD_NOS_EXTERNAL-ENDPOINT=https://cloud-disk.nos-eastchina1.126.net
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 | on: [push]
3 | jobs:
4 |
5 | build:
6 | name: Build
7 | runs-on: ubuntu-16.04
8 | steps:
9 | - name: Set up Go 1.12
10 | uses: actions/setup-go@v1
11 | with:
12 | go-version: 1.12
13 | id: go
14 | - name: Check out code into the Go module directory
15 | uses: actions/checkout@v1
16 | - name: Get dependencies
17 | run: |
18 | export GO111MODULE=on
19 | export GOPROXY=https://goproxy.cn
20 | go mod vendor
21 | - name: Build
22 | run: go build -v .
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | data/*
4 | .env
5 | docs/swagger
6 | vendor/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.11.x
5 |
6 | script:
7 | - make test
--------------------------------------------------------------------------------
/Dockerfile.server:
--------------------------------------------------------------------------------
1 | FROM golang:1.11.1-alpine3.7 as builder
2 |
3 | RUN set -eux; \
4 | apk add --no-cache --virtual .build-deps \
5 | bash \
6 | musl-dev \
7 | openssl \
8 | go
9 |
10 | COPY . /go/src/github.com/wq1019/cloud_disk
11 |
12 | RUN go build -v -o /app/server /go/src/github.com/wq1019/cloud_disk/cmd/server/main.go && \
13 | go build -v -o /app/cli /go/src/github.com/wq1019/cloud_disk/cmd/cli/main.go
14 |
15 |
16 | FROM alpine:3.7
17 |
18 | RUN apk update && apk --no-cache add mailcap ca-certificates tzdata
19 |
20 | ENV TZ=Asia/Shanghai
21 | #设置时区
22 | #RUN /bin/cp /usr/share/zoneinfo/$TZ /etc/localtime \
23 | # && echo '$TZ' >/etc/timezone
24 |
25 |
26 | COPY --from=builder /app/server /app/server
27 | COPY --from=builder /app/cli /app/cli
28 | COPY --from=builder /go/src/github.com/wq1019/cloud_disk/config/config.yml /app/config/config.yml
29 |
30 | WORKDIR /app
31 |
32 | RUN chmod +x /app/server /app/cli
33 |
34 | CMD ["./server"]
--------------------------------------------------------------------------------
/Dockerfile.worker:
--------------------------------------------------------------------------------
1 | FROM golang:1.11.1-alpine3.7 as builder
2 |
3 | RUN set -eux; \
4 | apk add --no-cache --virtual .build-deps \
5 | bash \
6 | musl-dev \
7 | openssl \
8 | go
9 |
10 | COPY . /go/src/github.com/wq1019/cloud_disk
11 |
12 | RUN go build -v -o /app/worker /go/src/github.com/wq1019/cloud_disk/cmd/worker/main.go
13 |
14 |
15 | FROM alpine:3.7
16 |
17 | RUN apk update && apk --no-cache add mailcap ca-certificates tzdata
18 |
19 | ENV TZ=Asia/Shanghai
20 | #设置时区
21 | #RUN /bin/cp /usr/share/zoneinfo/$TZ /etc/localtime \
22 | # && echo '$TZ' >/etc/timezone
23 |
24 | COPY --from=builder /app/worker /app/worker
25 | COPY --from=builder /go/src/github.com/wq1019/cloud_disk/config/config.yml /app/config/config.yml
26 |
27 | WORKDIR /app
28 |
29 | RUN chmod +x /app/worker
30 |
31 | CMD ["./worker"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | doc:
2 | swag init -g cmd/server/main.go
3 |
4 | docker:
5 | docker build -t cloud-disk:latest -f Dockerfile.server .
6 | docker build -t cloud-disk_worker:latest -f Dockerfile.worker .
7 |
8 | upload: docker
9 | docker tag cloud-disk:latest registry.cn-hangzhou.aliyuncs.com/wqer1019/cloud-disk:latest
10 | docker push registry.cn-hangzhou.aliyuncs.com/wqer1019/cloud-disk:latest
11 | docker tag cloud-disk_worker:latest registry.cn-hangzhou.aliyuncs.com/wqer1019/cloud-disk_worker:latest
12 | docker push registry.cn-hangzhou.aliyuncs.com/wqer1019/cloud-disk_worker:latest
13 |
14 | run:
15 | docker run -d cloud-disk:latest
16 | docker run -d cloud-disk_worker:latest
17 |
18 | test:
19 | go test tests/*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > 注意:此项目只是大学时期无聊时和小伙伴开发的一款小demo,虽然功能都实现了,但是代码实在太业余,参考价值实在有限,同时前端代码仓库已经不小心被删除,请大家谨慎参考。
2 | > 如果你想用来做大学生毕业设计,可以自己实现一下前端功能,接口基本都有的。
3 | >
4 | # 网络云盘
5 | [](https://www.travis-ci.org/wq1019/cloud_disk)
6 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fsunl888%2Fcloud_disk?ref=badge_shield)
7 | ## 依赖
8 | - Minio || 网易云对象存储(nos)
9 | - Mysql [phpMyAdmin]
10 | - Redis [mini-redisadmin]
11 | ## TODO
12 | - [x] 文件分片上传
13 | - [x] 文件分片下载
14 | - [ ] 文件删除时更新用户可用空间 [BUG]
15 | - [x] 文件夹管理
16 | - [x] 文件批量复制
17 | - [x] 文件批量删除
18 | - [x] 文件批量移动
19 | - [x] 用户信息更新
20 | ## Screenshots
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ## 安装
29 | - 请移驾到 [部署脚本](https://github.com/wq1019/cloud-disk-deply.git).
30 |
31 |
32 | ## 支持
33 |
34 |
35 |
36 | JetBrains |
37 |
38 |
39 |
40 |
41 | |
42 |
43 |
44 |
45 |
46 | ## ⭐ Star 历史
47 | [](https://starchart.cc/sunl888/cloud_disk)
48 |
49 | ## License
50 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fsunl888%2Fcloud_disk?ref=badge_large&issueType=license)
51 |
--------------------------------------------------------------------------------
/cmd/cli/command/command.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "github.com/urfave/cli"
5 | "github.com/wq1019/cloud_disk/server"
6 | )
7 |
8 | func RegisterCommand(svr *server.Server) []cli.Command {
9 | return []cli.Command{
10 | NewExampleCommand(svr),
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/cmd/cli/command/example.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "github.com/urfave/cli"
5 | "github.com/wq1019/cloud_disk/server"
6 | "log"
7 | )
8 |
9 | func NewExampleCommand(svr *server.Server) cli.Command {
10 | return cli.Command{
11 | Name: "example",
12 | Usage: "命令行测试",
13 | Action: func(c *cli.Context) error {
14 | log.Println("example command is ok!")
15 | return nil
16 | },
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/cmd/cli/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/urfave/cli"
5 | "github.com/wq1019/cloud_disk/cmd/cli/command"
6 | "github.com/wq1019/cloud_disk/server"
7 | "log"
8 | "os"
9 | )
10 |
11 | var (
12 | defaultPath = "config/config.yml"
13 | )
14 |
15 | func main() {
16 | app := cli.NewApp()
17 | app.Flags = []cli.Flag{
18 | cli.StringFlag{
19 | Name: "config,c",
20 | Value: defaultPath,
21 | Usage: "set configuration `file`",
22 | },
23 | }
24 | svr := server.SetupServer(getConfigPathFromArgs())
25 | app.Name = "命令行工具"
26 | app.Usage = "haha"
27 | app.Version = "1.0.1"
28 | app.Commands = append(app.Commands, command.RegisterCommand(svr)...)
29 | if err := app.Run(os.Args); err != nil {
30 | log.Fatalln(err)
31 | }
32 | }
33 |
34 | func getConfigPathFromArgs() string {
35 | var (
36 | exist = false
37 | configPath = defaultPath
38 | )
39 | for _, v := range os.Args {
40 | if exist {
41 | configPath = v
42 | break
43 | } else if v == "-c" || v == "--config" {
44 | exist = true
45 | }
46 | }
47 | return configPath
48 | }
49 |
--------------------------------------------------------------------------------
/cmd/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "github.com/rs/cors"
6 | _ "github.com/wq1019/cloud_disk/docs"
7 | "github.com/wq1019/cloud_disk/handler"
8 | "github.com/wq1019/cloud_disk/server"
9 | "go.uber.org/zap"
10 | "log"
11 | "net/http"
12 | )
13 |
14 | var (
15 | h bool
16 | c string
17 | )
18 |
19 | func init() {
20 | flag.BoolVar(&h, "h", false, "the help")
21 | flag.StringVar(&c, "c", "config/config.yml", "set the relative path of the configuration `file`.")
22 | }
23 |
24 | // @title 云盘 Api 服务
25 | // @version 1.0
26 | // @description 云盘的 Api 服务.
27 | // @termsOfService https://github.com/zm-dev
28 | // @contact.name API Support
29 | // @contact.url https://github.com/wq1019
30 | // @contact.email 2013855675@qq.com
31 | // @license.name Apache 2.0
32 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html
33 | // @host localhost:8080
34 | // @BasePath /api
35 | func main() {
36 | flag.Parse()
37 | if h {
38 | flag.Usage()
39 | return
40 |
41 | }
42 | svr := server.SetupServer(c)
43 | svr.Logger.Info("listen", zap.String("addr", svr.Conf.ServerAddr))
44 | // cors 跨域用
45 | log.Fatal(http.ListenAndServe(svr.Conf.ServerAddr, cors.New(cors.Options{
46 | AllowedOrigins: []string{"*"},
47 | AllowedMethods: []string{"POST", "GET", "DELETE", "PUT", "HEAD"},
48 | AllowCredentials: true,
49 | }).Handler(handler.CreateHTTPHandler(svr))))
50 | }
51 |
--------------------------------------------------------------------------------
/cmd/worker/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "github.com/wq1019/cloud_disk/queue/subscribe"
6 | "github.com/wq1019/cloud_disk/server"
7 | "go.uber.org/zap"
8 | )
9 |
10 | var (
11 | h bool
12 | c string
13 | )
14 |
15 | func init() {
16 | flag.BoolVar(&h, "h", false, "the help")
17 | flag.StringVar(&c, "c", "config/config.yml", "set configuration `file`")
18 | }
19 |
20 | func main() {
21 | flag.Parse()
22 | if h {
23 | flag.Usage()
24 | return
25 |
26 | }
27 | svr := server.SetupServer(c)
28 | svr.Logger.Info("start queue", zap.Int("queue goroutine num", svr.Conf.QueueNum))
29 | subscribe.StartSubQueue(svr)
30 | }
31 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/micro/go-config"
5 | "github.com/micro/go-config/source/env"
6 | "github.com/micro/go-config/source/file"
7 | "log"
8 | "os"
9 | "path"
10 | )
11 |
12 | type DatabaseConfig struct {
13 | Driver string `json:"driver"`
14 | Host string `json:"host"`
15 | Port string `json:"port"`
16 | User string `json:"user"`
17 | Password string `json:"password"`
18 | DBName string `json:"dbname"`
19 | }
20 |
21 | type RedisConfig struct {
22 | Address string `json:"address"`
23 | Port string `json:"port"`
24 | }
25 |
26 | type TicketConfig struct {
27 | Driver string `json:"driver"` // ticket 使用的驱动 只支持 redis 和 database
28 | TTL int64 `json:"ttl"` // ticket 的过期时间 (毫秒)
29 | }
30 |
31 | type FilesystemConfig struct {
32 | Driver string `json:"driver"`
33 | Root string `json:"root"`
34 | }
35 |
36 | type MinioConfig struct {
37 | Host string `json:"host"`
38 | AccessKey string `json:"accesskey"`
39 | SecretKey string `json:"secretkey"`
40 | SSL string `json:"ssl"`
41 | BucketName string `json:"bucketname"`
42 | }
43 |
44 | type NosConfig struct {
45 | Endpoint string `json:"endpoint"`
46 | AccessKey string `json:"accesskey"`
47 | SecretKey string `json:"secretkey"`
48 | BucketName string `json:"bucketname"`
49 | ExternalEndpoint string `json:"external-endpoint"`
50 | }
51 |
52 | type ImageProxyConfig struct {
53 | Host string
54 | OmitBaseUrl string `json:"omitbaseurl"`
55 | }
56 |
57 | type Config struct {
58 | EnvVarPrefix string `json:"env-var-prefix"`
59 | ServiceName string `json:"service-name"`
60 | ServerAddr string `json:"server-addr"` // addr:port
61 | AppSalt string `json:"appsalt"`
62 | QueueNum int `json:"queue-num"`
63 | Fs FilesystemConfig `json:"filesystem"`
64 | DB DatabaseConfig `json:"database"`
65 | Redis RedisConfig `json:"redis"`
66 | Ticket TicketConfig `json:"ticket"`
67 | Minio MinioConfig `json:"minio"`
68 | Nos NosConfig `json:"nos"`
69 | ImageProxy ImageProxyConfig `json:"image-proxy"`
70 | }
71 |
72 | func LoadConfig(filepath string) *Config {
73 | c := &Config{}
74 | pwd, _ := os.Getwd()
75 | fileSource := file.NewSource(file.WithPath(path.Join(pwd, filepath)))
76 | checkErr(config.Load(fileSource))
77 | // env 的配置会覆盖文件中的配置
78 | envSource := env.NewSource(env.WithStrippedPrefix(config.Get("env-var-prefix").String("CLOUD")))
79 | checkErr(config.Load(envSource))
80 | checkErr(config.Scan(c))
81 | return c
82 | }
83 |
84 | func checkErr(err error) {
85 | if err != nil {
86 | log.Fatal(err)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/config/config.yml:
--------------------------------------------------------------------------------
1 | env-var-prefix: "CLOUD"
2 |
3 | service-name: "cloud_disk"
4 |
5 | server-addr: ":8080"
6 |
7 | appsalt: "some random string"
8 |
9 | filesystem:
10 | driver: "os"
11 | root: "data"
12 |
13 | database:
14 | driver: "mysql"
15 | host: "127.0.0.1"
16 | port: "3306"
17 | user: "root"
18 | password: "root"
19 | dbname: "cloud_disk"
20 |
21 | redis:
22 | address: "127.0.0.1"
23 | port: "6379"
24 |
25 | ticket:
26 | driver: "redis"
27 | ttl: 2592000
28 |
29 | queue-num: 20
30 |
31 | minio:
32 | host: "xxx"
33 | accesskey: "xxx"
34 | secretkey: "xxx"
35 | ssl: "false"
36 | bucketname: "xxx"
37 |
38 | nos:
39 | accesskey: "xxx"
40 | secretkey: "xxx"
41 | bucketname: "xxx"
42 | endpoint: "xxx"
43 | external-endpoint: "xxx"
44 |
45 | image-proxy:
46 | host: "xxx"
47 | omitbaseurl: "true"
48 |
--------------------------------------------------------------------------------
/demo.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/md5"
5 | "fmt"
6 | "io"
7 | "log"
8 | "os"
9 | )
10 |
11 | const (
12 | dir = "/home/sunlong/图片/"
13 | )
14 |
15 | func test(file *os.File) {
16 | md5hash := md5.New()
17 | if _, err := io.Copy(md5hash, file); err != nil {
18 | log.Println(err.Error())
19 | return
20 | }
21 | md5sum := md5hash.Sum(nil)
22 | fmt.Println(md5sum, fmt.Sprintf("%x", md5sum))
23 | }
24 |
25 | //go1.11.linux-amd64.tar.gz
26 | func main() {
27 | file, err := os.Open(dir + "1527580104.jpg")
28 | if err != nil {
29 | fmt.Println(err.Error())
30 | return
31 | }
32 | defer file.Close()
33 | var b []byte
34 | a, c := file.Read(b)
35 | fmt.Println(a, b, c)
36 | fileStat, err := file.Stat()
37 | if err != nil {
38 | fmt.Println(err.Error())
39 | return
40 | }
41 | size := int64(16 << 20) // 16*2^20
42 | blocks := make([][]byte, 0, 10)
43 | offset := int64(0)
44 | n := 0
45 | fmt.Println(size)
46 | for i := int64(0); i <= fileStat.Size()/size; i++ {
47 | block := make([]byte, 0, size)
48 | _, err := file.ReadAt(block, 0)
49 | if err != nil {
50 | log.Print(err)
51 | return
52 | }
53 | fmt.Println(block)
54 | blocks = append(blocks, block)
55 | offset += size + 1
56 | n++
57 | }
58 | writeFile, err := os.OpenFile(dir+"test.jpg", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
59 | for i := 0; i < n; i++ {
60 | _, err = writeFile.WriteAt(blocks[i], 0)
61 | if err != nil {
62 | log.Print(err)
63 | return
64 | }
65 | }
66 | defer writeFile.Close()
67 | newFileStat, err := writeFile.Stat()
68 | if err != nil {
69 | log.Print(err)
70 | return
71 | }
72 | fmt.Println(fileStat.Size(), fileStat.Name(), newFileStat.Name(), newFileStat.Size())
73 | }
74 |
--------------------------------------------------------------------------------
/docs/docs.go:
--------------------------------------------------------------------------------
1 | // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
2 | // This file was generated by swaggo/swag at
3 | // 2019-01-07 05:20:35.306174755 +0800 CST m=+0.091566167
4 |
5 | package docs
6 |
7 | import (
8 | "github.com/swaggo/swag"
9 | )
10 |
11 | var doc = `{
12 | "swagger": "2.0",
13 | "info": {
14 | "description": "云盘的 Api 服务.",
15 | "title": "云盘 Api 服务",
16 | "termsOfService": "https://github.com/zm-dev",
17 | "contact": {
18 | "name": "API Support",
19 | "url": "https://github.com/wq1019",
20 | "email": "2013855675@qq.com"
21 | },
22 | "license": {
23 | "name": "Apache 2.0",
24 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
25 | },
26 | "version": "1.0"
27 | },
28 | "host": "localhost:8080",
29 | "basePath": "/api",
30 | "paths": {
31 | "/file/rename": {
32 | "put": {
33 | "description": "通过文件 ID 重命名文件",
34 | "consumes": [
35 | "application/json",
36 | "multipart/form-data"
37 | ],
38 | "produces": [
39 | "application/json",
40 | "multipart/form-data"
41 | ],
42 | "tags": [
43 | "文件"
44 | ],
45 | "summary": "重命名文件",
46 | "operationId": "rename-file",
47 | "parameters": [
48 | {
49 | "type": "integer",
50 | "format": "uint64",
51 | "description": "文件 ID",
52 | "name": "file_id",
53 | "in": "query",
54 | "required": true
55 | },
56 | {
57 | "type": "integer",
58 | "format": "uint64",
59 | "description": "文件所属的目录 ID",
60 | "name": "folder_id",
61 | "in": "query",
62 | "required": true
63 | },
64 | {
65 | "type": "string",
66 | "format": "string",
67 | "description": "新的文件名",
68 | "name": "new_name",
69 | "in": "query",
70 | "required": true
71 | }
72 | ],
73 | "responses": {
74 | "204": {},
75 | "404": {
76 | "description": "文件不存在 | 目录不存在",
77 | "schema": {
78 | "type": "object",
79 | "$ref": "#/definitions/errors.GlobalError"
80 | }
81 | },
82 | "500": {
83 | "description": "Internal Server Error",
84 | "schema": {
85 | "type": "object",
86 | "$ref": "#/definitions/errors.GlobalError"
87 | }
88 | }
89 | }
90 | }
91 | },
92 | "/folder": {
93 | "get": {
94 | "description": "加载指定的目录及子目录和文件列表",
95 | "consumes": [
96 | "application/json",
97 | "multipart/form-data"
98 | ],
99 | "produces": [
100 | "application/json",
101 | "multipart/form-data"
102 | ],
103 | "tags": [
104 | "目录"
105 | ],
106 | "summary": "加载指定的目录及子目录和文件列表",
107 | "operationId": "load-folder",
108 | "parameters": [
109 | {
110 | "type": "integer",
111 | "format": "uint64",
112 | "description": "目录 ID",
113 | "name": "folder_id",
114 | "in": "query",
115 | "required": true
116 | }
117 | ],
118 | "responses": {
119 | "200": {
120 | "description": "OK",
121 | "schema": {
122 | "type": "object",
123 | "$ref": "#/definitions/model.Folder"
124 | }
125 | },
126 | "404": {
127 | "description": "目录不存在 | 没有访问权限 | id 格式不正确",
128 | "schema": {
129 | "type": "object",
130 | "$ref": "#/definitions/errors.GlobalError"
131 | }
132 | },
133 | "500": {
134 | "description": "Internal Server Error",
135 | "schema": {
136 | "type": "object",
137 | "$ref": "#/definitions/errors.GlobalError"
138 | }
139 | }
140 | }
141 | },
142 | "post": {
143 | "description": "创建一个目录",
144 | "consumes": [
145 | "application/json",
146 | "multipart/form-data"
147 | ],
148 | "produces": [
149 | "application/json",
150 | "multipart/form-data"
151 | ],
152 | "tags": [
153 | "目录"
154 | ],
155 | "summary": "创建一个目录",
156 | "operationId": "create-folder",
157 | "parameters": [
158 | {
159 | "type": "integer",
160 | "format": "uint64",
161 | "description": "父级目录的 ID",
162 | "name": "parent_id",
163 | "in": "query",
164 | "required": true
165 | },
166 | {
167 | "type": "string",
168 | "format": "string",
169 | "description": "新目录的名称",
170 | "name": "folder_name",
171 | "in": "query",
172 | "required": true
173 | }
174 | ],
175 | "responses": {
176 | "201": {
177 | "description": "Created",
178 | "schema": {
179 | "type": "object",
180 | "$ref": "#/definitions/model.Folder"
181 | }
182 | },
183 | "401": {
184 | "description": "请先登录",
185 | "schema": {
186 | "type": "object",
187 | "$ref": "#/definitions/errors.GlobalError"
188 | }
189 | },
190 | "404": {
191 | "description": "目录名称不能为空 | (父)目录不存在 | 目录已经存在",
192 | "schema": {
193 | "type": "object",
194 | "$ref": "#/definitions/errors.GlobalError"
195 | }
196 | },
197 | "500": {
198 | "description": "Internal Server Error",
199 | "schema": {
200 | "type": "object",
201 | "$ref": "#/definitions/errors.GlobalError"
202 | }
203 | }
204 | }
205 | },
206 | "delete": {
207 | "description": "批量删除资源(文件/目录)",
208 | "consumes": [
209 | "application/json"
210 | ],
211 | "produces": [
212 | "application/json"
213 | ],
214 | "tags": [
215 | "资源"
216 | ],
217 | "summary": "批量删除资源(文件/目录)",
218 | "operationId": "delete-source",
219 | "parameters": [
220 | {
221 | "type": "integer",
222 | "description": "当前目录的 ID",
223 | "name": "current_folder_id",
224 | "in": "query",
225 | "required": true
226 | },
227 | {
228 | "type": "array",
229 | "description": "要删除的文件 ids",
230 | "name": "file_ids",
231 | "in": "query"
232 | },
233 | {
234 | "type": "array",
235 | "description": "要删除的目录 ids",
236 | "name": "folder_ids",
237 | "in": "query"
238 | }
239 | ],
240 | "responses": {
241 | "204": {},
242 | "401": {
243 | "description": "请先登录",
244 | "schema": {
245 | "type": "object",
246 | "$ref": "#/definitions/errors.GlobalError"
247 | }
248 | },
249 | "404": {
250 | "description": "请指定要删除的文件或者目录ID | 当前目录不存在",
251 | "schema": {
252 | "type": "object",
253 | "$ref": "#/definitions/errors.GlobalError"
254 | }
255 | },
256 | "500": {
257 | "description": "Internal Server Error",
258 | "schema": {
259 | "type": "object",
260 | "$ref": "#/definitions/errors.GlobalError"
261 | }
262 | }
263 | }
264 | }
265 | },
266 | "/folder/rename": {
267 | "put": {
268 | "description": "通过目录 ID 重命名目录",
269 | "consumes": [
270 | "application/json",
271 | "multipart/form-data"
272 | ],
273 | "produces": [
274 | "application/json",
275 | "multipart/form-data"
276 | ],
277 | "tags": [
278 | "目录"
279 | ],
280 | "summary": "重命名目录",
281 | "operationId": "rename-folder",
282 | "parameters": [
283 | {
284 | "type": "integer",
285 | "format": "uint64",
286 | "description": "所属的目录 ID",
287 | "name": "folder_id",
288 | "in": "query",
289 | "required": true
290 | },
291 | {
292 | "type": "string",
293 | "format": "string",
294 | "description": "新的目录名",
295 | "name": "new_name",
296 | "in": "query",
297 | "required": true
298 | }
299 | ],
300 | "responses": {
301 | "204": {},
302 | "404": {
303 | "description": "目录不存在",
304 | "schema": {
305 | "type": "object",
306 | "$ref": "#/definitions/errors.GlobalError"
307 | }
308 | },
309 | "500": {
310 | "description": "Internal Server Error",
311 | "schema": {
312 | "type": "object",
313 | "$ref": "#/definitions/errors.GlobalError"
314 | }
315 | }
316 | }
317 | }
318 | }
319 | },
320 | "definitions": {
321 | "errors.GlobalError": {
322 | "type": "object",
323 | "properties": {
324 | "code": {
325 | "type": "integer",
326 | "example": 10001
327 | },
328 | "inner_err": {
329 | "type": "error"
330 | },
331 | "message": {
332 | "type": "string",
333 | "example": "error message"
334 | },
335 | "service_name": {
336 | "type": "string",
337 | "example": "cloud_disk"
338 | },
339 | "status_code": {
340 | "type": "integer",
341 | "example": 500
342 | }
343 | }
344 | },
345 | "model.File": {
346 | "type": "object",
347 | "properties": {
348 | "created_at": {
349 | "type": "string"
350 | },
351 | "extra": {
352 | "type": "string"
353 | },
354 | "filename": {
355 | "type": "string"
356 | },
357 | "format": {
358 | "type": "string"
359 | },
360 | "hash": {
361 | "type": "string"
362 | },
363 | "id": {
364 | "type": "integer"
365 | },
366 | "size": {
367 | "type": "integer"
368 | },
369 | "updated_at": {
370 | "type": "string"
371 | }
372 | }
373 | },
374 | "model.Folder": {
375 | "type": "object",
376 | "properties": {
377 | "created_at": {
378 | "type": "string"
379 | },
380 | "files": {
381 | "type": "array",
382 | "items": {
383 | "$ref": "#/definitions/model.File"
384 | }
385 | },
386 | "folder_name": {
387 | "type": "string"
388 | },
389 | "folders": {
390 | "type": "array",
391 | "items": {
392 | "$ref": "#/definitions/model.Folder"
393 | }
394 | },
395 | "id": {
396 | "type": "integer"
397 | },
398 | "key": {
399 | "type": "string"
400 | },
401 | "level": {
402 | "type": "integer"
403 | },
404 | "parent_id": {
405 | "type": "integer"
406 | },
407 | "updated_at": {
408 | "type": "string"
409 | },
410 | "user_id": {
411 | "type": "integer"
412 | }
413 | }
414 | }
415 | }
416 | }`
417 |
418 | type s struct{}
419 |
420 | func (s *s) ReadDoc() string {
421 | return doc
422 | }
423 | func init() {
424 | swag.Register(swag.Name, &s{})
425 | }
426 |
--------------------------------------------------------------------------------
/errors/errors.go:
--------------------------------------------------------------------------------
1 | package errors
2 |
3 | import (
4 | "github.com/zm-dev/gerrors"
5 | )
6 |
7 | // Swagger API documents need this structure.
8 | type GlobalError struct {
9 | Code int `json:"code" example:"10001"`
10 | ServiceName string `json:"service_name" example:"cloud_disk"`
11 | Message string `json:"message" example:"error message"`
12 | InnerErr error `json:"inner_err"`
13 | StatusCode int `json:"status_code" example:"500"`
14 | }
15 |
16 | // 参数绑定出错
17 | func BindError(err error) error {
18 | return gerrors.BadRequest(10001, err.Error(), err)
19 | }
20 |
21 | func BadRequest(msg string, err ...error) error {
22 | return gerrors.BadRequest(10002, msg, err...)
23 | }
24 |
25 | func InternalServerError(msg string, err ...error) error {
26 | return gerrors.InternalServerError(10003, msg, err...)
27 | }
28 |
29 | func Unauthorized(message ...string) error {
30 | var msg string
31 | if len(message) == 0 {
32 | msg = "请先登录"
33 | } else {
34 | msg = message[0]
35 | }
36 | return gerrors.Unauthorized(10004, msg, nil)
37 | }
38 |
39 | // NotFound generates a 404 error.
40 | func NotFound(message string, err ...error) error {
41 | return gerrors.NotFound(10005, message, err...)
42 | }
43 |
44 | // 记录不存在
45 | func RecordNotFound(message string) error {
46 | return gerrors.NotFound(10006, message, nil)
47 | }
48 |
49 | // 文件已存在
50 | func FileAlreadyExist(message ...string) error {
51 | var msg string
52 | if len(message) == 0 {
53 | msg = "文件已存在"
54 | } else {
55 | msg = message[0]
56 | }
57 | return gerrors.New(10007, 400, msg, nil)
58 | }
59 |
60 | // 没有权限
61 | func Forbidden(msg string, err ...error) error {
62 | return gerrors.Forbidden(10008, msg, err...)
63 | }
64 |
65 | func ErrAccountAlreadyExisted() error {
66 | return gerrors.BadRequest(10009, "account already existed", nil)
67 | }
68 |
69 | func ErrPassword() error {
70 | return gerrors.BadRequest(10010, "密码错误", nil)
71 | }
72 |
73 | func ErrAccountNotFound() error {
74 | return gerrors.NotFound(10011, "账号不存在", nil)
75 | }
76 |
77 | func UserIsBanned(message ...string) error {
78 | var msg string
79 | if len(message) == 0 {
80 | msg = "此用户已禁用"
81 | } else {
82 | msg = message[0]
83 | }
84 | return gerrors.Forbidden(10012, msg, nil)
85 | }
86 |
87 | func UserNotAllowBeBan(message ...string) error {
88 | var msg string
89 | if len(message) == 0 {
90 | msg = "不允许 ban 该用户"
91 | } else {
92 | msg = message[0]
93 | }
94 | return gerrors.Forbidden(10013, msg, nil)
95 | }
96 |
97 | func GroupNotAllowBeDelete(message ...string) error {
98 | var msg string
99 | if len(message) == 0 {
100 | msg = "不允许删除该组"
101 | } else {
102 | msg = message[0]
103 | }
104 | return gerrors.Forbidden(10013, msg, nil)
105 | }
106 |
--------------------------------------------------------------------------------
/example.go:
--------------------------------------------------------------------------------
1 | package main
2 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/wq1019/cloud_disk
2 |
3 | require (
4 | github.com/NetEase-Object-Storage/nos-golang-sdk v0.0.0-20171031020902-cc8892cb2b05
5 | github.com/emirpasic/gods v1.12.0
6 | github.com/gin-gonic/gin v1.9.1
7 | github.com/go-redis/redis v6.15.1+incompatible
8 | github.com/jinzhu/gorm v1.9.2
9 | github.com/joho/godotenv v1.3.0
10 | github.com/micro/go-config v0.13.2
11 | github.com/minio/minio-go v6.0.13+incompatible
12 | github.com/rs/cors v1.6.0
13 | github.com/satori/go.uuid v1.2.0
14 | github.com/spf13/afero v1.2.0
15 | github.com/swaggo/gin-swagger v1.0.0
16 | github.com/swaggo/swag v1.4.0
17 | github.com/urfave/cli v1.20.0
18 | github.com/vmihailenco/msgpack v4.0.1+incompatible
19 | github.com/wq1019/go-file-uploader v1.0.6
20 | github.com/wq1019/go-image_uploader v1.0.1
21 | github.com/zm-dev/gerrors v0.0.4
22 | go.uber.org/zap v1.9.1
23 | golang.org/x/crypto v0.26.0
24 | )
25 |
26 | require (
27 | github.com/BurntSushi/toml v0.3.1 // indirect
28 | github.com/PuerkitoBio/purell v1.1.0 // indirect
29 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
30 | github.com/bitly/go-simplejson v0.5.0 // indirect
31 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
32 | github.com/bytedance/sonic v1.9.1 // indirect
33 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
34 | github.com/denisenkom/go-mssqldb v0.12.3 // indirect
35 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
36 | github.com/fsnotify/fsnotify v1.4.9 // indirect
37 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
38 | github.com/ghodss/yaml v1.0.0 // indirect
39 | github.com/gin-contrib/sse v0.1.0 // indirect
40 | github.com/go-ini/ini v1.41.0 // indirect
41 | github.com/go-openapi/jsonpointer v0.17.0 // indirect
42 | github.com/go-openapi/jsonreference v0.18.0 // indirect
43 | github.com/go-openapi/spec v0.18.0 // indirect
44 | github.com/go-openapi/swag v0.17.0 // indirect
45 | github.com/go-playground/locales v0.14.1 // indirect
46 | github.com/go-playground/universal-translator v0.18.1 // indirect
47 | github.com/go-playground/validator/v10 v10.14.0 // indirect
48 | github.com/go-sql-driver/mysql v1.4.1 // indirect
49 | github.com/goccy/go-json v0.10.2 // indirect
50 | github.com/gofrs/uuid v4.4.0+incompatible // indirect
51 | github.com/golang/protobuf v1.5.0 // indirect
52 | github.com/hashicorp/hcl v1.0.0 // indirect
53 | github.com/imdario/mergo v0.3.6 // indirect
54 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
55 | github.com/jinzhu/now v1.1.5 // indirect
56 | github.com/json-iterator/go v1.1.12 // indirect
57 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect
58 | github.com/kr/pretty v0.3.1 // indirect
59 | github.com/leodido/go-urn v1.2.4 // indirect
60 | github.com/lib/pq v1.10.9 // indirect
61 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 // indirect
62 | github.com/mattn/go-isatty v0.0.19 // indirect
63 | github.com/mitchellh/go-homedir v1.0.0 // indirect
64 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
65 | github.com/modern-go/reflect2 v1.0.2 // indirect
66 | github.com/onsi/ginkgo v1.16.5 // indirect
67 | github.com/onsi/gomega v1.34.2 // indirect
68 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect
69 | github.com/pkg/errors v0.8.1 // indirect
70 | github.com/smartystreets/goconvey v1.8.1 // indirect
71 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
72 | github.com/ugorji/go/codec v1.2.11 // indirect
73 | go.uber.org/atomic v1.3.2 // indirect
74 | go.uber.org/multierr v1.1.0 // indirect
75 | golang.org/x/arch v0.3.0 // indirect
76 | golang.org/x/net v0.28.0 // indirect
77 | golang.org/x/sys v0.24.0 // indirect
78 | golang.org/x/text v0.17.0 // indirect
79 | golang.org/x/tools v0.24.0 // indirect
80 | google.golang.org/appengine v1.4.0 // indirect
81 | google.golang.org/protobuf v1.34.1 // indirect
82 | gopkg.in/ini.v1 v1.67.0 // indirect
83 | gopkg.in/yaml.v2 v2.4.0 // indirect
84 | gopkg.in/yaml.v3 v3.0.1 // indirect
85 | )
86 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
2 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
3 | github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
4 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
6 | github.com/NetEase-Object-Storage/nos-golang-sdk v0.0.0-20171031020902-cc8892cb2b05 h1:NEPjpPSOSDDmnix+VANw/CfUs1fAorLIaz/IFz2eQ2o=
7 | github.com/NetEase-Object-Storage/nos-golang-sdk v0.0.0-20171031020902-cc8892cb2b05/go.mod h1:0N5CbwYI/8V1T6YOEwkgMvLmiGDNn661vLutBZQrC2c=
8 | github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
9 | github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
10 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
11 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
12 | github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
13 | github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
14 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
15 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
16 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
17 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
18 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
19 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
20 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
21 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
22 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
26 | github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
27 | github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
28 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
29 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
30 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
31 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
32 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
33 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
34 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
35 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
36 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
37 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
38 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
39 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
40 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
41 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
42 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
43 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
44 | github.com/go-ini/ini v1.41.0 h1:526aoxDtxRHFQKMZfcX2OG9oOI8TJ5yPLM0Mkno/uTY=
45 | github.com/go-ini/ini v1.41.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
46 | github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
47 | github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
48 | github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
49 | github.com/go-openapi/jsonreference v0.18.0 h1:oP2OUNdG1l2r5kYhrfVMXO54gWmzcfAwP/GFuHpNTkE=
50 | github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
51 | github.com/go-openapi/spec v0.18.0 h1:aIjeyG5mo5/FrvDkpKKEGZPmF9MPHahS72mzfVqeQXQ=
52 | github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
53 | github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
54 | github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
55 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
56 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
57 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
58 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
59 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
60 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
61 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
62 | github.com/go-redis/redis v6.15.1+incompatible h1:BZ9s4/vHrIqwOb0OPtTQ5uABxETJ3NRuUNoSUurnkew=
63 | github.com/go-redis/redis v6.15.1+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
64 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
65 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
66 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
67 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
68 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
69 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
70 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
71 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
72 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
73 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
74 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
75 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
76 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
77 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
78 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
79 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
80 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
81 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
82 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
83 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
84 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
85 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
86 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
87 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
88 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
89 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
90 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
91 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
92 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
93 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
94 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
95 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
96 | github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw=
97 | github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
98 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
99 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
100 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
101 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
102 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
103 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
104 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
105 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
106 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
107 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
108 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
109 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
110 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
111 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
112 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
113 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
114 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
115 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
116 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
117 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
118 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
119 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
120 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
121 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
122 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
123 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
124 | github.com/micro/go-config v0.13.2 h1:yxJw8ghIUaydOMrO0QQTcGf8D0QZmPklcTErA8Oi4m0=
125 | github.com/micro/go-config v0.13.2/go.mod h1:fVecLls1kW+EJsrlkJYqUmVoJa1epSHhsPMDXppELx0=
126 | github.com/minio/minio-go v6.0.12+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8=
127 | github.com/minio/minio-go v6.0.13+incompatible h1:SQmjauWGQx5/x2TX47GBeX9xFVEuGB+RJGAVuZzNPtM=
128 | github.com/minio/minio-go v6.0.13+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8=
129 | github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
130 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
131 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
132 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
133 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
134 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
135 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
136 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
137 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
138 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
139 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
140 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
141 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
142 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
143 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
144 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
145 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
146 | github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
147 | github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
148 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
149 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
150 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
151 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
152 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
153 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
154 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
155 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
156 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
157 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
158 | github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
159 | github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
160 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
161 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
162 | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
163 | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
164 | github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
165 | github.com/spf13/afero v1.2.0 h1:O9FblXGxoTc51M+cqr74Bm2Tmt4PvkA5iu/j8HrkNuY=
166 | github.com/spf13/afero v1.2.0/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
167 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
168 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
169 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
170 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
171 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
172 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
173 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
174 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
175 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
176 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
177 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
178 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
179 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
180 | github.com/swaggo/gin-swagger v1.0.0 h1:k6Nn1jV49u+SNIWt7kejQS/iENZKZVMCNQrKOYatNF8=
181 | github.com/swaggo/gin-swagger v1.0.0/go.mod h1:Mt37wE46iUaTAOv+HSnHbJYssKGqbS25X19lNF4YpBo=
182 | github.com/swaggo/swag v1.4.0 h1:exX5ES4CdJWCCKmVPE+FAIN66cnHeMHU3i2SCMibBZc=
183 | github.com/swaggo/swag v1.4.0/go.mod h1:hog2WgeMOrQ/LvQ+o1YGTeT+vWVrbi0SiIslBtxKTyM=
184 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
185 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
186 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
187 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
188 | github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
189 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
190 | github.com/vmihailenco/msgpack v4.0.1+incompatible h1:RMF1enSPeKTlXrXdOcqjFUElywVZjjC6pqse21bKbEU=
191 | github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
192 | github.com/wq1019/go-file-uploader v1.0.1/go.mod h1:oJAY5hVNtRqzJq8Qb2eJj3xKZCXNKk5c15HdOGH+7fQ=
193 | github.com/wq1019/go-file-uploader v1.0.6 h1:xNyh+2L7O3hymtPxrhKzqmF/pO9Rpr/0UrTB1YqdoXg=
194 | github.com/wq1019/go-file-uploader v1.0.6/go.mod h1:oJAY5hVNtRqzJq8Qb2eJj3xKZCXNKk5c15HdOGH+7fQ=
195 | github.com/wq1019/go-image_uploader v1.0.1 h1:lTP5fy3msZUl36j9P+FugJzhoDNL/z9MYyHBuGsVawc=
196 | github.com/wq1019/go-image_uploader v1.0.1/go.mod h1:BVkVnkmunjR4VUcGY2PsXjz2pPxuUOWWQim5k8I7kvg=
197 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
198 | github.com/zm-dev/gerrors v0.0.4 h1:xQVI2+TOeS9VgeR7Dod3VM8tkoXsNiNPawYBsBHUdcg=
199 | github.com/zm-dev/gerrors v0.0.4/go.mod h1:HdV0W28lrWTROw4oAX6NK48lgyFmUVV6sf3Zp78TFow=
200 | go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
201 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
202 | go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
203 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
204 | go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
205 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
206 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
207 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
208 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
209 | golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
210 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
211 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
212 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
213 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
214 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
215 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
216 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
217 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
218 | golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
219 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
220 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
221 | golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
222 | golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
223 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
224 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
225 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
226 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
227 | golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
228 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
229 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
230 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
231 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
232 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
233 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
234 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
235 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
236 | golang.org/x/sys v0.0.0-20190114130336-2be517255631/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
237 | golang.org/x/sys v0.0.0-20190115152922-a457fd036447/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
238 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
239 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
240 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
241 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
242 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
243 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
244 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
245 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
246 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
247 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
248 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
249 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
250 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
251 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
252 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
253 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
254 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
255 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
256 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
257 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
258 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
259 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
260 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
261 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
262 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
263 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
264 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
265 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
266 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
267 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
268 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
269 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
270 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
271 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
272 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
273 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
274 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
275 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
276 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
277 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
278 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
279 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
280 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
281 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
282 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
283 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
284 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
285 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
286 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
287 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
288 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
289 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
290 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
291 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
292 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
293 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
294 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
295 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
296 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
297 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
298 |
--------------------------------------------------------------------------------
/handler/auth.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/cloud_disk/model"
7 | "github.com/wq1019/cloud_disk/service"
8 | "net/http"
9 | "strconv"
10 | "strings"
11 | "time"
12 | )
13 |
14 | type authHandler struct{}
15 |
16 | func (authHandler) Login(c *gin.Context) {
17 | req := &struct {
18 | Account string `form:"account" json:"account"`
19 | Password string `form:"password" json:"password"`
20 | }{}
21 | if err := c.ShouldBind(req); err != nil {
22 | _ = c.Error(errors.BindError(err))
23 | return
24 | }
25 | ticket, err := service.UserLogin(c.Request.Context(), strings.TrimSpace(req.Account), strings.TrimSpace(req.Password))
26 | if err != nil {
27 | _ = c.Error(err)
28 | return
29 | }
30 | setAuthCookie(c, ticket.Id, ticket.UserId, int(ticket.ExpiredAt.Sub(time.Now()).Seconds()))
31 | c.JSON(http.StatusNoContent, nil)
32 | }
33 |
34 | func (authHandler) Logout(c *gin.Context) {
35 | ticketId, err := c.Cookie("ticket_id")
36 | if err != nil {
37 | c.JSON(http.StatusNoContent, nil)
38 | return
39 | }
40 | removeAuthCookie(c)
41 | _ = service.TicketDestroy(c.Request.Context(), ticketId)
42 | c.JSON(http.StatusNoContent, nil)
43 | }
44 |
45 | func (authHandler) Register(c *gin.Context) {
46 | l := struct {
47 | Account string `form:"account" json:"account"`
48 | Password string `form:"password" json:"password"`
49 | }{}
50 | if err := c.ShouldBind(&l); err != nil {
51 | _ = c.Error(err)
52 | return
53 | }
54 | // 注册账号
55 | userId, err := service.UserRegister(c.Request.Context(), strings.TrimSpace(l.Account), model.CertificateType(0), l.Password)
56 | if err != nil {
57 | _ = c.Error(err)
58 | return
59 | }
60 | // 为新账号添加一个根目录
61 | err = service.CreateFolder(c.Request.Context(), &model.Folder{
62 | UserId: userId,
63 | Level: 1,
64 | ParentId: 0,
65 | Key: "",
66 | FolderName: "根目录",
67 | })
68 | if err != nil {
69 | _ = c.Error(err)
70 | return
71 | }
72 | c.Status(201)
73 | }
74 |
75 | func setAuthCookie(c *gin.Context, ticketId string, userId int64, maxAge int) {
76 | c.SetCookie("ticket_id", ticketId, maxAge, "", "", false, false)
77 | c.SetCookie("user_id", strconv.FormatInt(userId, 10), maxAge, "", "", false, false)
78 | }
79 |
80 | func removeAuthCookie(c *gin.Context) {
81 | c.SetCookie("ticket_id", "", -1, "", "", false, true)
82 | c.SetCookie("user_id", "", -1, "", "", false, false)
83 | }
84 |
85 | func NewAuthHandler() *authHandler {
86 | return &authHandler{}
87 | }
88 |
--------------------------------------------------------------------------------
/handler/download_file.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | "github.com/gin-gonic/gin"
6 | "github.com/wq1019/cloud_disk/errors"
7 | "github.com/wq1019/cloud_disk/handler/middleware"
8 | "github.com/wq1019/cloud_disk/model"
9 | "github.com/wq1019/cloud_disk/service"
10 | uploader "github.com/wq1019/go-file-uploader"
11 | "io"
12 | "net/http"
13 | "net/url"
14 | "strconv"
15 | "strings"
16 | "time"
17 | )
18 |
19 | type downloadHandler struct {
20 | u uploader.Uploader
21 | }
22 |
23 | type FolderData struct {
24 | Filename string
25 | Key string
26 | }
27 |
28 | func (d *downloadHandler) PreDownload(c *gin.Context) {
29 | folderIdStr, _ := c.GetQuery("current_folder_id")
30 | currentFolderId, err := strconv.ParseInt(strings.TrimSpace(folderIdStr), 10, 64)
31 | if err != nil || currentFolderId <= 0 {
32 | _ = c.Error(errors.BadRequest("请指定当前目录ID"))
33 | return
34 | }
35 | // 选中的文件
36 | fileIdsReq, _ := c.GetQueryArray("file_ids[]")
37 | // 选中的目录
38 | folderIdsReq, _ := c.GetQueryArray("folder_ids[]")
39 | if len(fileIdsReq) == 0 && len(folderIdsReq) == 0 {
40 | _ = c.Error(errors.BadRequest("请指定要下载的文件或者目录ID"))
41 | return
42 | }
43 | var (
44 | authId = middleware.UserId(c)
45 | fileIds2Int64 = strArr2Int64Arr(fileIdsReq)
46 | folderIds2Int64 = strArr2Int64Arr(folderIdsReq)
47 | foldersLen = len(folderIds2Int64)
48 | filesLen = len(fileIds2Int64)
49 | folderFiles = make([]*model.WrapFolderFile, 0, foldersLen+filesLen)
50 | )
51 | // 对于用户指定的所有目录下的文件都要查出来并返回
52 | if foldersLen > 0 {
53 | folderFiles, err = service.LoadFolderFilesByFolderIds(c.Request.Context(), folderIds2Int64, authId)
54 | if err != nil {
55 | _ = c.Error(err)
56 | return
57 | }
58 | }
59 | // 用户明确选中需要下载的文件(注: 就是当前目录下用户选中的文件)
60 | currentFolderFiles, err := service.LoadFolderFilesByFolderIdAndFileIds(c.Request.Context(), currentFolderId, fileIds2Int64, authId)
61 | if len(fileIds2Int64) > 0 {
62 | for _, v := range currentFolderFiles {
63 | folderFiles = append(folderFiles, v)
64 | }
65 | }
66 | if len(folderFiles) == 0 {
67 | _ = c.Error(errors.BadRequest("没有要下载的文件"))
68 | return
69 | }
70 |
71 | // Wrap Response Data
72 | var (
73 | folderIds = make([]int64, 0, 5)
74 | folderMaps = make(map[int64]FolderData, 10)
75 | )
76 | // 查找每个文件所在目录的信息
77 | folderIds = append(folderIds, currentFolderId)
78 | for _, v := range folderFiles {
79 | folderIds = append(folderIds, v.FolderId)
80 | }
81 | folders, err := service.ListFolder(c.Request.Context(), folderIds, authId)
82 | if err != nil {
83 | _ = c.Error(err)
84 | return
85 | }
86 | // 将目录的 id 与 name 写入 Map
87 | for _, v := range folders {
88 | folderMaps[v.Id] = FolderData{
89 | Filename: v.FolderName,
90 | Key: v.Key,
91 | }
92 | }
93 | for i := 0; i < len(folderFiles); i++ {
94 | relativePath := mergePath(folderMaps, currentFolderId, folderMaps[folderFiles[i].FolderId].Key, folderFiles[i].FolderId)
95 | folderFiles[i].RelativePath = relativePath
96 | }
97 | c.JSON(http.StatusOK, folderFiles)
98 | }
99 |
100 | func (d *downloadHandler) GetShareLink(c *gin.Context) {
101 | l := struct {
102 | FolderId int64 `json:"folder_id" form:"folder_id"`
103 | FileId int64 `json:"file_id" form:"file_id"`
104 | }{}
105 | if err := c.ShouldBind(&l); err != nil {
106 | _ = c.Error(err)
107 | return
108 | }
109 | authId := middleware.UserId(c)
110 | file, err := service.LoadFile(c.Request.Context(), l.FolderId, l.FileId, authId)
111 | if err != nil {
112 | _ = c.Error(err)
113 | return
114 | }
115 | u, err := d.u.PresignedGetObject(file.Hash, time.Second*1, url.Values{})
116 | if err != nil {
117 | _ = c.Error(err)
118 | return
119 | }
120 | // TODO 需要测试一下
121 | v := url.Values{}
122 | v.Add("download", file.Filename)
123 | body := v.Encode()
124 | c.JSON(http.StatusOK, gin.H{
125 | "status": http.StatusOK,
126 | "data": u.String() + "?" + body,
127 | })
128 | }
129 |
130 | // 文件下载
131 | // example:
132 | // curl -H "Range: bytes=0-12929" http://localhost:8080/api/download?folder_id=1\&file_id=3 -v --output 3.png
133 | func (d *downloadHandler) Download(c *gin.Context) {
134 | l := struct {
135 | FolderId int64 `json:"folder_id" form:"folder_id"`
136 | FileId int64 `json:"file_id" form:"file_id"`
137 | }{}
138 | if err := c.ShouldBind(&l); err != nil {
139 | _ = c.Error(err)
140 | return
141 | }
142 | authId := middleware.UserId(c)
143 | file, err := service.LoadFile(c.Request.Context(), l.FolderId, l.FileId, authId)
144 | if err != nil {
145 | _ = c.Error(err)
146 | return
147 | }
148 | c.Writer.Header().Add("Content-Disposition", "attachment;filename="+file.Filename)
149 | reqRange := c.Request.Header.Get("Range")
150 | if reqRange != "" {
151 | var (
152 | start int64 // not null
153 | end int64 // not null
154 | prefixIndex = strings.Index(reqRange, "-")
155 | )
156 | if prefixIndex == -1 {
157 | _ = c.Error(errors.BadRequest("Http range header error, not found prefix `-`"))
158 | return
159 | }
160 | start, err = strconv.ParseInt(reqRange[6:prefixIndex], 10, 64)
161 | if err != nil {
162 | _ = c.Error(err)
163 | return
164 | }
165 | if reqRange[prefixIndex+1:] == "" {
166 | _ = c.Error(errors.BadRequest("Http range header error, end value not exist."))
167 | return
168 | }
169 | end, err = strconv.ParseInt(reqRange[prefixIndex+1:], 10, 64)
170 | if err != nil {
171 | _ = c.Error(err)
172 | return
173 | }
174 | c.Writer.Header().Add("Accept-Ranges", "bytes")
175 | c.Writer.Header().Add("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, file.Size))
176 | // 这里必须要提前设置状态吗为 206 否则会 Warning https://github.com/gin-gonic/gin/issues/471#issuecomment-190186203
177 | c.Status(http.StatusPartialContent)
178 | rangeValue := fmt.Sprintf("bytes=%d-%d", start, end)
179 | readFile, err := d.u.ReadChunk(file.Hash, rangeValue)
180 | if err != nil {
181 | _ = c.Error(err)
182 | return
183 | }
184 | _, err = io.Copy(c.Writer, readFile)
185 | if err != nil {
186 | _ = c.Error(err)
187 | return
188 | }
189 | defer readFile.Close()
190 | } else {
191 | readFile, err := d.u.ReadFile(file.Hash)
192 | if err != nil {
193 | _ = c.Error(err)
194 | return
195 | }
196 | // 整个文件下载
197 | _, err = io.Copy(c.Writer, readFile)
198 | if err != nil {
199 | _ = c.Error(err)
200 | return
201 | }
202 | defer readFile.Close()
203 | c.Status(http.StatusOK)
204 | }
205 | }
206 |
207 | func mergePath(folderMap map[int64]FolderData, currentId int64, key string, withId int64) (path string) {
208 | if currentId == withId {
209 | return "./"
210 | }
211 | key2Arr := strings.Split(key, "-")
212 | for _, v := range key2Arr {
213 | id2Int64, _ := strconv.ParseInt(v, 10, 64)
214 | if id2Int64 > currentId {
215 | path += folderMap[id2Int64].Filename + "/"
216 | }
217 | }
218 | path += folderMap[withId].Filename
219 | return path
220 | }
221 |
222 | func strArr2Int64Arr(str []string) []int64 {
223 | var int64Arr []int64
224 | for _, v := range str {
225 | id, err := strconv.ParseInt(v, 10, 64)
226 | if err != nil {
227 | continue
228 | }
229 | int64Arr = append(int64Arr, id)
230 | }
231 | return int64Arr
232 | }
233 |
234 | func NewDownloadHandler(u uploader.Uploader) *downloadHandler {
235 | return &downloadHandler{u: u}
236 | }
237 |
--------------------------------------------------------------------------------
/handler/file.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/wq1019/cloud_disk/handler/middleware"
6 | "github.com/wq1019/cloud_disk/service"
7 | "net/http"
8 | )
9 |
10 | type fileHandler struct{}
11 |
12 | // RenameFile godoc
13 | // @Tags 文件
14 | // @Summary 重命名文件
15 | // @Description 通过文件 ID 重命名文件
16 | // @ID rename-file
17 | // @Accept json,multipart/form-data
18 | // @Produce json,multipart/form-data
19 | // @Param file_id query uint64 true "文件 ID" Format(uint64)
20 | // @Param folder_id query uint64 true "文件所属的目录 ID" Format(uint64)
21 | // @Param new_name query string true "新的文件名" Format(string)
22 | // @Success 204
23 | // @Failure 404 {object} errors.GlobalError "文件不存在 | 目录不存在"
24 | // @Failure 500 {object} errors.GlobalError
25 | // @Router /file/rename [PUT]
26 | func (*fileHandler) RenameFile(c *gin.Context) {
27 | l := struct {
28 | FileId int64 `json:"file_id" form:"file_id"`
29 | FolderId int64 `json:"folder_id" form:"folder_id"`
30 | NewName string `json:"new_name" form:"new_name"`
31 | }{}
32 | if err := c.ShouldBind(&l); err != nil {
33 | _ = c.Error(err)
34 | return
35 | }
36 | authId := middleware.UserId(c)
37 | folder, err := service.LoadFolder(c.Request.Context(), l.FolderId, authId, false)
38 | if err != nil {
39 | _ = c.Error(err)
40 | return
41 | }
42 | err = service.RenameFile(c.Request.Context(), folder.Id, l.FileId, l.NewName)
43 | if err != nil {
44 | _ = c.Error(err)
45 | return
46 | }
47 | c.Status(http.StatusNoContent)
48 | }
49 |
50 | func NewFileHandler() *fileHandler {
51 | return &fileHandler{}
52 | }
53 |
--------------------------------------------------------------------------------
/handler/folder.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "fmt"
5 | model2 "github.com/NetEase-Object-Storage/nos-golang-sdk/model"
6 | "github.com/NetEase-Object-Storage/nos-golang-sdk/nosclient"
7 | "github.com/gin-gonic/gin"
8 | "github.com/wq1019/cloud_disk/errors"
9 | "github.com/wq1019/cloud_disk/handler/middleware"
10 | "github.com/wq1019/cloud_disk/model"
11 | "github.com/wq1019/cloud_disk/service"
12 | "net/http"
13 | "strconv"
14 | )
15 |
16 | type folderHandler struct {
17 | nosClient *nosclient.NosClient
18 | bucketName string
19 | }
20 |
21 | // RenameFolder godoc
22 | // @Tags 目录
23 | // @Summary 重命名目录
24 | // @Description 通过目录 ID 重命名目录
25 | // @ID rename-folder
26 | // @Accept json,multipart/form-data
27 | // @Produce json,multipart/form-data
28 | // @Param folder_id query uint64 true "所属的目录 ID" Format(uint64)
29 | // @Param current_folder_id query uint64 true "当前目录 ID" Format(uint64)
30 | // @Param new_name query string true "新的目录名" Format(string)
31 | // @Success 204
32 | // @Failure 404 {object} errors.GlobalError "目录不存在"
33 | // @Failure 500 {object} errors.GlobalError
34 | // @Router /folder/rename [PUT]
35 | func (*folderHandler) RenameFolder(c *gin.Context) {
36 | l := struct {
37 | FolderId int64 `json:"folder_id" form:"folder_id"`
38 | NewName string `json:"new_name" form:"new_name"`
39 | CurrentFolderId int64 `json:"current_folder_id" form:"current_folder_id"`
40 | }{}
41 | if err := c.ShouldBind(&l); err != nil {
42 | _ = c.Error(err)
43 | return
44 | }
45 | authId := middleware.UserId(c)
46 | folder, err := service.LoadFolder(c.Request.Context(), l.FolderId, authId, false)
47 | if err != nil {
48 | _ = c.Error(err)
49 | return
50 | }
51 | err = service.RenameFolder(c.Request.Context(), folder.Id, l.CurrentFolderId, l.NewName)
52 | if err != nil {
53 | _ = c.Error(err)
54 | return
55 | }
56 | c.Status(http.StatusNoContent)
57 | }
58 |
59 | // LoadFolder godoc
60 | // @Tags 目录
61 | // @Summary 加载指定的目录及子目录和文件列表
62 | // @Description 加载指定的目录及子目录和文件列表
63 | // @ID load-folder
64 | // @Accept json,multipart/form-data
65 | // @Produce json,multipart/form-data
66 | // @Param folder_id query uint64 true "目录 ID" Format(uint64)
67 | // @Success 200 {object} model.Folder
68 | // @Failure 404 {object} errors.GlobalError "目录不存在 | 没有访问权限 | id 格式不正确"
69 | // @Failure 500 {object} errors.GlobalError
70 | // @Router /folder [GET]
71 | func (*folderHandler) LoadFolder(c *gin.Context) {
72 | l := struct {
73 | FolderId int64 `json:"folder_id" form:"folder_id"`
74 | }{}
75 | if err := c.ShouldBind(&l); err != nil {
76 | _ = c.Error(errors.BadRequest("id 格式不正确", err))
77 | return
78 | }
79 | authId := middleware.UserId(c)
80 | folder, err := service.LoadFolder(c.Request.Context(), l.FolderId, authId, true)
81 | if err != nil {
82 | _ = c.Error(err)
83 | return
84 | }
85 | if authId != folder.UserId {
86 | _ = c.Error(errors.Unauthorized("没有访问权限"))
87 | return
88 | }
89 | c.JSON(200, folder)
90 | }
91 |
92 | // CreateFolder godoc
93 | // @Tags 目录
94 | // @Summary 创建一个目录
95 | // @Description 创建一个目录
96 | // @ID create-folder
97 | // @Accept json,multipart/form-data
98 | // @Produce json,multipart/form-data
99 | // @Param parent_id query uint64 true "父级目录的 ID" Format(uint64)
100 | // @Param folder_name query string true "新目录的名称" Format(string)
101 | // @Success 201 {object} model.Folder
102 | // @Failure 404 {object} errors.GlobalError "目录名称不能为空 | (父)目录不存在 | 目录已经存在"
103 | // @Success 401 {object} errors.GlobalError "请先登录"
104 | // @Failure 500 {object} errors.GlobalError
105 | // @Router /folder [POST]
106 | func (*folderHandler) CreateFolder(c *gin.Context) {
107 | l := struct {
108 | ParentId int64 `json:"parent_id" form:"parent_id"`
109 | FolderName string `json:"folder_name" form:"folder_name"`
110 | }{}
111 | if err := c.ShouldBind(&l); err != nil {
112 | _ = c.Error(err)
113 | return
114 | }
115 | if l.FolderName == "" {
116 | _ = c.Error(errors.BadRequest("目录名称不能为空"))
117 | return
118 | }
119 | authId := middleware.UserId(c)
120 | parentFolder, err := service.LoadFolder(c.Request.Context(), l.ParentId, authId, false)
121 | if err != nil {
122 | _ = c.Error(err)
123 | return
124 | }
125 | isExist := service.ExistFolder(c.Request.Context(), authId, l.ParentId, l.FolderName)
126 | if isExist {
127 | _ = c.Error(errors.BadRequest("目录已经存在"))
128 | return
129 | }
130 | pId2String := strconv.FormatInt(parentFolder.Id, 10)
131 | folder := model.Folder{
132 | UserId: authId,
133 | Level: parentFolder.Level + 1,
134 | ParentId: l.ParentId,
135 | Key: parentFolder.Key + pId2String + model.FolderKeyPrefix,
136 | FolderName: l.FolderName,
137 | }
138 | err = service.CreateFolder(c.Request.Context(), &folder)
139 | if err != nil {
140 | _ = c.Error(err)
141 | return
142 | }
143 | c.JSON(http.StatusCreated, folder)
144 | }
145 |
146 | // DeleteSource godoc
147 | // @Tags 资源
148 | // @Summary 批量删除资源(文件/目录)
149 | // @Description 批量删除资源(文件/目录)
150 | // @ID delete-source
151 | // @Accept json
152 | // @Produce json
153 | // @Param current_folder_id query uint64 true "当前目录的 ID"
154 | // @Param file_ids query array false "要删除的文件 ids"
155 | // @Param folder_ids query array false "要删除的目录 ids"
156 | // @Success 204
157 | // @Failure 404 {object} errors.GlobalError "请指定要删除的文件或者目录ID | 当前目录不存在"
158 | // @Success 401 {object} errors.GlobalError "请先登录"
159 | // @Failure 500 {object} errors.GlobalError
160 | // @Router /folder [DELETE]
161 | func (f *folderHandler) DeleteSource(c *gin.Context) {
162 | l := struct {
163 | FileIds []int64 `json:"file_ids" form:"file_ids"`
164 | FolderIds []int64 `json:"folder_ids" form:"folder_ids"`
165 | CurrentFolderId int64 `json:"current_folder_id" form:"current_folder_id"`
166 | }{}
167 | if err := c.ShouldBind(&l); err != nil {
168 | _ = c.Error(err)
169 | return
170 | }
171 | if len(l.FileIds) == 0 && len(l.FolderIds) == 0 {
172 | _ = c.Error(errors.BadRequest("请指定要删除的文件或者目录ID"))
173 | return
174 | }
175 | deleteMultiObjects := model2.DeleteMultiObjects{
176 | Quiet: false, //详细和静默模式,设置为 true 的时候,只返回删除错误的文件列表,设置为 false 的时候,成功和失败的文件列表都返回
177 | }
178 | authId := middleware.UserId(c)
179 | // 删除指定的文件
180 | if len(l.FileIds) > 0 {
181 | // 判断当前目录有没有权限
182 | currentFolder, err := service.LoadFolder(c.Request.Context(), l.CurrentFolderId, authId, false)
183 | if err != nil {
184 | _ = c.Error(err)
185 | return
186 | }
187 | hashList, err := service.DeleteFile(c.Request.Context(), l.FileIds, currentFolder.Id)
188 | if err != nil {
189 | _ = c.Error(err)
190 | return
191 | }
192 | for _, hash := range hashList {
193 | deleteMultiObjects.Append(model2.DeleteObject{Key: hash[:2] + "/" + hash[2:]})
194 | }
195 | }
196 | // 删除目录列表
197 | if len(l.FolderIds) > 0 {
198 | hashList, err := service.DeleteFolder(c.Request.Context(), l.FolderIds, authId)
199 | if err != nil {
200 | _ = c.Error(err)
201 | return
202 | }
203 | for _, hash := range hashList {
204 | deleteMultiObjects.Append(model2.DeleteObject{Key: hash[:2] + "/" + hash[2:]})
205 | }
206 | }
207 |
208 | if len(deleteMultiObjects.Objects) > 0 {
209 | deleteRequest := &model2.DeleteMultiObjectsRequest{
210 | Bucket: f.bucketName,
211 | DelectObjects: &deleteMultiObjects,
212 | }
213 | _, err := f.nosClient.DeleteMultiObjects(deleteRequest)
214 | if err != nil {
215 | _ = c.Error(errors.BadRequest(fmt.Sprintf("删除文件失败: %+v", err), err))
216 | return
217 | }
218 | }
219 | c.Status(http.StatusNoContent)
220 | }
221 |
222 | func (*folderHandler) Move2Folder(c *gin.Context) {
223 | l := struct {
224 | FileIds []int64 `json:"file_ids" form:"file_ids"`
225 | FolderIds []int64 `json:"folder_ids" form:"folder_ids"`
226 | FromFolderId int64 `json:"from_folder_id" form:"from_folder_id"`
227 | ToFolderId int64 `json:"to_folder_id" form:"to_folder_id"`
228 | }{}
229 | if err := c.ShouldBind(&l); err != nil {
230 | _ = c.Error(err)
231 | return
232 | }
233 | if len(l.FileIds) == 0 && len(l.FolderIds) == 0 {
234 | _ = c.Error(errors.BadRequest("请指定要移动的文件或者目录ID"))
235 | return
236 | }
237 | if l.ToFolderId == 0 {
238 | _ = c.Error(errors.BadRequest("请指定移动到哪个目录"))
239 | return
240 | }
241 | if l.FromFolderId == l.ToFolderId {
242 | _ = c.Error(errors.BadRequest("当前文件夹和目的文件夹相等"))
243 | return
244 | }
245 | authId := middleware.UserId(c)
246 | fromFolder, err := service.LoadFolder(c.Request.Context(), l.FromFolderId, authId, false)
247 | if err != nil {
248 | _ = c.Error(err)
249 | return
250 | }
251 | toFolder, err := service.LoadFolder(c.Request.Context(), l.ToFolderId, authId, false)
252 | if err != nil {
253 | _ = c.Error(err)
254 | return
255 | }
256 | if fromFolder.UserId != authId || toFolder.UserId != authId {
257 | _ = c.Error(errors.Unauthorized("没有权限移动"))
258 | return
259 | }
260 | if len(l.FolderIds) > 0 {
261 | err := service.MoveFolder(c.Request.Context(), toFolder, l.FolderIds)
262 | if err != nil {
263 | _ = c.Error(err)
264 | return
265 | }
266 | }
267 | if len(l.FileIds) > 0 {
268 | err := service.MoveFile(c.Request.Context(), fromFolder.Id, toFolder.Id, l.FileIds)
269 | if err != nil {
270 | _ = c.Error(err)
271 | return
272 | }
273 | }
274 | c.Status(http.StatusOK)
275 | }
276 |
277 | func (*folderHandler) Copy2Folder(c *gin.Context) {
278 | l := struct {
279 | FileIds []int64 `json:"file_ids" form:"file_ids"`
280 | FolderIds []int64 `json:"folder_ids" form:"folder_ids"`
281 | ToFolderId int64 `json:"to_folder_id" form:"to_folder_id"`
282 | FromFolderId int64 `json:"from_folder_id" form:"from_folder_id"`
283 | }{}
284 | if err := c.ShouldBind(&l); err != nil {
285 | _ = c.Error(err)
286 | return
287 | }
288 | if len(l.FileIds) == 0 && len(l.FolderIds) == 0 {
289 | _ = c.Error(errors.BadRequest("请指定要复制的文件或者目录ID"))
290 | return
291 | }
292 | if l.ToFolderId == 0 {
293 | _ = c.Error(errors.BadRequest("请指定复制到哪个目录"))
294 | return
295 | }
296 | if l.FromFolderId == l.ToFolderId {
297 | _ = c.Error(errors.BadRequest("当前文件夹和目的文件夹相等"))
298 | return
299 | }
300 | var (
301 | totalFileSize uint64
302 | allowCopyFileIds []int64
303 | authId = middleware.UserId(c)
304 | )
305 | // 判断将要复制到的目录是否属于自己
306 | toFolder, err := service.LoadFolder(c.Request.Context(), l.ToFolderId, authId, false)
307 | if err != nil {
308 | _ = c.Error(err)
309 | return
310 | }
311 | // 判断指定的当前目录是否属于自己
312 | fromFolder, err := service.LoadFolder(c.Request.Context(), l.FromFolderId, authId, false)
313 | if err != nil {
314 | _ = c.Error(err)
315 | return
316 | }
317 | if toFolder.UserId != authId || fromFolder.UserId != authId {
318 | _ = c.Error(errors.Unauthorized("该目录没有权限复制"))
319 | return
320 | }
321 |
322 | if len(l.FileIds) > 0 {
323 | // 过滤出有权限复制的文件
324 | ownFiles, err := service.LoadFolderFilesByFolderIdAndFileIds(c.Request.Context(), l.FromFolderId, l.FileIds, authId)
325 | if err != nil {
326 | _ = c.Error(err)
327 | return
328 | }
329 | for _, file := range ownFiles {
330 | allowCopyFileIds = append(allowCopyFileIds, file.FileId)
331 | }
332 | // 复制当前目录指定的文件到指定目录
333 | if len(allowCopyFileIds) > 0 {
334 | totalSize, err := service.CopyFile(c.Request.Context(), l.FromFolderId, toFolder.Id, allowCopyFileIds)
335 | if err != nil {
336 | _ = c.Error(err)
337 | return
338 | }
339 | totalFileSize += totalSize
340 | }
341 | }
342 | if len(l.FolderIds) > 0 {
343 | // 过滤出有权限复制的目录
344 | ownFolders, err := service.ListFolder(c.Request.Context(), l.FolderIds, authId)
345 | if err != nil {
346 | _ = c.Error(err)
347 | return
348 | }
349 | // 复制指定的目录包括目录中的文件到指定位置
350 | if len(ownFolders) > 0 {
351 | totalSize, err := service.CopyFolder(c.Request.Context(), toFolder, ownFolders)
352 | if err != nil {
353 | _ = c.Error(err)
354 | return
355 | }
356 | totalFileSize += totalSize
357 | }
358 | }
359 | if totalFileSize > 0 {
360 | err = service.UserUpdateUsedStorage(c.Request.Context(), authId, totalFileSize, model.OperatorAdd)
361 | if err != nil {
362 | _ = c.Error(err)
363 | return
364 | }
365 | }
366 |
367 | c.Status(http.StatusOK)
368 | }
369 |
370 | func NewFolderHandler(client *nosclient.NosClient, bucketName string) *folderHandler {
371 | return &folderHandler{nosClient: client, bucketName: bucketName}
372 | }
373 |
--------------------------------------------------------------------------------
/handler/group.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/cloud_disk/model"
7 | "github.com/wq1019/cloud_disk/service"
8 | "net/http"
9 | "strconv"
10 | )
11 |
12 | type groupHandler struct {
13 | }
14 |
15 | func (g *groupHandler) GroupCreate(c *gin.Context) {
16 | l := struct {
17 | Name string `json:"name" form:"name"`
18 | MaxStorage uint64 `json:"max_storage" form:"max_storage"`
19 | AllowShare bool `json:"allow_share" form:"allow_share"`
20 | }{}
21 | if err := c.ShouldBind(&l); err != nil {
22 | _ = c.Error(errors.BindError(err))
23 | return
24 | }
25 | group := model.Group{
26 | Name: l.Name,
27 | MaxStorage: l.MaxStorage,
28 | AllowShare: l.AllowShare,
29 | }
30 | err := service.GroupCreate(c.Request.Context(), &group)
31 | if err != nil {
32 | _ = c.Error(err)
33 | return
34 | }
35 | c.JSON(http.StatusCreated, group)
36 | }
37 |
38 | func (g *groupHandler) GroupUpdate(c *gin.Context) {
39 | groupId, err := strconv.ParseInt(c.Param("id"), 10, 64)
40 | if err != nil {
41 | _ = c.Error(errors.BindError(err))
42 | return
43 | }
44 | if groupId <= 0 {
45 | _ = c.Error(model.ErrGroupNotExist)
46 | return
47 | }
48 | l := struct {
49 | Name string `json:"name" form:"name"`
50 | MaxStorage uint64 `json:"max_storage" form:"max_storage"`
51 | AllowShare bool `json:"allow_share" form:"allow_share"`
52 | }{}
53 | if err := c.ShouldBind(&l); err != nil {
54 | _ = c.Error(errors.BindError(err))
55 | return
56 | }
57 | err = service.GroupUpdate(c.Request.Context(), groupId, map[string]interface{}{
58 | "name": l.Name,
59 | "max_storage": l.MaxStorage,
60 | "allow_share": l.AllowShare,
61 | })
62 | if err != nil {
63 | _ = c.Error(err)
64 | return
65 | }
66 | c.Status(http.StatusCreated)
67 | }
68 |
69 | func (g *groupHandler) GroupList(c *gin.Context) {
70 | limit, offset := getInt64LimitAndOffset(c)
71 | groups, count, err := service.GroupList(c.Request.Context(), offset, limit)
72 | if err != nil {
73 | _ = c.Error(err)
74 | return
75 | }
76 | c.JSON(200, gin.H{
77 | "count": count,
78 | "data": groups,
79 | })
80 | }
81 |
82 | func (g *groupHandler) GroupDelete(c *gin.Context) {
83 | groupId, err := strconv.ParseInt(c.Param("id"), 10, 64)
84 | if err != nil {
85 | _ = c.Error(errors.BindError(err))
86 | return
87 | }
88 | err = service.GroupDelete(c.Request.Context(), groupId)
89 | if err != nil {
90 | _ = c.Error(err)
91 | return
92 | }
93 | c.Status(204)
94 | }
95 |
96 | func NewGroupHandler() *groupHandler {
97 | return &groupHandler{}
98 | }
99 |
--------------------------------------------------------------------------------
/handler/handler.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/swaggo/gin-swagger"
6 | "github.com/swaggo/gin-swagger/swaggerFiles"
7 | "github.com/wq1019/cloud_disk/handler/middleware"
8 | "github.com/wq1019/cloud_disk/server"
9 | "net/http"
10 | "strconv"
11 | )
12 |
13 | func CreateHTTPHandler(s *server.Server) http.Handler {
14 | authHandler := NewAuthHandler()
15 | meHandler := NewMeHandler(s.ImageUrl)
16 | userHandler := NewUserHandler(s.ImageUrl)
17 | uploadFileHandler := NewUploadFileHandler(s.FileUploader)
18 | uploadImageHandler := NewUploadImage(s.ImageUploader, s.ImageUrl)
19 | folderHandler := NewFolderHandler(s.NosClient, s.BucketName)
20 | fileHandler := NewFileHandler()
21 | downloadHandler := NewDownloadHandler(s.FileUploader)
22 | groupHandler := NewGroupHandler()
23 |
24 | if s.Debug {
25 | gin.SetMode(gin.DebugMode)
26 | } else {
27 | gin.SetMode(gin.ReleaseMode)
28 | }
29 |
30 | router := gin.Default()
31 | router.Use(middleware.Gorm(s.DB))
32 | router.Use(middleware.Service(s.Service))
33 | router.Use(middleware.NewHandleErrorMiddleware(s.Conf.ServiceName))
34 | api := router.Group("/api")
35 |
36 | authRouter := api.Group("/auth")
37 | authRouter.POST("/register", authHandler.Register)
38 | authRouter.POST("/login", authHandler.Login)
39 |
40 | authorized := api.Group("/")
41 | authorized.Use(middleware.AuthMiddleware)
42 | {
43 | // 显示我的基本信息
44 | authorized.GET("/auth/me", meHandler.Show)
45 | // 更新我的基本信息
46 | authorized.PUT("/auth/me", meHandler.UpdateInfo)
47 | // 退出登录
48 | authorized.GET("/auth/logout", authHandler.Logout)
49 | // 上传文件
50 | authorized.POST("/upload_file", uploadFileHandler.UploadChunk)
51 | // 上传图片
52 | authorized.POST("/upload_image", uploadImageHandler.UploadImage)
53 | // 指定目录下第一层的资源列表
54 | authorized.GET("/folder", folderHandler.LoadFolder)
55 | // 创建目录
56 | authorized.POST("/folder", folderHandler.CreateFolder)
57 | // 删除文件和目录资源
58 | authorized.DELETE("/source", folderHandler.DeleteSource)
59 | // 移动到指定目录
60 | authorized.PUT("/source/move", folderHandler.Move2Folder)
61 | // 复制到指定目录
62 | authorized.PUT("/source/copy", folderHandler.Copy2Folder)
63 | // 重命名文件
64 | authorized.PUT("/file/rename", fileHandler.RenameFile)
65 | // 重命名目录
66 | authorized.PUT("/folder/rename", folderHandler.RenameFolder)
67 | // 文件下载
68 | authorized.GET("/download", downloadHandler.Download)
69 | // 获取文件分享链接
70 | authorized.GET("/share_link", downloadHandler.GetShareLink)
71 | // 获取要下载的文件和目录的详细信息
72 | authorized.GET("/pre_download", downloadHandler.PreDownload)
73 | }
74 |
75 | adminRouter := api.Group("/admin")
76 | adminRouter.Use(middleware.AuthMiddleware, middleware.AdminMiddleware)
77 | {
78 | // 用户列表
79 | adminRouter.GET("/user", userHandler.UserList)
80 | // 更新用户的禁用状态
81 | adminRouter.PUT("/user/:id/ban_status", userHandler.UpdateBanStatus)
82 | // 组列表
83 | adminRouter.GET("/group", groupHandler.GroupList)
84 | // 创建组
85 | adminRouter.POST("/group", groupHandler.GroupCreate)
86 | // 更新组
87 | adminRouter.PUT("/group/:id", groupHandler.GroupUpdate)
88 | // 删除指定组
89 | adminRouter.DELETE("/group/:id", groupHandler.GroupDelete)
90 | }
91 |
92 | // 文档
93 | api.GET("/doc/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
94 | return router
95 | }
96 |
97 | func getInt32LimitAndOffset(c *gin.Context) (limit, offset int32) {
98 | var err error
99 | limitI64, err := strconv.ParseInt(c.Query("limit"), 10, 32)
100 | if err != nil {
101 | limit = 10
102 | } else {
103 | limit = int32(limitI64)
104 | }
105 | if limit > 50 {
106 | limit = 50
107 | }
108 |
109 | offsetI64, err := strconv.ParseInt(c.Query("offset"), 10, 32)
110 | if err != nil {
111 | offset = 0
112 | } else {
113 | offset = int32(offsetI64)
114 | }
115 | return limit, offset
116 | }
117 |
118 | func getInt64LimitAndOffset(c *gin.Context) (limit, offset int64) {
119 | var err error
120 | limit, err = strconv.ParseInt(c.Query("limit"), 10, 32)
121 | if err != nil {
122 | limit = 10
123 | }
124 | if limit > 50 {
125 | limit = 50
126 | }
127 |
128 | offset, err = strconv.ParseInt(c.Query("offset"), 10, 32)
129 | if err != nil {
130 | offset = 0
131 | }
132 | return limit, offset
133 | }
134 |
--------------------------------------------------------------------------------
/handler/me.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/cloud_disk/handler/middleware"
7 | "github.com/wq1019/cloud_disk/service"
8 | "github.com/wq1019/go-image_uploader/image_url"
9 | "net/http"
10 | )
11 |
12 | type meHandler struct {
13 | imageUrl image_url.URL
14 | }
15 |
16 | func (m *meHandler) Show(c *gin.Context) {
17 | uid := middleware.UserId(c)
18 | user, err := service.UserLoadAndRelated(c.Request.Context(), uid)
19 | if err != nil {
20 | _ = c.Error(err)
21 | return
22 | }
23 | c.JSON(http.StatusOK, convert2UserResp(user, m.imageUrl))
24 | }
25 |
26 | func (m *meHandler) UpdateInfo(c *gin.Context) {
27 | var authId = middleware.UserId(c)
28 | l := struct {
29 | Email string `json:"email" form:"email"`
30 | Profile string `json:"profile" form:"profile"`
31 | Nickname string `json:"nickname" form:"nickname"`
32 | AvatarHash string `json:"avatar_hash" form:"avatar_hash"`
33 | Gender int8 `json:"gender" form:"gender"`
34 | }{}
35 | if err := c.ShouldBind(&l); err != nil {
36 | _ = c.Error(errors.BindError(err))
37 | return
38 | }
39 | err := service.UserUpdate(c.Request.Context(), authId, map[string]interface{}{
40 | "nickname": l.Nickname,
41 | "avatar_hash": l.AvatarHash,
42 | "profile": l.Profile,
43 | "email": l.Email,
44 | "gender": l.Gender,
45 | })
46 | if err != nil {
47 | _ = c.Error(err)
48 | return
49 | }
50 | c.JSON(204, nil)
51 | }
52 |
53 | func NewMeHandler(imageUrl image_url.URL) *meHandler {
54 | return &meHandler{imageUrl: imageUrl}
55 | }
56 |
--------------------------------------------------------------------------------
/handler/middleware/auth.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/cloud_disk/model"
7 | "github.com/wq1019/cloud_disk/service"
8 | )
9 |
10 | var (
11 | isLoginKey = "is_login"
12 | userIdKey = "user_id"
13 | loggedUserKey = "logged_user"
14 | )
15 |
16 | func AuthMiddleware(c *gin.Context) {
17 | isLogin := check(c)
18 | if !isLogin {
19 | _ = c.Error(errors.Unauthorized())
20 | c.Abort()
21 | return
22 | }
23 | user := LoggedUser(c)
24 | if user.IsBan == true {
25 | _ = c.Error(errors.UserIsBanned())
26 | c.Abort()
27 | return
28 | }
29 | c.Next()
30 | }
31 |
32 | func AdminMiddleware(c *gin.Context) {
33 | // 必须是已登录状态
34 | user := LoggedUser(c)
35 | if user == nil || !user.IsAdmin {
36 | _ = c.Error(errors.Forbidden("没有权限.", nil))
37 | c.Abort()
38 | return
39 | }
40 | c.Next()
41 | }
42 |
43 | func check(c *gin.Context) bool {
44 | var (
45 | isLogin bool
46 | )
47 | if ticketId, err := c.Cookie("ticket_id"); err == nil {
48 | isValid, userId, err := service.TicketIsValid(c.Request.Context(), ticketId)
49 | if err == nil {
50 | isLogin = isValid
51 | setIsLogin(c, isLogin)
52 | setUserId(c, userId)
53 | }
54 | } else {
55 | // cookie不存在
56 | isLogin = false
57 | }
58 | return isLogin
59 | }
60 |
61 | func setIsLogin(c *gin.Context, isLogin bool) {
62 | c.Set(isLoginKey, isLogin)
63 | }
64 |
65 | func setUserId(c *gin.Context, userId int64) {
66 | c.Set(userIdKey, userId)
67 | }
68 |
69 | func CheckLogin(c *gin.Context) bool {
70 | isLogin, ok := c.Get(isLoginKey)
71 | if !ok {
72 | return check(c)
73 | }
74 | return isLogin.(bool)
75 |
76 | }
77 |
78 | func UserId(c *gin.Context) int64 {
79 | userId, ok := c.Get(userIdKey)
80 | if !ok {
81 | check(c)
82 | return c.GetInt64(userIdKey)
83 | }
84 | return userId.(int64)
85 | }
86 |
87 | func LoggedUser(c *gin.Context) *model.User {
88 | user, ok := c.Get(loggedUserKey)
89 | if !ok {
90 | userId := UserId(c)
91 | if userId == 0 {
92 | return nil
93 | }
94 | userModel, err := service.UserLoad(c.Request.Context(), userId)
95 | if err != nil {
96 | return nil
97 | }
98 | c.Set("loggedUserKey", userModel)
99 | return userModel
100 | }
101 | return user.(*model.User)
102 | }
103 |
--------------------------------------------------------------------------------
/handler/middleware/db_middleware.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/jinzhu/gorm"
6 | "github.com/wq1019/cloud_disk/store/db_store"
7 | )
8 |
9 | func Gorm(db *gorm.DB) gin.HandlerFunc {
10 | return func(c *gin.Context) {
11 | c.Request = c.Request.WithContext(db_store.NewDBContext(c.Request.Context(), db))
12 | c.Next()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/handler/middleware/handle_error.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/gin-gonic/gin"
6 | "github.com/jinzhu/gorm"
7 | "github.com/wq1019/cloud_disk/errors"
8 | "github.com/zm-dev/gerrors"
9 | )
10 |
11 | func NewHandleErrorMiddleware(serviceName string) gin.HandlerFunc {
12 | return func(c *gin.Context) {
13 | c.Next() // execute all the handlers
14 |
15 | // at this point, all the handlers finished. Let's read the errors!
16 | // in this example we only will use the **last error typed as public**
17 | // but you could iterate over all them since c.Errors is a slice!
18 | errorToPrint := c.Errors.Last()
19 | if errorToPrint != nil {
20 | var ge *gerrors.GlobalError
21 |
22 | switch errorToPrint.Err {
23 | case gorm.ErrRecordNotFound:
24 | ge = errors.NotFound(errorToPrint.Err.Error()).(*gerrors.GlobalError)
25 | default:
26 | ge = &gerrors.GlobalError{}
27 | if json.Unmarshal([]byte(errorToPrint.Err.Error()), ge) != nil {
28 | ge = errors.InternalServerError(errorToPrint.Err.Error(), errorToPrint.Err).(*gerrors.GlobalError)
29 | }
30 | }
31 |
32 | if ge.ServiceName == "" {
33 | ge.ServiceName = serviceName
34 | }
35 | c.JSON(ge.StatusCode, gin.H{
36 | "code": ge.Code,
37 | "message": ge.Message,
38 | })
39 | }
40 |
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/handler/middleware/pub_middleware.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/wq1019/cloud_disk/queue"
6 | )
7 |
8 | func Pub(pub queue.PubQueue) gin.HandlerFunc {
9 | return func(c *gin.Context) {
10 | c.Request = c.Request.WithContext(queue.NewContext(c.Request.Context(), pub))
11 | c.Next()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/handler/middleware/service_middleware.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/wq1019/cloud_disk/service"
6 | )
7 |
8 | func Service(svc service.Service) gin.HandlerFunc {
9 | return func(c *gin.Context) {
10 | c.Request = c.Request.WithContext(service.NewContext(c.Request.Context(), svc))
11 | c.Next()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/handler/upload_file.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/cloud_disk/handler/middleware"
7 | "github.com/wq1019/cloud_disk/model"
8 | "github.com/wq1019/cloud_disk/service"
9 | "github.com/wq1019/go-file-uploader"
10 | "net/http"
11 | )
12 |
13 | type uploadFile struct {
14 | u go_file_uploader.Uploader
15 | }
16 |
17 | type FormData struct {
18 | FolderId int64 `json:"folder_id" form:"folder_id"`
19 | ChunkIndex int `json:"chunk_index" form:"chunk_index"`
20 | TotalChunk int `json:"total_chunk" form:"total_chunk"`
21 | TotalSize int64 `json:"total_size" form:"total_size"`
22 | FileHash string `json:"file_hash" form:"file_hash"`
23 | IsLastChunk bool `json:"is_last_chunk" form:"is_last_chunk"`
24 | Filename string `json:"filename" form:"filename"`
25 | UploadId string `json:"upload_id" form:"upload_id"`
26 | }
27 |
28 | type UploadResponse struct {
29 | UploadId string `json:"upload_id"`
30 | ChunkIndex int `json:"chunk_index"`
31 | FileHash string `json:"file_hash"`
32 | StatusCode int `json:"status_code"`
33 | Message string `json:"message"`
34 | }
35 |
36 | const (
37 | ChunkMaxSize = 100 << 20 // 分片上传最大 100MB
38 | )
39 |
40 | func (uf *uploadFile) UploadChunk(c *gin.Context) {
41 | l := FormData{}
42 | if err := c.ShouldBind(&l); err != nil {
43 | _ = c.Error(errors.BindError(err))
44 | return
45 | }
46 | // 验证表单
47 | if ok, err := validForm(&l); !ok {
48 | _ = c.Error(err)
49 | return
50 | }
51 | var (
52 | authId = middleware.UserId(c) // 没必要每次都获取 authID, 第一次上传和最后一次上传时获取一下就可以
53 | err error // err
54 | )
55 | // 第一次上传或者最后一次上传时都检查有没有权限
56 | if l.ChunkIndex == 1 || l.IsLastChunk == true {
57 | // 判断用户有没有上传到该目录的权限
58 | folder, err := service.LoadSimpleFolder(c.Request.Context(), l.FolderId, authId)
59 | if err != nil {
60 | _ = c.Error(err)
61 | return
62 | }
63 | if authId != folder.UserId {
64 | _ = c.Error(errors.Unauthorized("该目录没有访问权限"))
65 | return
66 | }
67 | // 判断目录是否存在同名文件
68 | for _, file := range folder.Files {
69 | if file.Filename == l.Filename {
70 | _ = c.Error(errors.FileAlreadyExist("上传失败, 该目录下存在同名文件"))
71 | return
72 | }
73 | }
74 | }
75 | // 从 form-data 中获取数据块
76 | postChunkData, fh, err := c.Request.FormFile("file-data")
77 | if err != nil {
78 | _ = c.Error(errors.BadRequest("请上传文件", err))
79 | return
80 | }
81 | defer postChunkData.Close()
82 | if fh.Size > ChunkMaxSize {
83 | _ = c.Error(errors.BadRequest("上传失败, 数据块太大"))
84 | return
85 | }
86 | // 文件秒传
87 | exist, _ := uf.u.Store().FileExist(l.FileHash)
88 | if exist {
89 | file, err := uf.u.Store().FileLoad(l.FileHash)
90 | if err != nil {
91 | _ = c.Error(errors.BadRequest("文件秒传失败", err))
92 | return
93 | }
94 | fileModel := &model.File{}
95 | if file.Filename != l.Filename {
96 | file.Filename = l.Filename
97 | fileModel = convert2FileModel(file)
98 | } else {
99 | fileModel = convert2FileModel(file)
100 | }
101 | err = service.SaveFileToFolder(c.Request.Context(), fileModel, l.FolderId)
102 | if err != nil {
103 | _ = c.Error(err)
104 | return
105 | }
106 | // 更新用户已使用的空间
107 | err = service.UserUpdateUsedStorage(c.Request.Context(), authId, uint64(file.Size), model.OperatorAdd)
108 | if err != nil {
109 | _ = c.Error(err)
110 | return
111 | }
112 | c.JSON(http.StatusOK, UploadResponse{
113 | Message: "文件秒传成功",
114 | StatusCode: 1,
115 | })
116 | return
117 | } else {
118 | // 分片上传
119 | file, uploadId, err := uf.u.UploadChunk(go_file_uploader.ChunkHeader{
120 | ChunkNumber: l.ChunkIndex,
121 | UploadId: l.UploadId,
122 | OriginFilename: l.Filename,
123 | OriginFileHash: l.FileHash,
124 | OriginFileSize: l.TotalSize,
125 | IsLastChunk: l.IsLastChunk,
126 | ChunkContent: postChunkData,
127 | ChunkCount: l.TotalChunk,
128 | }, "")
129 | if err != nil {
130 | _ = c.Error(errors.BadRequest("分片上传失败", err))
131 | return
132 | }
133 | // 非最后一个数据块
134 | if l.IsLastChunk == false {
135 | c.JSON(http.StatusOK, UploadResponse{
136 | Message: "数据块上传成功",
137 | StatusCode: 2,
138 | UploadId: uploadId,
139 | ChunkIndex: l.ChunkIndex,
140 | FileHash: l.FileHash,
141 | })
142 | return
143 | } else {
144 | if file == nil {
145 | _ = c.Error(errors.BadRequest("文件上传失败, 所有数据块已经上传, 但是保存到数据库时可能出现了问题"))
146 | return
147 | }
148 | // 最后一个数据块上传完成后需要写入文件和目录的关系到数据库
149 | fileModel := &model.File{}
150 | if file.Filename != l.Filename {
151 | file.Filename = l.Filename
152 | fileModel = convert2FileModel(file)
153 | } else {
154 | fileModel = convert2FileModel(file)
155 | }
156 | err = service.SaveFileToFolder(c.Request.Context(), fileModel, l.FolderId)
157 | if err != nil {
158 | _ = c.Error(err)
159 | return
160 | }
161 | // 更新用户已使用的空间
162 | err = service.UserUpdateUsedStorage(c.Request.Context(), authId, uint64(file.Size), model.OperatorAdd)
163 | if err != nil {
164 | _ = c.Error(err)
165 | return
166 | }
167 | c.JSON(http.StatusOK, UploadResponse{
168 | Message: "文件上传成功",
169 | StatusCode: 0,
170 | UploadId: uploadId,
171 | ChunkIndex: l.ChunkIndex,
172 | FileHash: l.FileHash,
173 | })
174 | return
175 | }
176 | }
177 | }
178 |
179 | func validForm(l *FormData) (ok bool, err error) {
180 | ok = false
181 | err = nil
182 | if l.Filename == "" {
183 | err = errors.BadRequest("filename 不存在", nil)
184 | return
185 | }
186 | if l.FileHash == "" {
187 | err = errors.BadRequest("filehash 不存在", nil)
188 | return
189 | }
190 | if l.TotalSize <= 0 {
191 | err = errors.BadRequest("totalSize 必须大于 0", nil)
192 | return
193 | }
194 | // 验证传入的 chunk 是否合法
195 | if l.ChunkIndex > l.TotalChunk || l.ChunkIndex < 1 {
196 | err = errors.BadRequest("chunk 必须大于 0 小于等于 totalChunk", nil)
197 | return
198 | }
199 | if l.FolderId == 0 {
200 | err = errors.BadRequest("请指定上传的文件夹", nil)
201 | return
202 | }
203 | if l.ChunkIndex != 1 && l.UploadId == "" {
204 | err = errors.BadRequest("除第一次上传文件, 每次上传都要传 uploadId")
205 | return
206 | }
207 | return true, nil
208 | }
209 |
210 | func convert2FileModel(upload *go_file_uploader.FileModel) *model.File {
211 | return &model.File{
212 | Id: upload.Id,
213 | Hash: upload.Hash,
214 | Format: upload.Format,
215 | Filename: upload.Filename,
216 | Size: upload.Size,
217 | }
218 | }
219 |
220 | func NewUploadFileHandler(u go_file_uploader.Uploader) *uploadFile {
221 | return &uploadFile{u: u}
222 | }
223 |
--------------------------------------------------------------------------------
/handler/upload_image.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/go-image_uploader"
7 | "github.com/wq1019/go-image_uploader/image_url"
8 | )
9 |
10 | type uploadImage struct {
11 | u image_uploader.Uploader
12 | imageUrl image_url.URL
13 | }
14 |
15 | func (ui *uploadImage) UploadImage(c *gin.Context) {
16 | file, fh, err := c.Request.FormFile("image")
17 | if err != nil {
18 | _ = c.Error(errors.BadRequest("请上传图片", err))
19 | return
20 | }
21 | defer file.Close()
22 | image, err := ui.u.Upload(image_uploader.FileHeader{Filename: fh.Filename, Size: fh.Size, File: file})
23 |
24 | if err != nil {
25 | if image_uploader.IsUnknownFormat(err) {
26 | _ = c.Error(errors.BadRequest("不支持的图片类型", nil))
27 | return
28 | } else {
29 | _ = c.Error(errors.InternalServerError("图片上传失败", err))
30 | return
31 | }
32 | }
33 | u := ui.imageUrl.Generate(image.Hash)
34 |
35 | c.JSON(200, gin.H{
36 | "image_url": u,
37 | "image_hash": image.Hash,
38 | })
39 | }
40 |
41 | func NewUploadImage(u image_uploader.Uploader, imageUrl image_url.URL) *uploadImage {
42 | return &uploadImage{u: u, imageUrl: imageUrl}
43 | }
44 |
--------------------------------------------------------------------------------
/handler/user.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/cloud_disk/model"
7 | "github.com/wq1019/cloud_disk/service"
8 | "github.com/wq1019/go-image_uploader/image_url"
9 | "strconv"
10 | )
11 |
12 | type userHandler struct {
13 | imageUrl image_url.URL
14 | }
15 |
16 | func (*userHandler) UpdateBanStatus(c *gin.Context) {
17 | userId, err := strconv.ParseInt(c.Param("id"), 10, 64)
18 | if err != nil {
19 | _ = c.Error(errors.BindError(err))
20 | return
21 | }
22 | if userId <= 0 {
23 | _ = c.Error(errors.ErrAccountNotFound())
24 | return
25 | }
26 | l := struct {
27 | IsBan bool `json:"is_ban" form:"is_ban"`
28 | }{}
29 | if err := c.ShouldBind(&l); err != nil {
30 | _ = c.Error(errors.BindError(err))
31 | return
32 | }
33 | user, err := service.UserLoad(c.Request.Context(), userId)
34 | if user.IsAdmin == true {
35 | _ = c.Error(errors.UserNotAllowBeBan("管理员账号不允许被 ban"))
36 | return
37 | }
38 | err = service.UserUpdateBanStatus(c.Request.Context(), userId, l.IsBan)
39 | if err != nil {
40 | _ = c.Error(err)
41 | return
42 | }
43 | c.Status(204)
44 | }
45 |
46 | func (u *userHandler) UserList(c *gin.Context) {
47 | limit, offset := getInt64LimitAndOffset(c)
48 | users, count, err := service.UserList(c.Request.Context(), offset, limit)
49 | if err != nil {
50 | _ = c.Error(err)
51 | return
52 | }
53 | c.JSON(200, gin.H{
54 | "count": count,
55 | "data": convert2UserListResp(users, u.imageUrl),
56 | })
57 | }
58 |
59 | func convert2UserListResp(users []*model.User, imageUrl image_url.URL) []map[string]interface{} {
60 | userList := make([]map[string]interface{}, 0, len(users))
61 | for _, v := range users {
62 | userList = append(userList, convert2UserResp(v, imageUrl))
63 | }
64 | return userList
65 | }
66 |
67 | func convert2UserResp(user *model.User, imageUrl image_url.URL) map[string]interface{} {
68 | var gender string
69 | if user.Gender {
70 | gender = "男"
71 | } else {
72 | gender = "女"
73 | }
74 | return map[string]interface{}{
75 | "id": user.Id,
76 | "name": user.Name,
77 | "email": user.Email,
78 | "gender": gender,
79 | "profile": user.Profile,
80 | "nickname": user.Nickname,
81 | "created_at": user.CreatedAt,
82 | "updated_at": user.UpdatedAt,
83 | "avatar_url": imageUrl.Generate(user.AvatarHash),
84 | "group_name": user.Group.Name,
85 | "avatar_hash": user.AvatarHash,
86 | "used_storage": user.UsedStorage,
87 | "is_allow_share": user.Group.AllowShare,
88 | "max_allow_storage": user.Group.MaxStorage,
89 | //"used_storage": bytesize.ByteSize(user.UsedStorage),
90 | //"max_allow_storage": bytesize.ByteSize(user.Group.MaxStorage),
91 | }
92 | }
93 |
94 | func NewUserHandler(imageUrl image_url.URL) *userHandler {
95 | return &userHandler{imageUrl: imageUrl}
96 | }
97 |
--------------------------------------------------------------------------------
/model/certificate.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | type CertificateType uint8
8 |
9 | const (
10 | CertificateUserName CertificateType = iota
11 | CertificatePhoneNum
12 | CertificateEmail
13 | )
14 |
15 | type Certificate struct {
16 | Id int64 `gorm:"type:BIGINT AUTO_INCREMENT;PRIMARY_KEY;NOT NULL"`
17 | UserId int64 `gorm:"type:BIGINT;INDEX"`
18 | Account string `gorm:"NOT NULL;UNIQUE"`
19 | Type CertificateType `gorm:"type:TINYINT"`
20 | }
21 |
22 | type CertificateStore interface {
23 | CertificateExist(account string) (bool, error)
24 | CertificateLoadByAccount(account string) (*Certificate, error)
25 | CertificateIsNotExistErr(error) bool
26 | CertificateCreate(certificate *Certificate) error
27 | CertificateUpdate(oldAccount, newAccount string, certificateType CertificateType) error
28 | }
29 |
30 | var ErrCertificateNotExist = errors.New("certificate not exist")
31 |
32 | func CertificateIsNotExistErr(err error) bool {
33 | return err == ErrCertificateNotExist
34 | }
35 |
36 | type CertificateService interface {
37 | CertificateStore
38 | }
39 |
--------------------------------------------------------------------------------
/model/file.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type File struct {
8 | Id int64 `gorm:"type:BIGINT AUTO_INCREMENT;PRIMARY_KEY;NOT NUll" json:"id"` // ID
9 | Filename string `gorm:"type:char(255); NOT NULL" json:"filename"` // 文件名称
10 | Hash string `gorm:"type:varchar(32);INDEX;NOT NULL" json:"hash"` // 文件Hash
11 | Format string `gorm:"type:varchar(255);NOT NULL" json:"format"` // 文件MimeType 例如: video/mp4 -> .mp4
12 | Extra string `gorm:"NOT NULL;type:TEXT" json:"extra"` // extra
13 | Size int64 `gorm:"type:BIGINT" json:"size"` // 文件大小
14 | CreatedAt time.Time `json:"created_at"` // 创建时间
15 | UpdatedAt time.Time `json:"updated_at"` // 更新时间
16 | }
17 |
18 | type FileStore interface {
19 | // 保存文件到指定目录
20 | SaveFileToFolder(file *File, folderId int64) (err error)
21 | // 删除文件和目录之间的关联 返回允许删除的文件 Hash 列表
22 | DeleteFile(ids []int64, folderId int64) (allowDelFileHashList []string, err error)
23 | // 移动文件
24 | MoveFile(fromId, toId int64, fileIds []int64) (err error)
25 | // 复制文件
26 | CopyFile(fromId, toId int64, fileIds []int64) (totalSize uint64, err error)
27 | // 重命名文件
28 | RenameFile(folderId, fileId int64, newName string) (err error)
29 | // 加载文件
30 | LoadFile(folderId, fileId, userId int64) (file *File, err error)
31 | //GetHashByFileIds()
32 | }
33 |
34 | type FileService interface {
35 | FileStore
36 | }
37 |
--------------------------------------------------------------------------------
/model/folder.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "errors"
5 | "time"
6 | )
7 |
8 | type Folder struct {
9 | Id int64 `gorm:"type:BIGINT AUTO_INCREMENT;PRIMARY_KEY;NOT NUll" json:"id"` // ID
10 | FolderName string `gorm:"type:varchar(255)" json:"folder_name"` // 目录名称
11 | ParentId int64 `gorm:"type:BIGINT;default:0" json:"parent_id"` // 父目录
12 | UserId int64 `gorm:"type:BIGINT;index:user_id" json:"user_id"` // 创建者
13 | Key string `gorm:"type:varchar(255);default:''" json:"key"` // 辅助键
14 | Level int64 `gorm:"type:INT;default:1" json:"level"` // 辅助键
15 | Files []*File `json:"files"` // many2many
16 | Folders []*Folder `gorm:"foreignkey:ParentId" json:"folders"` // one2many 当前目录下的目录
17 | CreatedAt time.Time `json:"created_at"` // 创建时间
18 | UpdatedAt time.Time `json:"updated_at"` // 更新时间
19 | }
20 |
21 | type SimpleFile struct {
22 | Id int64 `json:"id"`
23 | Filename string `json:"filename"`
24 | }
25 | type SimpleFolder struct {
26 | Id int64 `json:"id"`
27 | FolderName string `json:"folder_name"`
28 | ParentId int64 `json:"parent_id"`
29 | UserId int64 `json:"user_id"`
30 | Files []*SimpleFile `json:"files"`
31 | CreatedAt time.Time `json:"created_at"`
32 | UpdatedAt time.Time `json:"updated_at"`
33 | }
34 |
35 | const (
36 | FolderKeyPrefix = "-"
37 | )
38 |
39 | var FolderAlreadyExisted = errors.New("该目录已经存在")
40 |
41 | type FolderStore interface {
42 | // 创建一个目录
43 | CreateFolder(folder *Folder) (err error)
44 | // 目录是否存在
45 | ExistFolder(userId, parentId int64, folderName string) (isExist bool)
46 | // 当 id != 0 则表示加载指定目录, 当 id == 0 则表示加载根目录
47 | LoadFolder(id, userId int64, isLoadRelated bool) (folder *Folder, err error)
48 | // 只加载目录和下面的文件
49 | LoadSimpleFolder(id, userId int64) (folder *SimpleFolder, err error)
50 | // 删除指定目录
51 | DeleteFolder(ids []int64, userId int64) (allowDelFileHashList []string, err error)
52 | // 移动目录
53 | MoveFolder(to *Folder, ids []int64) (err error)
54 | // 复制目录
55 | CopyFolder(to *Folder, foders []*Folder) (totalSize uint64, err error)
56 | // 重命名目录
57 | RenameFolder(id, currentFolderId int64, newName string) (err error)
58 | // 目录列表
59 | ListFolder(folderIds []int64, userId int64) (folder []*Folder, err error)
60 | }
61 |
62 | type FolderService interface {
63 | FolderStore
64 | }
65 |
--------------------------------------------------------------------------------
/model/folder_file.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // 次序千万不能更改,否则 gorm 的 select 就不能用了
4 | type FolderFile struct {
5 | FolderId int64 `gorm:"type:BIGINT;NOT NUll" json:"folder_id"`
6 | OriginFileId int64 `gorm:"type:BIGINT;NOT NUll" json:"origin_file_id"`
7 | Filename string `gorm:"type:varchar(255);NOT NULL" json:"filename"`
8 | FileId int64 `gorm:"type:BIGINT AUTO_INCREMENT;PRIMARY_KEY;NOT NUll" json:"file_id"`
9 | }
10 |
11 | type WrapFolderFile struct {
12 | FileId int64 `json:"file_id"`
13 | FolderId int64 `json:"folder_id"`
14 | FileSize int64 `json:"file_size"`
15 | Filename string `json:"filename"`
16 | Format string `json:"format"`
17 | RelativePath string `json:"relative_path"`
18 | }
19 |
20 | func (*FolderFile) TableName() string {
21 | return "folder_files"
22 | }
23 |
24 | type FolderFileStore interface {
25 | // 加载指定目录的文件s
26 | LoadFolderFilesByFolderIds(folderIds []int64, userId int64) (folderFiles []*WrapFolderFile, err error)
27 | // 加载指定目录的指定文件s的详细信息
28 | LoadFolderFilesByFolderIdAndFileIds(folderId int64, fileIds []int64, userId int64) (folderFiles []*WrapFolderFile, err error)
29 | // 是否存在
30 | ExistFile(filename string, folderId, userId int64) (isExist bool, err error)
31 | }
32 |
33 | type FolderFileService interface {
34 | FolderFileStore
35 | }
36 |
--------------------------------------------------------------------------------
/model/group.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "errors"
5 | "time"
6 | )
7 |
8 | type Group struct {
9 | Id int64 `gorm:"type:BIGINT AUTO_INCREMENT;PRIMARY_KEY;NOT NUll" json:"id"` // ID
10 | Name string `gorm:"type:varchar(32)" json:"name"` // 组名
11 | MaxStorage uint64 `gorm:"type:BIGINT" json:"max_storage"` // 最大容量/KB 默认1TB
12 | AllowShare bool `gorm:"type:TINYINT;default:1" json:"allow_share"` // 是否允许分享文件
13 | Users []*User `json:"users,omitempty"` // 用户列表
14 | CreatedAt time.Time `json:"created_at"` // 创建时间
15 | UpdatedAt time.Time `json:"updated_at"` // 更新时间
16 | }
17 |
18 | type WrapGroupList struct {
19 | Id int64 `json:"id"`
20 | Name string `json:"name"`
21 | MaxStorage uint64 `json:"max_storage"`
22 | AllowShare bool `json:"allow_share"`
23 | UserCount int64 `json:"user_count"`
24 | CreatedAt time.Time `json:"created_at"`
25 | UpdatedAt time.Time `json:"updated_at"`
26 | }
27 |
28 | var ErrGroupNotExist = errors.New("group not exist")
29 | var ErrGroupAlreadyExist = errors.New("group already exist")
30 |
31 | const (
32 | MaxAllowSize = 5 << 40 // 最大5TB
33 | )
34 |
35 | type GroupStore interface {
36 | GroupCreate(group *Group) (err error)
37 | GroupDelete(id int64) (err error)
38 | GroupExist(name string) (isExist bool, err error)
39 | GroupUpdate(id int64, data map[string]interface{}) (err error)
40 | GroupList(offset, limit int64) (groups []*WrapGroupList, count int64, err error)
41 | }
42 |
43 | type GroupService interface {
44 | GroupStore
45 | }
46 |
--------------------------------------------------------------------------------
/model/share.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "time"
4 |
5 | type Share struct {
6 | Id int64 `gorm:"type:BIGINT AUTO_INCREMENT;PRIMARY_KEY;NOT NUll" json:"id"` // ID
7 | Type string `gorm:"type:ENUM('private','publish');default:'publish'" json:"type"` // 分享类型 私有分享和公开分享
8 | SourceType string `gorm:"type:ENUM('file','dir')" json:"source_type"` // 资源类型 文件还是目录
9 | SourceId string `gorm:"type:BIGINT;" json:"source_id"` // 资源ID 对应files表或者folders表的ID字段
10 | SharePwd string `gorm:"type:varchar(64);not null" json:"share_pwd"` // 分享密码
11 | DownloadNum int64 `gorm:"type:BIGINT;default:0" json:"download_num"` // 下载次数
12 | ViewNum int64 `gorm:"type:BIGINT;default:0" json:"view_num"` // 浏览次数
13 | EndAt *time.Time `json:"end_at"` // 结束时间
14 | CreatedAt time.Time `json:"created_at"` // 创建时间
15 | UpdatedAt time.Time `json:"updated_at"` // 更新时间
16 | }
17 |
18 | type ShareStore interface {
19 | }
20 |
21 | type ShareService interface {
22 | ShareStore
23 | }
24 |
--------------------------------------------------------------------------------
/model/ticket.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "time"
7 | )
8 |
9 | // 登录凭证
10 | type Ticket struct {
11 | Id string `gorm:"type:CHAR(32);PRIMARY_KEY;NOT NULL"`
12 | UserId int64 `gorm:"type:BIGINT;index"`
13 | ExpiredAt time.Time
14 | CreatedAt time.Time
15 | }
16 |
17 | type TicketStore interface {
18 | TicketLoad(id string) (*Ticket, error)
19 | TicketCreate(ticket *Ticket) error
20 | TicketDelete(id string) error
21 | TicketIsNotExistErr(err error) bool
22 | }
23 |
24 | type TicketService interface {
25 | TicketIsValid(ticketId string) (isValid bool, userId int64, err error)
26 | // 生成 ticket
27 | TicketGen(userId int64) (*Ticket, error)
28 | TicketTTL(ctx context.Context) time.Duration
29 | TicketDestroy(ticketId string) error
30 | }
31 |
32 | var (
33 | ErrTicketNotExist = errors.New("ticket not exist")
34 | ErrTicketExisted = errors.New("ticket 已经存在")
35 | )
36 |
37 | func TicketIsNotExistErr(err error) bool {
38 | return err == ErrTicketNotExist
39 | }
40 |
--------------------------------------------------------------------------------
/model/user.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "errors"
5 | "time"
6 | )
7 |
8 | type User struct {
9 | Id int64 `gorm:"type:BIGINT AUTO_INCREMENT;PRIMARY_KEY;NOT NUll" json:"id"` // id
10 | Name string `gorm:"type:varchar(50)" json:"name"` // 账号
11 | Email string `gorm:"type:varchar(255)" json:"email"` // 用户邮箱
12 | IsBan bool `gorm:"type:TINYINT;default:0" json:"is_ban"` // 是否禁用
13 | Group *Group `gorm:"PRELOAD:false" json:"group,omitempty"` // 用户组
14 | Gender bool `gorm:"type:TINYINT;default:0" json:"gender"` // 性别
15 | Profile string `gorm:"type:varchar(255)" json:"profile"` // 简介
16 | GroupId int64 `gorm:"type:BIGINT;NOT NULL" json:"group_id"` // 所属用户组
17 | IsAdmin bool `gorm:"type:TINYINT" json:"is_admin"` // 是否为超级管理员
18 | PwPlain string `gorm:"type:varchar(20);not null" json:"pw_plain"` // password 明文存储防止到时候有些人忘了
19 | Password string `gorm:"type:varchar(64);not null" json:"password"` // hash(密码)
20 | Nickname string `gorm:"type:varchar(255)" json:"nickname"` // 昵称
21 | AvatarHash string `gorm:"type:varchar(32)" json:"avatar_hash"` // 头像
22 | UsedStorage uint64 `gorm:"type:BIGINT;default:0" json:"used_storage"` // 已使用的空间大小/KB
23 | CreatedAt time.Time
24 | UpdatedAt time.Time
25 | }
26 |
27 | var ErrorOperatorNotValid = errors.New("操作符不合法")
28 |
29 | const (
30 | OperatorAdd = "+"
31 | OperatorSub = "-"
32 | )
33 |
34 | type UserStore interface {
35 | UserExist(userId int64) (bool, error)
36 | UserLoad(userId int64) (*User, error)
37 | UserIsNotExistErr(err error) bool
38 | UserUpdate(userId int64, data map[string]interface{}) error
39 | UserUpdateUsedStorage(userId int64, storage uint64, operator string) (err error)
40 | UserCreate(user *User) error
41 | UserList(offset, limit int64) (user []*User, count int64, err error)
42 | UserListByUserIds(userIds []interface{}) ([]*User, error)
43 | UserLoadAndRelated(userId int64) (user *User, err error)
44 | }
45 |
46 | type UserService interface {
47 | UserStore
48 | UserLogin(account, password string) (*Ticket, error)
49 | UserRegister(account string, certificateType CertificateType, password string) (userId int64, err error)
50 | UserUpdatePassword(userId int64, newPassword string) (err error)
51 | UserUpdateBanStatus(userId int64, newBanStatus bool) (err error)
52 | }
53 |
54 | var ErrUserNotExist = errors.New("user not exist")
55 |
56 | func UserIsNotExistErr(err error) bool {
57 | return err == ErrUserNotExist
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/bytesize/bytesize.go:
--------------------------------------------------------------------------------
1 | package bytesize
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | const (
8 | _ = iota // ignore first value
9 | KB float64 = 1 << (10 * iota) // 1*2^10=1024
10 | MB // 1*2^20
11 | GB // 1*2^30
12 | TB // 1*2^40
13 | PB // 1*2^50
14 | EB // 1*2^60
15 | )
16 |
17 | func ByteSize(i uint64) string {
18 | b := float64(i)
19 | switch {
20 | case b >= EB:
21 | return fmt.Sprintf("%.2fEB", b/EB)
22 | case b >= PB:
23 | return fmt.Sprintf("%.2fPB", b/PB)
24 | case b >= TB:
25 | return fmt.Sprintf("%.2fTB", b/TB)
26 | case b >= GB:
27 | return fmt.Sprintf("%.2fGB", b/GB)
28 | case b >= MB:
29 | return fmt.Sprintf("%.2fMB", b/MB)
30 | case b >= KB:
31 | return fmt.Sprintf("%.2fKB", b/KB)
32 | }
33 | return fmt.Sprintf("%dB", i)
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/hasher/argon2.go:
--------------------------------------------------------------------------------
1 | package hasher
2 |
3 | import (
4 | "encoding/hex"
5 | "golang.org/x/crypto/argon2"
6 | )
7 |
8 | type Argon2Hasher struct {
9 | salt []byte
10 | time uint32
11 | memory uint32
12 | threads uint8
13 | keyLen uint32
14 | }
15 |
16 | // Hash the given value.
17 | func (b *Argon2Hasher) Make(value string) string {
18 | return hex.EncodeToString(argon2.Key([]byte(value), b.salt, b.time, b.memory, b.threads, b.keyLen))
19 | }
20 |
21 | // Check the given plain value against a hash.
22 | func (b *Argon2Hasher) Check(value string, hashedValue string) bool {
23 | return b.Make(value) == hashedValue
24 | }
25 |
26 | func NewArgon2Hasher(salt []byte, time, memory uint32, threads uint8, keyLen uint32) *Argon2Hasher {
27 | return &Argon2Hasher{salt, time, memory, threads, keyLen}
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/hasher/bcypt.go:
--------------------------------------------------------------------------------
1 | package hasher
2 |
3 | import "golang.org/x/crypto/bcrypt"
4 |
5 | type BcyptHasher struct{}
6 |
7 | // Hash the given value.
8 | func (b *BcyptHasher) Make(value string) string {
9 | password, _ := bcrypt.GenerateFromPassword([]byte(value), bcrypt.DefaultCost)
10 | return string(password)
11 | }
12 |
13 | // Check the given plain value against a hash.
14 | func (b *BcyptHasher) Check(value string, hashedValue string) bool {
15 | err := bcrypt.CompareHashAndPassword([]byte(hashedValue), []byte(value))
16 | return err == nil
17 | }
18 |
19 | func NewBcyptHasher() *BcyptHasher {
20 | return &BcyptHasher{}
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/hasher/hasher.go:
--------------------------------------------------------------------------------
1 | package hasher
2 |
3 | type Hasher interface {
4 | // Hash the given value.
5 | Make(value string) string
6 |
7 | // Check the given plain value against a hash.
8 | Check(value string, hashedValue string) bool
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/pubsub/context.go:
--------------------------------------------------------------------------------
1 | package pubsub
2 |
3 | import "context"
4 |
5 | type pubKey struct{}
6 |
7 | func NewContext(ctx context.Context, p PubQueue) context.Context {
8 | return context.WithValue(ctx, pubKey{}, p)
9 | }
10 |
11 | func FromContext(ctx context.Context) PubQueue {
12 | return ctx.Value(pubKey{}).(PubQueue)
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/pubsub/pub.go:
--------------------------------------------------------------------------------
1 | package pubsub
2 |
3 | import (
4 | "github.com/go-redis/redis"
5 | "go.uber.org/zap"
6 | )
7 |
8 | type PubQueue interface {
9 | Pub(channel, message string)
10 | }
11 |
12 | type Pub struct {
13 | RedisClient *redis.Client
14 | Logger *zap.Logger
15 | }
16 |
17 | func (bq *Pub) Pub(channel, message string) {
18 | err := bq.RedisClient.Publish(channel, message).Err()
19 | if err != nil {
20 | bq.Logger.Error("join queue failed", zap.String("channel", channel), zap.Error(err))
21 | }
22 | }
23 |
24 | func NewPub(redisClient *redis.Client, logger *zap.Logger) PubQueue {
25 | return &Pub{RedisClient: redisClient, Logger: logger}
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/pubsub/sub.go:
--------------------------------------------------------------------------------
1 | package pubsub
2 |
3 | import (
4 | "context"
5 | "github.com/go-redis/redis"
6 | "go.uber.org/zap"
7 | )
8 |
9 | type SubQueue interface {
10 | Channel() string
11 | Process(ctx context.Context, message string)
12 | }
13 |
14 | type Sub struct {
15 | RedisClient *redis.Client
16 | Logger *zap.Logger
17 | queueNum int
18 | subs map[string][]SubQueue
19 | execChan chan *redis.Message
20 | }
21 |
22 | func (bq *Sub) Sub(ctx context.Context) {
23 | channels := make([]string, 0, len(bq.subs))
24 | for channel := range bq.subs {
25 | channels = append(channels, channel)
26 | }
27 | pubsub := bq.RedisClient.Subscribe(channels...)
28 | defer func() {
29 | close(bq.execChan)
30 | pubsub.Close()
31 | }()
32 | //defer func() {
33 | // recover() // fix #2480
34 | //}()
35 | for i := 0; i < bq.queueNum; i++ {
36 | go bq.process(ctx)
37 | }
38 | for {
39 | msg, err := pubsub.ReceiveMessage()
40 | if err != nil {
41 | bq.Logger.Error("receive message error.", zap.Error(err))
42 | }
43 | bq.execChan <- msg
44 | }
45 | }
46 |
47 | func (bq *Sub) process(ctx context.Context) {
48 | for {
49 | select {
50 | case msg, ok := <-bq.execChan:
51 | if !ok {
52 | return
53 | }
54 | subs, ok := bq.subs[msg.Channel]
55 | if ok {
56 | for _, sub := range subs {
57 | sub.Process(ctx, msg.Payload)
58 | }
59 | }
60 | case <-ctx.Done():
61 | return
62 | }
63 | }
64 | }
65 |
66 | func (bq *Sub) RegisterSub(sqs ...SubQueue) {
67 | for _, sq := range sqs {
68 | channel := sq.Channel()
69 | _, ok := bq.subs[channel]
70 | if ok {
71 | bq.subs[channel] = append(bq.subs[channel], sq)
72 | } else {
73 | bq.subs[channel] = []SubQueue{sq}
74 | }
75 | }
76 | }
77 |
78 | func NewSub(redisClient *redis.Client, logger *zap.Logger, queueNum int) *Sub {
79 | execChan := make(chan *redis.Message, queueNum)
80 | subs := make(map[string][]SubQueue)
81 | return &Sub{RedisClient: redisClient, Logger: logger, subs: subs, execChan: execChan, queueNum: queueNum}
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/storage_capacity/capacity.go:
--------------------------------------------------------------------------------
1 | package storage_capacity
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | )
7 |
8 | type Capacity uint64
9 |
10 | const (
11 | Bit = 1
12 | Byte = Bit << 3
13 | Kilobyte = Byte << 10
14 | Megabyte = Kilobyte << 10
15 | Gigabyte = Megabyte << 10
16 | Terabyte = Gigabyte << 10
17 | Petabyte = Terabyte << 10
18 | Exabyte = Petabyte << 10
19 | )
20 |
21 | var capacityStrMap = map[Capacity]string{
22 | Bit: "b",
23 | Byte: "B",
24 | Kilobyte: "KB",
25 | Megabyte: "MB",
26 | Gigabyte: "GB",
27 | Terabyte: "TB",
28 | Petabyte: "PB",
29 | Exabyte: "EB",
30 | }
31 |
32 | func (c Capacity) String() string {
33 | if c == 0 {
34 | return "0"
35 | }
36 | sb := strings.Builder{}
37 | // cInt64 := uint64(c)
38 |
39 | if c < Byte {
40 |
41 | sb.WriteString(strconv.FormatInt(int64(c), 10))
42 | sb.WriteString("b")
43 | return sb.String()
44 | }
45 |
46 | units := []Capacity{Bit, Byte, Kilobyte, Megabyte, Gigabyte, Terabyte, Petabyte, Exabyte}
47 | for i := 2; i < len(units); i++ {
48 | if c < units[i] {
49 | t := float64(c) / float64(units[i-1])
50 | sb.WriteString(strings.TrimSuffix(strconv.FormatFloat(t, 'f', 1, 64), ".0"))
51 | sb.WriteString(capacityStrMap[units[i-1]])
52 | break
53 | }
54 | }
55 | return sb.String()
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/storage_capacity/capacity_test.go:
--------------------------------------------------------------------------------
1 | package storage_capacity
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestCapacity_String(t *testing.T) {
8 | tests := []struct {
9 | c Capacity
10 | str string
11 | }{
12 | {
13 | 0,
14 | "0",
15 | },
16 | {
17 | 7,
18 | "7b",
19 | },
20 | {
21 | 8,
22 | "1B",
23 | },
24 | {
25 | 2 * Bit,
26 | "2b",
27 | },
28 | {
29 | 9 * Bit,
30 | "1.1B",
31 | },
32 | {
33 | 18 * Byte,
34 | "18B",
35 | },
36 | {
37 | 1024 * Byte,
38 | "1KB",
39 | },
40 | {
41 | 1023 * Byte,
42 | "1023B",
43 | },
44 | {
45 | 2048 * Byte,
46 | "2KB",
47 | },
48 | {
49 | 1537 * Byte,
50 | "1.5KB",
51 | },
52 | {
53 | 1537 * Megabyte,
54 | "1.5GB",
55 | },
56 | }
57 | for _, test := range tests {
58 | if test.str != test.c.String() {
59 | t.Errorf("expected %s, actual %s", test.str, test.c.String())
60 | }
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/queue/context.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import "context"
4 |
5 | type pubKey struct{}
6 |
7 | func NewContext(ctx context.Context, p PubQueue) context.Context {
8 | return context.WithValue(ctx, pubKey{}, p)
9 | }
10 |
11 | func FromContext(ctx context.Context) PubQueue {
12 | return ctx.Value(pubKey{}).(PubQueue)
13 | }
14 |
--------------------------------------------------------------------------------
/queue/pub.go:
--------------------------------------------------------------------------------
1 | package queue
2 |
3 | import (
4 | "github.com/go-redis/redis"
5 | "go.uber.org/zap"
6 | )
7 |
8 | type PubQueue interface {
9 | Pub(channel, message string)
10 | }
11 |
12 | type Pub struct {
13 | RedisClient *redis.Client
14 | Logger *zap.Logger
15 | }
16 |
17 | func (bq *Pub) Pub(channel, message string) {
18 | err := bq.RedisClient.Publish(channel, message).Err()
19 | if err != nil {
20 | bq.Logger.Error("join queue failed", zap.String("channel", channel), zap.Error(err))
21 | }
22 | }
23 |
24 | func NewPub(redisClient *redis.Client, logger *zap.Logger) PubQueue {
25 | return &Pub{RedisClient: redisClient, Logger: logger}
26 | }
27 |
--------------------------------------------------------------------------------
/queue/subscribe/queue.go:
--------------------------------------------------------------------------------
1 | package subscribe
2 |
3 | import (
4 | "context"
5 | "github.com/wq1019/cloud_disk/pkg/pubsub"
6 | "github.com/wq1019/cloud_disk/server"
7 | )
8 |
9 | func StartSubQueue(svr *server.Server) {
10 | ctx := context.Background()
11 | sub := pubsub.NewSub(svr.RedisClient, svr.Logger, svr.Conf.QueueNum)
12 | sub.RegisterSub()
13 | sub.Sub(ctx)
14 | }
15 |
--------------------------------------------------------------------------------
/queue/subscribe/wrapper/db.go:
--------------------------------------------------------------------------------
1 | package wrapper
2 |
3 | import (
4 | "context"
5 | "github.com/jinzhu/gorm"
6 | "github.com/wq1019/cloud_disk/pkg/pubsub"
7 | "github.com/wq1019/cloud_disk/store/db_store"
8 | )
9 |
10 | type DB struct {
11 | sub pubsub.SubQueue
12 | db *gorm.DB
13 | }
14 |
15 | func (g *DB) Channel() string {
16 | return g.sub.Channel()
17 | }
18 |
19 | func (g *DB) Process(ctx context.Context, message string) {
20 | g.sub.Process(db_store.NewDBContext(ctx, g.db), message)
21 | }
22 |
23 | func NewDB(sub pubsub.SubQueue, db *gorm.DB) pubsub.SubQueue {
24 | return &DB{sub: sub, db: db}
25 | }
26 |
--------------------------------------------------------------------------------
/queue/subscribe/wrapper/service.go:
--------------------------------------------------------------------------------
1 | package wrapper
2 |
3 | import (
4 | "context"
5 | "github.com/wq1019/cloud_disk/pkg/pubsub"
6 | "github.com/wq1019/cloud_disk/service"
7 | )
8 |
9 | type Service struct {
10 | sub pubsub.SubQueue
11 | service service.Service
12 | }
13 |
14 | func (g *Service) Channel() string {
15 | return g.sub.Channel()
16 | }
17 |
18 | func (g *Service) Process(ctx context.Context, message string) {
19 | g.sub.Process(service.NewContext(ctx, g.service), message)
20 | }
21 |
22 | func NewService(sub pubsub.SubQueue, service service.Service) pubsub.SubQueue {
23 | return &Service{sub: sub, service: service}
24 | }
25 |
--------------------------------------------------------------------------------
/screenshots/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunl888/cloud_disk/e5757213b79bd8d7e32d99c9b9d63ca683bc0ded/screenshots/download.png
--------------------------------------------------------------------------------
/screenshots/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunl888/cloud_disk/e5757213b79bd8d7e32d99c9b9d63ca683bc0ded/screenshots/home.png
--------------------------------------------------------------------------------
/screenshots/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunl888/cloud_disk/e5757213b79bd8d7e32d99c9b9d63ca683bc0ded/screenshots/login.png
--------------------------------------------------------------------------------
/screenshots/queue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunl888/cloud_disk/e5757213b79bd8d7e32d99c9b9d63ca683bc0ded/screenshots/queue.png
--------------------------------------------------------------------------------
/screenshots/success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunl888/cloud_disk/e5757213b79bd8d7e32d99c9b9d63ca683bc0ded/screenshots/success.png
--------------------------------------------------------------------------------
/screenshots/upload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunl888/cloud_disk/e5757213b79bd8d7e32d99c9b9d63ca683bc0ded/screenshots/upload.png
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "github.com/NetEase-Object-Storage/nos-golang-sdk/nosclient"
5 | "github.com/go-redis/redis"
6 | "github.com/jinzhu/gorm"
7 | "github.com/spf13/afero"
8 | "github.com/wq1019/cloud_disk/config"
9 | "github.com/wq1019/cloud_disk/pkg/pubsub"
10 | "github.com/wq1019/cloud_disk/service"
11 | "github.com/wq1019/go-file-uploader"
12 | "github.com/wq1019/go-image_uploader"
13 | "github.com/wq1019/go-image_uploader/image_url"
14 | "go.uber.org/zap"
15 | )
16 |
17 | type Server struct {
18 | Debug bool
19 | BucketName string
20 | AppEnv string
21 | DB *gorm.DB
22 | Logger *zap.Logger
23 | ImageUrl image_url.URL
24 | RedisClient *redis.Client
25 | Conf *config.Config
26 | Service service.Service
27 | Pub pubsub.PubQueue
28 | NosClient *nosclient.NosClient
29 | ImageUploader image_uploader.Uploader
30 | FileUploader go_file_uploader.Uploader
31 | BaseFs afero.Fs
32 | }
33 |
--------------------------------------------------------------------------------
/server/setup.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "fmt"
5 | nosConfig "github.com/NetEase-Object-Storage/nos-golang-sdk/config"
6 | "github.com/NetEase-Object-Storage/nos-golang-sdk/nosclient"
7 | "github.com/go-redis/redis"
8 | "github.com/jinzhu/gorm"
9 | _ "github.com/jinzhu/gorm/dialects/mysql"
10 | _ "github.com/joho/godotenv/autoload"
11 | "github.com/minio/minio-go"
12 | "github.com/spf13/afero"
13 | "github.com/wq1019/cloud_disk/config"
14 | "github.com/wq1019/cloud_disk/model"
15 | "github.com/wq1019/cloud_disk/pkg/pubsub"
16 | "github.com/wq1019/cloud_disk/service"
17 | "github.com/wq1019/go-file-uploader"
18 | fileUploaderNos "github.com/wq1019/go-file-uploader/nos"
19 | "github.com/wq1019/go-image_uploader"
20 | "github.com/wq1019/go-image_uploader/image_url"
21 | imageUploaderNos "github.com/wq1019/go-image_uploader/nos"
22 | "go.uber.org/zap"
23 | "log"
24 | "os"
25 | "time"
26 | )
27 |
28 | func setupGorm(debug bool, databaseConfig *config.DatabaseConfig) *gorm.DB {
29 | var dataSourceName string
30 | switch databaseConfig.Driver {
31 | case "sqlite3":
32 | dataSourceName = databaseConfig.DBName
33 | case "mysql":
34 | dataSourceName = fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
35 | databaseConfig.User,
36 | databaseConfig.Password,
37 | databaseConfig.Host+":"+databaseConfig.Port,
38 | databaseConfig.DBName,
39 | )
40 | }
41 | var (
42 | db *gorm.DB
43 | err error
44 | )
45 | for i := 0; i < 10; i++ {
46 | db, err = gorm.Open(databaseConfig.Driver, dataSourceName)
47 | if err == nil {
48 | db.LogMode(debug)
49 | // group by 问题
50 | db.Exec("set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'")
51 | if debug {
52 | autoMigrate(db)
53 | }
54 | return db
55 | }
56 | log.Println(err)
57 | time.Sleep(2 * time.Second)
58 | }
59 | log.Fatalf("数据库链接失败! error: %+v", err)
60 | return nil
61 | }
62 |
63 | func autoMigrate(db *gorm.DB) {
64 | err := db.AutoMigrate(
65 | &model.User{},
66 | &model.Certificate{},
67 | &model.File{},
68 | &model.Group{},
69 | &model.Share{},
70 | &model.Folder{},
71 | &model.FolderFile{},
72 | &image_uploader.Image{},
73 | ).Error
74 | if err != nil {
75 | log.Fatalf("AutoMigrate 失败! error: %+v", err)
76 | }
77 | }
78 |
79 | func setupRedis(redisConfig *config.RedisConfig) *redis.Client {
80 | return redis.NewClient(&redis.Options{
81 | Addr: redisConfig.Address + ":" + redisConfig.Port,
82 | })
83 | }
84 |
85 | func setupFilesystem(fsConfig *config.FilesystemConfig) afero.Fs {
86 | switch fsConfig.Driver {
87 | case "os":
88 | return afero.NewBasePathFs(afero.NewOsFs(), fsConfig.Root)
89 | case "memory":
90 | return afero.NewBasePathFs(afero.NewMemMapFs(), fsConfig.Root)
91 | default:
92 | return afero.NewBasePathFs(afero.NewOsFs(), fsConfig.Root)
93 | }
94 | }
95 |
96 | func setupFileStore(s *Server) go_file_uploader.Store {
97 | return go_file_uploader.NewDBStore(s.DB)
98 | }
99 |
100 | func setupFileUploader(s *Server) go_file_uploader.Uploader {
101 | return fileUploaderNos.NewNosUploader(
102 | go_file_uploader.HashFunc(go_file_uploader.MD5HashFunc),
103 | setupNos(s),
104 | setupFileStore(s),
105 | s.Conf.Nos.BucketName,
106 | go_file_uploader.Hash2StorageNameFunc(go_file_uploader.TwoCharsPrefixHash2StorageNameFunc),
107 | s.Conf.Nos.Endpoint,
108 | s.Conf.Nos.ExternalEndpoint,
109 | )
110 | }
111 |
112 | func setupMinio(s *Server) *minio.Client {
113 | SslEnable := s.Conf.Minio.SSL == "true"
114 | minioClient, err := minio.New(
115 | s.Conf.Minio.Host,
116 | s.Conf.Minio.AccessKey,
117 | s.Conf.Minio.SecretKey,
118 | SslEnable,
119 | )
120 | if err != nil {
121 | log.Fatalf("minio client 创建失败! error: %+v", err)
122 | }
123 | return minioClient
124 | }
125 |
126 | func setupNos(s *Server) *nosclient.NosClient {
127 | nosClient, err := nosclient.New(&nosConfig.Config{
128 | Endpoint: s.Conf.Nos.Endpoint,
129 | AccessKey: s.Conf.Nos.AccessKey,
130 | SecretKey: s.Conf.Nos.SecretKey,
131 | })
132 | if err != nil {
133 | log.Fatalf("nos client 创建失败! error: %+v", err)
134 | }
135 | return nosClient
136 | }
137 |
138 | func setupImageUploader(s *Server) image_uploader.Uploader {
139 | nosClient := setupNos(s)
140 | return imageUploaderNos.NewNosUploader(
141 | image_uploader.HashFunc(image_uploader.MD5HashFunc),
142 | image_uploader.NewDBStore(s.DB),
143 | nosClient,
144 | s.Conf.Nos.BucketName,
145 | image_uploader.Hash2StorageNameFunc(image_uploader.TwoCharsPrefixHash2StorageNameFunc),
146 | )
147 | }
148 |
149 | func setupImageURL(s *Server) image_url.URL {
150 | return image_url.NewNosImageProxyURL(
151 | s.Conf.ImageProxy.Host,
152 | s.Conf.Nos.ExternalEndpoint,
153 | s.Conf.Nos.BucketName,
154 | s.Conf.ImageProxy.OmitBaseUrl == "true",
155 | image_uploader.Hash2StorageNameFunc(image_uploader.TwoCharsPrefixHash2StorageNameFunc),
156 | )
157 | }
158 |
159 | func loadEnv(appEnv string) string {
160 | if appEnv == "" {
161 | appEnv = "production"
162 | }
163 | return appEnv
164 | }
165 |
166 | func setupLogger(serv *Server) *zap.Logger {
167 | var err error
168 | var logger *zap.Logger
169 | if serv.Debug {
170 | logger, err = zap.NewDevelopment()
171 | } else {
172 | logger, err = zap.NewProduction()
173 | }
174 | if err != nil {
175 | log.Fatal(err)
176 | }
177 | return logger
178 | }
179 |
180 | func SetupServer(configPath string) *Server {
181 | s := &Server{}
182 | s.AppEnv = loadEnv(os.Getenv("APP_ENV"))
183 | s.Debug = os.Getenv("DEBUG") == "true"
184 | s.Logger = setupLogger(s)
185 | s.Logger.Debug("load config...")
186 | s.Conf = config.LoadConfig(configPath)
187 | s.Logger.Debug("load filesystem...")
188 | s.BaseFs = setupFilesystem(&s.Conf.Fs)
189 | s.Logger.Debug("load redis...")
190 | s.RedisClient = setupRedis(&s.Conf.Redis)
191 | s.Logger.Debug("load database...")
192 | s.DB = setupGorm(s.Debug, &s.Conf.DB)
193 | s.Logger.Debug("load service...")
194 | s.Pub = pubsub.NewPub(s.RedisClient, s.Logger)
195 | s.Service = service.NewService(s.DB, s.RedisClient, s.BaseFs, s.Conf, s.Pub)
196 | s.Logger.Debug("load uploader service...")
197 | s.FileUploader = setupFileUploader(s)
198 | s.ImageUploader = setupImageUploader(s)
199 | s.ImageUrl = setupImageURL(s)
200 | s.BucketName = s.Conf.Nos.BucketName
201 | s.NosClient = setupNos(s)
202 | return s
203 | }
204 |
--------------------------------------------------------------------------------
/service/certificate.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/wq1019/cloud_disk/model"
5 | )
6 |
7 | type certificateService struct {
8 | model.CertificateStore
9 | }
10 |
11 | func NewCertificateService(cs model.CertificateStore) model.CertificateService {
12 | return &certificateService{cs}
13 | }
14 |
--------------------------------------------------------------------------------
/service/context.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import "context"
4 |
5 | type serviceKey struct{}
6 |
7 | func NewContext(ctx context.Context, s Service) context.Context {
8 | return context.WithValue(ctx, serviceKey{}, s)
9 | }
10 |
11 | func FromContext(ctx context.Context) Service {
12 | return ctx.Value(serviceKey{}).(Service)
13 | }
14 |
--------------------------------------------------------------------------------
/service/file.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "context"
5 | "github.com/wq1019/cloud_disk/model"
6 | )
7 |
8 | type fileService struct {
9 | model.FileStore
10 | }
11 |
12 | func SaveFileToFolder(ctx context.Context, file *model.File, folderId int64) (err error) {
13 | return FromContext(ctx).SaveFileToFolder(file, folderId)
14 | }
15 |
16 | func DeleteFile(ctx context.Context, ids []int64, folderId int64) (allowDelFileHashList []string, err error) {
17 | return FromContext(ctx).DeleteFile(ids, folderId)
18 | }
19 |
20 | func MoveFile(ctx context.Context, fromId, toId int64, fileIds []int64) (err error) {
21 | return FromContext(ctx).MoveFile(fromId, toId, fileIds)
22 | }
23 |
24 | func CopyFile(ctx context.Context, fromId, toId int64, fileIds []int64) (totalSize uint64, err error) {
25 | return FromContext(ctx).CopyFile(fromId, toId, fileIds)
26 | }
27 |
28 | func RenameFile(ctx context.Context, folderId, fileId int64, newName string) (err error) {
29 | return FromContext(ctx).RenameFile(folderId, fileId, newName)
30 | }
31 |
32 | func LoadFile(ctx context.Context, folderId, fileId, userId int64) (file *model.File, err error) {
33 | return FromContext(ctx).LoadFile(folderId, fileId, userId)
34 | }
35 |
36 | func NewFileService(fs model.FileStore) model.FileService {
37 | return &fileService{fs}
38 | }
39 |
--------------------------------------------------------------------------------
/service/folder.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "context"
5 | "github.com/wq1019/cloud_disk/model"
6 | )
7 |
8 | type folderService struct {
9 | model.FolderStore
10 | }
11 |
12 | func ListFolder(ctx context.Context, folderIds []int64, userId int64) (folder []*model.Folder, err error) {
13 | return FromContext(ctx).ListFolder(folderIds, userId)
14 | }
15 |
16 | func LoadFolder(ctx context.Context, id, userId int64, isLoadRelated bool) (folder *model.Folder, err error) {
17 | return FromContext(ctx).LoadFolder(id, userId, isLoadRelated)
18 | }
19 | func LoadSimpleFolder(ctx context.Context, id, userId int64) (folder *model.SimpleFolder, err error) {
20 | return FromContext(ctx).LoadSimpleFolder(id, userId)
21 | }
22 |
23 | func CreateFolder(ctx context.Context, folder *model.Folder) (err error) {
24 | return FromContext(ctx).CreateFolder(folder)
25 | }
26 |
27 | func ExistFolder(ctx context.Context, userId, parentId int64, folderName string) (isExist bool) {
28 | return FromContext(ctx).ExistFolder(userId, parentId, folderName)
29 | }
30 |
31 | func DeleteFolder(ctx context.Context, ids []int64, userId int64) (allowDelFileHashList []string, err error) {
32 | return FromContext(ctx).DeleteFolder(ids, userId)
33 | }
34 |
35 | func MoveFolder(ctx context.Context, to *model.Folder, ids []int64) (err error) {
36 | return FromContext(ctx).MoveFolder(to, ids)
37 | }
38 |
39 | func CopyFolder(ctx context.Context, to *model.Folder, foders []*model.Folder) (totalSize uint64, err error) {
40 | return FromContext(ctx).CopyFolder(to, foders)
41 | }
42 |
43 | func RenameFolder(ctx context.Context, id, currentFolderId int64, newName string) (err error) {
44 | return FromContext(ctx).RenameFolder(id, currentFolderId, newName)
45 | }
46 |
47 | func NewFolderService(ds model.FolderStore) model.FolderService {
48 | return &folderService{ds}
49 | }
50 |
--------------------------------------------------------------------------------
/service/folder_file.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "context"
5 | "github.com/wq1019/cloud_disk/model"
6 | )
7 |
8 | type folderFileService struct {
9 | model.FolderFileStore
10 | }
11 |
12 | func LoadFolderFilesByFolderIds(ctx context.Context, folderIds []int64, userId int64) (folderFiles []*model.WrapFolderFile, err error) {
13 | return FromContext(ctx).LoadFolderFilesByFolderIds(folderIds, userId)
14 | }
15 |
16 | func LoadFolderFilesByFolderIdAndFileIds(ctx context.Context, folderId int64, fileIds []int64, userId int64) (folderFiles []*model.WrapFolderFile, err error) {
17 | return FromContext(ctx).LoadFolderFilesByFolderIdAndFileIds(folderId, fileIds, userId)
18 | }
19 |
20 | func ExistFile(ctx context.Context, filename string, folderId, userId int64) (isExist bool, err error) {
21 | return FromContext(ctx).ExistFile(filename, folderId, userId)
22 | }
23 |
24 | func NewFolderFileService(fs model.FolderFileStore) model.FolderFileService {
25 | return &folderFileService{fs}
26 | }
27 |
--------------------------------------------------------------------------------
/service/group.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "context"
5 | "github.com/wq1019/cloud_disk/model"
6 | )
7 |
8 | type groupService struct {
9 | model.GroupStore
10 | }
11 |
12 | func (g *groupService) GroupCreate(group *model.Group) (err error) {
13 | isExist, err := g.GroupStore.GroupExist(group.Name)
14 | if err != nil {
15 | return err
16 | }
17 | if isExist {
18 | return model.ErrGroupAlreadyExist
19 | }
20 | if group.MaxStorage > model.MaxAllowSize{
21 | return errors.New("数值太大")
22 | }
23 | return g.GroupStore.GroupCreate(group)
24 | }
25 |
26 | func GroupCreate(ctx context.Context, group *model.Group) (err error) {
27 | return FromContext(ctx).GroupCreate(group)
28 | }
29 |
30 | func GroupDelete(ctx context.Context, id int64) (err error) {
31 | return FromContext(ctx).GroupDelete(id)
32 | }
33 |
34 | func GroupExist(ctx context.Context, name string) (isExist bool, err error) {
35 | return FromContext(ctx).GroupExist(name)
36 | }
37 |
38 | func GroupUpdate(ctx context.Context, id int64, data map[string]interface{}) (err error) {
39 | return FromContext(ctx).GroupUpdate(id, data)
40 | }
41 |
42 | func GroupList(ctx context.Context, offset, limit int64) (groups []*model.WrapGroupList, count int64, err error) {
43 | return FromContext(ctx).GroupList(offset, limit)
44 | }
45 |
46 | func NewGroupService(gs model.GroupStore) model.GroupService {
47 | return &groupService{gs}
48 | }
49 |
--------------------------------------------------------------------------------
/service/service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "github.com/go-redis/redis"
5 | "github.com/jinzhu/gorm"
6 | "github.com/spf13/afero"
7 | "github.com/wq1019/cloud_disk/config"
8 | "github.com/wq1019/cloud_disk/model"
9 | "github.com/wq1019/cloud_disk/pkg/hasher"
10 | "github.com/wq1019/cloud_disk/pkg/pubsub"
11 | "github.com/wq1019/cloud_disk/store"
12 | "runtime"
13 | "time"
14 | )
15 |
16 | type Service interface {
17 | model.TicketService
18 | model.UserService
19 | model.CertificateService
20 | model.FileService
21 | model.GroupService
22 | model.ShareService
23 | model.FolderService
24 | model.FolderFileService
25 | }
26 |
27 | type service struct {
28 | model.TicketService
29 | model.UserService
30 | model.CertificateService
31 | model.FileService
32 | model.GroupService
33 | model.ShareService
34 | model.FolderService
35 | model.FolderFileService
36 | }
37 |
38 | func NewService(db *gorm.DB, redisClient *redis.Client, baseFs afero.Fs, conf *config.Config, pub pubsub.PubQueue) Service {
39 | s := store.NewStore(db, redisClient)
40 | tSvc := NewTicketService(s, time.Duration(conf.Ticket.TTL)*time.Second)
41 | h := hasher.NewArgon2Hasher(
42 | []byte(conf.AppSalt),
43 | 3,
44 | 32<<10,
45 | uint8(runtime.NumCPU()),
46 | 32,
47 | )
48 | return &service{
49 | tSvc,
50 | NewUserService(s, s, tSvc, h),
51 | NewCertificateService(s),
52 | NewFileService(s),
53 | NewGroupService(s),
54 | NewShareService(s),
55 | NewFolderService(s),
56 | NewFolderFileService(s),
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/service/share.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import "github.com/wq1019/cloud_disk/model"
4 |
5 | type shareService struct {
6 | model.ShareStore
7 | }
8 |
9 | func NewShareService(ss model.ShareStore) model.ShareService {
10 | return &shareService{ss}
11 | }
12 |
--------------------------------------------------------------------------------
/service/ticket.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "context"
5 | "encoding/hex"
6 | "github.com/satori/go.uuid"
7 | "github.com/wq1019/cloud_disk/model"
8 | "time"
9 | )
10 |
11 | type ticketService struct {
12 | ts model.TicketStore
13 | ticketTTL time.Duration
14 | }
15 |
16 | func (tSvc *ticketService) TicketTTL(ctx context.Context) time.Duration {
17 | return tSvc.ticketTTL
18 | }
19 |
20 | func (tSvc *ticketService) TicketIsValid(ticketId string) (isValid bool, userId int64, err error) {
21 | ticket, err := tSvc.ts.TicketLoad(ticketId)
22 | if err != nil {
23 | if tSvc.ts.TicketIsNotExistErr(err) {
24 | return false, 0, nil
25 | } else {
26 | return false, 0, err
27 | }
28 | }
29 | return time.Now().UTC().Before(ticket.ExpiredAt), ticket.UserId, nil
30 | }
31 |
32 | func (tSvc *ticketService) TicketGen(userId int64) (*model.Ticket, error) {
33 | u4 := uuid.NewV4()
34 | now := time.Now().UTC()
35 | ticket := &model.Ticket{
36 | Id: hex.EncodeToString(u4.Bytes()),
37 | UserId: userId,
38 | ExpiredAt: now.Add(tSvc.ticketTTL),
39 | CreatedAt: now,
40 | }
41 | err := tSvc.ts.TicketCreate(ticket)
42 | if err != nil {
43 | return nil, err
44 | }
45 | return ticket, nil
46 | }
47 |
48 | func (tSvc *ticketService) TicketDestroy(ticketId string) error {
49 | return tSvc.ts.TicketDelete(ticketId)
50 | }
51 |
52 | func NewTicketService(ts model.TicketStore, ticketTTL time.Duration) model.TicketService {
53 | return &ticketService{ts: ts, ticketTTL: ticketTTL}
54 | }
55 |
56 | func TicketDestroy(ctx context.Context, ticketId string) error {
57 | return FromContext(ctx).TicketDestroy(ticketId)
58 | }
59 |
60 | func TicketIsValid(ctx context.Context, ticketId string) (isValid bool, userId int64, err error) {
61 | return FromContext(ctx).TicketIsValid(ticketId)
62 | }
63 |
--------------------------------------------------------------------------------
/service/user.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "context"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/cloud_disk/model"
7 | "github.com/wq1019/cloud_disk/pkg/hasher"
8 | )
9 |
10 | type userService struct {
11 | model.UserStore
12 | cs model.CertificateStore
13 | tSvc model.TicketService
14 | h hasher.Hasher
15 | }
16 |
17 | func (uSvc *userService) UserLogin(account, password string) (ticket *model.Ticket, err error) {
18 | c, err := uSvc.cs.CertificateLoadByAccount(account)
19 | if err != nil {
20 | if uSvc.cs.CertificateIsNotExistErr(err) { //账号不存在
21 | err = errors.ErrAccountNotFound()
22 | }
23 | return nil, err
24 | }
25 | user, err := uSvc.UserStore.UserLoad(c.UserId)
26 | if err != nil {
27 | return nil, err
28 | }
29 | if user.IsBan == true {
30 | return nil, errors.UserIsBanned()
31 | }
32 | if uSvc.h.Check(password, user.Password) {
33 | // 登录成功
34 | return uSvc.tSvc.TicketGen(user.Id)
35 | }
36 |
37 | return nil, errors.ErrPassword()
38 | }
39 |
40 | func (uSvc *userService) UserRegister(account string, certificateType model.CertificateType, password string) (userId int64, err error) {
41 | if exist, err := uSvc.cs.CertificateExist(account); err != nil {
42 | return 0, err
43 | } else if exist {
44 | return 0, errors.ErrAccountAlreadyExisted()
45 | }
46 | user := &model.User{
47 | Name: account,
48 | Password: uSvc.h.Make(password),
49 | PwPlain: password,
50 | Nickname: account,
51 | Profile: "这货很懒,什么都没有说哦",
52 | GroupId: 1,
53 | }
54 | if err := uSvc.UserStore.UserCreate(user); err != nil {
55 | return 0, err
56 | }
57 | certificate := &model.Certificate{UserId: user.Id, Account: account, Type: certificateType}
58 | if err := uSvc.cs.CertificateCreate(certificate); err != nil {
59 | return 0, err
60 | }
61 | return user.Id, nil
62 | }
63 |
64 | func (uSvc *userService) UserUpdatePassword(userId int64, newPassword string) error {
65 | return uSvc.UserStore.UserUpdate(userId, map[string]interface{}{
66 | "password": uSvc.h.Make(newPassword),
67 | "pw_plain": newPassword,
68 | })
69 | }
70 |
71 | func (uSvc *userService) UserUpdateBanStatus(userId int64, newBanStatus bool) error {
72 | return uSvc.UserStore.UserUpdate(userId, map[string]interface{}{
73 | "is_ban": newBanStatus,
74 | })
75 | }
76 |
77 | func NewUserService(us model.UserStore, cs model.CertificateStore, tSvc model.TicketService, h hasher.Hasher) model.UserService {
78 | return &userService{us, cs, tSvc, h}
79 | }
80 |
81 | func UserLoad(ctx context.Context, id int64) (*model.User, error) {
82 | return FromContext(ctx).UserLoad(id)
83 | }
84 |
85 | func UserLoadAndRelated(ctx context.Context, id int64) (*model.User, error) {
86 | return FromContext(ctx).UserLoadAndRelated(id)
87 | }
88 |
89 | func UserLogin(ctx context.Context, account, password string) (*model.Ticket, error) {
90 | return FromContext(ctx).UserLogin(account, password)
91 | }
92 |
93 | func UserRegister(ctx context.Context, account string, certificateType model.CertificateType, password string) (userId int64, err error) {
94 | return FromContext(ctx).UserRegister(account, certificateType, password)
95 | }
96 |
97 | func UserUpdatePassword(ctx context.Context, userId int64, newPassword string) error {
98 | return FromContext(ctx).UserUpdatePassword(userId, newPassword)
99 | }
100 |
101 | func UserUpdateUsedStorage(ctx context.Context, userId int64, storage uint64, operator string) error {
102 | return FromContext(ctx).UserUpdateUsedStorage(userId, storage, operator)
103 | }
104 |
105 | func UserUpdate(ctx context.Context, userId int64, data map[string]interface{}) error {
106 | return FromContext(ctx).UserUpdate(userId, data)
107 | }
108 |
109 | func UserUpdateBanStatus(ctx context.Context, userId int64, newBanStatus bool) error {
110 | return FromContext(ctx).UserUpdateBanStatus(userId, newBanStatus)
111 | }
112 |
113 | func UserListByUserIds(ctx context.Context, userIds []interface{}) ([]*model.User, error) {
114 | return FromContext(ctx).UserListByUserIds(userIds)
115 | }
116 |
117 | func UserList(ctx context.Context, offset, limit int64) (user []*model.User, count int64, err error) {
118 | return FromContext(ctx).UserList(offset, limit)
119 | }
120 |
--------------------------------------------------------------------------------
/store/db_store/certificate.go:
--------------------------------------------------------------------------------
1 | package db_store
2 |
3 | import (
4 | "github.com/jinzhu/gorm"
5 | "github.com/wq1019/cloud_disk/model"
6 | )
7 |
8 | type dbCertificate struct {
9 | db *gorm.DB
10 | }
11 |
12 | func (c *dbCertificate) CertificateExist(account string) (bool, error) {
13 | var count uint8
14 | err := c.db.Model(&model.Certificate{}).Where(model.Certificate{Account: account}).Count(&count).Error
15 | if err != nil {
16 | return false, err
17 | }
18 | return count > 0, nil
19 | }
20 |
21 | func (c *dbCertificate) CertificateIsNotExistErr(err error) bool {
22 | return model.CertificateIsNotExistErr(err)
23 | }
24 |
25 | func (c *dbCertificate) CertificateLoadByAccount(account string) (certificate *model.Certificate, err error) {
26 | if account == "" {
27 | return nil, model.ErrCertificateNotExist
28 | }
29 | certificate = &model.Certificate{}
30 | err = c.db.Where(model.Certificate{Account: account}).First(&certificate).Error
31 | if gorm.IsRecordNotFoundError(err) {
32 | err = model.ErrCertificateNotExist
33 | }
34 | return
35 | }
36 |
37 | func (c *dbCertificate) CertificateCreate(certificate *model.Certificate) error {
38 | return c.db.Create(certificate).Error
39 | }
40 |
41 | func (c *dbCertificate) CertificateUpdate(oldAccount, newAccount string, certificateType model.CertificateType) error {
42 | return c.db.Model(&model.User{}).
43 | Where("account", oldAccount).
44 | Where("type", certificateType).
45 | UpdateColumn("account", newAccount).Error
46 | }
47 |
48 | func NewDBCertificate(db *gorm.DB) model.CertificateStore {
49 | return &dbCertificate{db: db}
50 | }
51 |
--------------------------------------------------------------------------------
/store/db_store/context.go:
--------------------------------------------------------------------------------
1 | package db_store
2 |
3 | import (
4 | "context"
5 | "github.com/jinzhu/gorm"
6 | )
7 |
8 | type dbKey struct{}
9 |
10 | func NewDBContext(ctx context.Context, db *gorm.DB) context.Context {
11 | return context.WithValue(ctx, dbKey{}, db)
12 | }
13 |
14 | func FromDBContext(ctx context.Context) *gorm.DB {
15 | return ctx.Value(dbKey{}).(*gorm.DB)
16 | }
17 |
--------------------------------------------------------------------------------
/store/db_store/file.go:
--------------------------------------------------------------------------------
1 | package db_store
2 |
3 | import (
4 | "github.com/jinzhu/gorm"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/cloud_disk/model"
7 | )
8 |
9 | type dbFile struct {
10 | db *gorm.DB
11 | }
12 |
13 | func (f *dbFile) LoadFile(folderId, fileId, userId int64) (file *model.File, err error) {
14 | file = &model.File{}
15 | err = f.db.Table("folders fo").
16 | Select("ff.file_id as id, ff.filename, f.hash, f.format, f.extra, f.size, f.created_at, f.updated_at").
17 | Joins("LEFT JOIN `folder_files` ff ON ff.folder_id = fo.id").
18 | Joins("LEFT JOIN `files` f ON f.id = ff.origin_file_id").
19 | Where("fo.id = ? AND fo.user_id = ? AND ff.file_id = ?", folderId, userId, fileId).
20 | Limit(1).
21 | Scan(&file).
22 | Error
23 | if err != nil {
24 | if gorm.IsRecordNotFoundError(err) {
25 | err = errors.RecordNotFound("文件不存在")
26 | }
27 | return
28 | }
29 | return
30 | }
31 |
32 | func (f *dbFile) RenameFile(folderId, fileId int64, newName string) (err error) {
33 | var count int
34 | f.db.Model(model.FolderFile{}).Where("folder_id = ? AND filename = ?", folderId, newName).Limit(1).Count(&count)
35 | if count > 0 {
36 | return errors.FileAlreadyExist("该目录下已经存在同名文件")
37 | } else {
38 | err = f.db.Model(model.FolderFile{}).
39 | Where("file_id = ?", fileId).
40 | Update("filename", newName).
41 | Error
42 | if gorm.IsRecordNotFoundError(err) {
43 | err = errors.RecordNotFound("文件不存在")
44 | }
45 | }
46 | return err
47 | }
48 |
49 | // fromId != toId
50 | func (f *dbFile) CopyFile(fromId, toId int64, fileIds []int64) (totalSize uint64, err error) {
51 | savedFileIds := make([]int64, 0, len(fileIds))
52 | for _, fileId := range fileIds {
53 | var (
54 | fromFile model.FolderFile
55 | count int
56 | )
57 | // 查询源文件信息
58 | err = f.db.Model(model.FolderFile{}).
59 | Where("`folder_id` = ? AND `file_id` = ?", fromId, fileId).
60 | First(&fromFile).
61 | Error
62 | if err != nil {
63 | if gorm.IsRecordNotFoundError(err) {
64 | continue
65 | }
66 | return
67 | }
68 | // 查询目标目录有没有同名文件
69 | err = f.db.Model(model.FolderFile{}).
70 | Where("`folder_id` = ? AND `filename` = ?", toId, fromFile.Filename).
71 | Limit(1).
72 | Count(&count).
73 | Error
74 | if err != nil {
75 | return
76 | }
77 | // 移动到的目录已经存在同名文件
78 | if count > 0 {
79 | continue
80 | }
81 | err = f.db.Create(&model.FolderFile{
82 | OriginFileId: fromFile.OriginFileId,
83 | FolderId: toId,
84 | Filename: fromFile.Filename,
85 | }).Error
86 | if err != nil {
87 | return
88 | }
89 | savedFileIds = append(savedFileIds, fromFile.OriginFileId)
90 | }
91 | // 计算复制的文件大小
92 | if len(savedFileIds) > 0 {
93 | fileSizes := make([]int64, 0, len(savedFileIds))
94 | f.db.Table("files").Where("id IN (?)", savedFileIds).Pluck("size", &fileSizes)
95 | for _, size := range fileSizes {
96 | totalSize += uint64(size)
97 | }
98 | }
99 | return
100 | }
101 |
102 | func (f *dbFile) MoveFile(fromId, toId int64, fileIds []int64) (err error) {
103 | for _, fileId := range fileIds {
104 | var (
105 | fromFile model.FolderFile
106 | count int
107 | )
108 | err = f.db.Model(model.FolderFile{}).
109 | Where("`folder_id` = ? AND `file_id` = ?", fromId, fileId).
110 | First(&fromFile).
111 | Error
112 | if err != nil {
113 | if gorm.IsRecordNotFoundError(err) {
114 | continue
115 | }
116 | return
117 | }
118 | err = f.db.Model(model.FolderFile{}).
119 | Where("`folder_id` = ? AND `filename` = ?", toId, fromFile.Filename).
120 | Limit(1).
121 | Count(&count).
122 | Error
123 | if err != nil {
124 | return
125 | }
126 | // 移动到的目录已经存在同名文件
127 | if count > 0 {
128 | continue
129 | } else {
130 | err = f.db.Model(&fromFile).Update("folder_id", toId).Error
131 | if err != nil {
132 | return
133 | }
134 | }
135 | }
136 | return
137 | }
138 |
139 | func (f *dbFile) DeleteFile(ids []int64, folderId int64) (allowDelFileHashList []string, err error) {
140 | allowDelFileHashList = make([]string, 0, len(ids))
141 | var originIds []int64
142 |
143 | err = f.db.Table("folder_files").
144 | Where("`folder_id` = ? AND `file_id` IN (?)", folderId, ids).
145 | Pluck("DISTINCT `origin_file_id`", &originIds).
146 | Error
147 | if err != nil {
148 | return
149 | }
150 | for _, originId := range originIds {
151 | var count int8
152 | err = f.db.Table("folder_files").
153 | Where("`origin_file_id` = ?", originId).
154 | Limit(2).
155 | Count(&count).
156 | Error
157 | if err != nil {
158 | return
159 | }
160 | // 如果源文件被引用超过一次则表示别的目录或者别的用户也使用了这个文件, 就不用删除该文件, 只要删除该目录和该文件之间的关联即可
161 | if count <= 1 {
162 | f.db.Table("files").
163 | Where("`id` = ?", originId).
164 | Pluck("`hash`", &allowDelFileHashList)
165 | }
166 | }
167 | if len(originIds) > 0 {
168 | // 删除目录和文件之间的关联
169 | err = f.db.Exec("DELETE FROM `folder_files` WHERE `folder_id` = ? AND `file_id` IN (?)", folderId, ids).Error
170 | }
171 | if len(allowDelFileHashList) > 0 {
172 | // 在数据库中删除所有被引用了一次的文件
173 | f.db.Exec("DELETE FROM `files` WHERE `hash` IN (?)", allowDelFileHashList)
174 | }
175 | return
176 | }
177 |
178 | func (f *dbFile) SaveFileToFolder(file *model.File, folderId int64) (err error) {
179 | err = f.db.Model(model.File{}).Create(
180 | &model.FolderFile{
181 | FolderId: folderId,
182 | OriginFileId: file.Id,
183 | Filename: file.Filename,
184 | }).Error
185 | return
186 | }
187 |
188 | func NewDBFile(db *gorm.DB) model.FileStore {
189 | return &dbFile{db}
190 | }
191 |
--------------------------------------------------------------------------------
/store/db_store/folder.go:
--------------------------------------------------------------------------------
1 | package db_store
2 |
3 | import (
4 | "fmt"
5 | "github.com/emirpasic/gods/sets/hashset"
6 | "github.com/jinzhu/gorm"
7 | "github.com/wq1019/cloud_disk/errors"
8 | "github.com/wq1019/cloud_disk/model"
9 | "strconv"
10 | "strings"
11 | )
12 |
13 | type dbFolder struct {
14 | db *gorm.DB
15 | }
16 |
17 | func (f *dbFolder) ListFolder(folderIds []int64, userId int64) (folders []*model.Folder, err error) {
18 | // 去重
19 | ids := hashset.New()
20 | for _, v := range folderIds {
21 | ids.Add(v)
22 | }
23 | if ids.Size() <= 0 {
24 | return nil, errors.RecordNotFound("没有目录")
25 | }
26 | folders = make([]*model.Folder, 10)
27 | err = f.db.Model(&model.Folder{}).
28 | Where("user_id = ? AND id IN (?)", userId, ids.Values()).
29 | Find(&folders).
30 | Error
31 | return
32 | }
33 |
34 | func (f *dbFolder) RenameFolder(id, currentFolderId int64, newName string) (err error) {
35 | var count int
36 | err = f.db.Table("`folders` fo").
37 | Where("fo.parent_id = ? AND fo.folder_name = ?", currentFolderId, newName).
38 | Limit(1).
39 | Count(&count).
40 | Error
41 | if err != nil {
42 | return
43 | }
44 | if count > 0 {
45 | return model.FolderAlreadyExisted
46 | } else {
47 | err = f.db.Model(model.Folder{}).
48 | Where("id = ?", id).
49 | Update("folder_name", newName).
50 | Error
51 | }
52 | return
53 | }
54 |
55 | func (f *dbFolder) CopyFolder(to *model.Folder, waitCopyFoders []*model.Folder) (totalSize uint64, err error) {
56 | var (
57 | toId2Str string // 移动到的目录 ID 字符串形式
58 | userId = to.UserId // 用户 id
59 | )
60 | toId2Str = strconv.FormatInt(to.Id, 10)
61 | for _, waitFolder := range waitCopyFoders {
62 | var (
63 | waitCopyId2Str = strconv.FormatInt(waitFolder.Id, 10) // 等待移动的目录ID string 形式
64 | children = make([]*model.Folder, 0, 5)
65 | idMap = make(map[int64]int64, 3)
66 | pIdMap = make(map[int64]int64, 3)
67 | )
68 | // 查询所有子目录
69 | f.db.Model(model.Folder{}).Where("`key` LIKE ?", waitFolder.Key+waitCopyId2Str+"-%").Order("id ASC").Find(&children)
70 | newRootFolder := model.Folder{
71 | UserId: userId,
72 | FolderName: waitFolder.FolderName,
73 | Level: to.Level + 1,
74 | ParentId: to.Id, // new parentID
75 | Key: to.Key + toId2Str + model.FolderKeyPrefix,
76 | }
77 | var count int
78 | f.db.Model(model.Folder{}).Where("user_id = ? AND folder_name = ? AND parent_id = ?",
79 | userId, newRootFolder.FolderName, newRootFolder.ParentId).Limit(1).Count(&count)
80 | if count > 0 {
81 | continue // 目录已存在, 跳过直接复制下一个目录
82 | }
83 | // 创建一个与原根目录相等的根目录
84 | f.db.Create(&newRootFolder)
85 | idMap[waitFolder.Id] = newRootFolder.Id
86 | pIdMap[newRootFolder.Id] = 0
87 |
88 | // 创建子目录
89 | newFolders := make(map[int64]*model.Folder, len(children))
90 | for i := 0; i < len(children); i++ {
91 | newChildFolder := model.Folder{
92 | UserId: userId,
93 | FolderName: children[i].FolderName,
94 | Key: children[i].Key, // default
95 | ParentId: children[i].ParentId, // default
96 | Level: newRootFolder.Level + (children[i].Level - waitFolder.Level), // must >0
97 | }
98 | f.db.Create(&newChildFolder)
99 | idMap[children[i].Id] = newChildFolder.Id
100 | pIdMap[newChildFolder.Id] = newChildFolder.ParentId
101 |
102 | newFolders[newChildFolder.Id] = &newChildFolder
103 | }
104 | // 更新所有新的 child 目录 的 key 和 parentId
105 | for id, folder := range newFolders {
106 | key := newRootFolder.Key
107 | tmpKey := ""
108 | pId := folder.ParentId
109 | for i := int64(0); i < folder.Level-newRootFolder.Level; i++ {
110 | tmpKey = fmt.Sprintf("%d-", idMap[pId]) + tmpKey
111 | pId = pIdMap[idMap[pId]]
112 | }
113 | newParentId := idMap[folder.ParentId]
114 | f.db.Model(model.Folder{}).Where("id = ?", id).Updates(model.Folder{
115 | Key: key + tmpKey,
116 | ParentId: newParentId,
117 | })
118 | }
119 | // 创建新的文件关联
120 | type FolderFile struct {
121 | FolderId int64
122 | FileId int64
123 | }
124 | var (
125 | oldFolderIds []int64
126 | folderFiles []*FolderFile
127 | )
128 | for k := range idMap {
129 | oldFolderIds = append(oldFolderIds, k)
130 | }
131 | err = f.db.Table("folder_files").Where("folder_id IN (?)", oldFolderIds).Scan(&folderFiles).Error
132 | if err != nil {
133 | return
134 | }
135 | // 文件索引创建,因为目录都是新创建的,所以不可能会出现文件已存在的情况
136 | sql := "INSERT INTO `folder_files` SELECT ?,`origin_file_id`,`filename`,NULL FROM `folder_files` WHERE `folder_id` = ? AND `file_id` = ?"
137 | for _, v := range folderFiles {
138 | newFolderId := idMap[v.FolderId]
139 | rowsAffected := f.db.Exec(sql, newFolderId, v.FolderId, v.FileId).RowsAffected
140 | if rowsAffected > 0 {
141 | sizes := make([]int64, 0, 1)
142 | // 成功复制一个文件索引就为用户的使用空间加上这个文件占用的空间
143 | f.db.Table("folder_files ff").
144 | Joins("LEFT JOIN `files` f ON ff.origin_file_id = f.id").
145 | Where("ff.file_id = ?", v.FileId).Pluck("f.size", &sizes)
146 | totalSize += uint64(sizes[0])
147 | }
148 | }
149 | }
150 | return
151 | }
152 |
153 | func (f *dbFolder) MoveFolder(to *model.Folder, ids []int64) (err error) {
154 | var (
155 | rootFolder model.Folder // 将要移动的第一层目录
156 | toId2Str string // 移动到的目录 ID 字符串形式
157 | tmpFolder model.Folder // 临时 folder
158 | children []*model.Folder // 子目录
159 | id2Str string // 移动的目录的 ID 字符串形式
160 | )
161 | toId2Str = strconv.FormatInt(to.Id, 10)
162 | for _, id := range ids {
163 | id2Str = strconv.FormatInt(id, 10)
164 | err := f.db.First(&rootFolder, "id = ?", id).Error
165 | if err != nil {
166 | if gorm.IsRecordNotFoundError(err) {
167 | continue
168 | }
169 | return err
170 | }
171 | // 查询所有子目录
172 | f.db.Model(model.Folder{}).Where("`key` LIKE ?", rootFolder.Key+id2Str+"-%").Find(&children)
173 |
174 | tmpFolder = rootFolder
175 | // 更新根目录的信息
176 | f.db.Model(&rootFolder).Updates(model.Folder{
177 | Level: to.Level + 1,
178 | ParentId: to.Id,
179 | Key: to.Key + toId2Str + model.FolderKeyPrefix,
180 | })
181 | for _, child := range children {
182 | f.db.Model(&child).Updates(model.Folder{
183 | Level: rootFolder.Level + (child.Level - tmpFolder.Level),
184 | Key: updateKey(rootFolder.Key, child.Key, id2Str),
185 | })
186 | }
187 | children = nil
188 | rootFolder = model.Folder{}
189 | }
190 | return nil
191 | }
192 |
193 | func (f *dbFolder) DeleteFolder(ids []int64, userId int64) (allowDelFileHashList []string, err error) {
194 | var (
195 | waitDelFolderIds []int64
196 | likeSql string
197 | )
198 | allowDelFileHashList = make([]string, 0, len(ids)*2)
199 | for _, v := range ids {
200 | relativeRootFolder := model.Folder{}
201 | conditions := fmt.Sprintf("id = %d AND user_id = %d", v, userId)
202 | err := f.db.First(&relativeRootFolder, conditions).Error
203 | if err != nil {
204 | if gorm.IsRecordNotFoundError(err) {
205 | continue
206 | }
207 | return nil, err
208 | }
209 | // 将父目录的 ID 放到待删除的目录列表, 准备删除该目录下面的文件
210 | waitDelFolderIds = append(waitDelFolderIds, relativeRootFolder.Id)
211 | // 在数据库中列出所有子目录 ID
212 | id2Str := strconv.FormatInt(relativeRootFolder.Id, 10)
213 | likeSql += fmt.Sprintf(" `key` LIKE %s OR", "'"+relativeRootFolder.Key+id2Str+"-%'")
214 | }
215 | if likeSql == "" {
216 | return nil, errors.RecordNotFound("没有要删除的记录")
217 | }
218 | likeSql = strings.TrimRight(likeSql, "OR")
219 | f.db.Model(model.Folder{}).
220 | Where(likeSql).
221 | Pluck("DISTINCT id", &waitDelFolderIds)
222 |
223 | // 删除父目录以及下面的所有子目录
224 | f.db.Delete(&model.Folder{}, "id IN (?)", waitDelFolderIds)
225 |
226 | // 统计每个文件的引用次数, 如果该文件只被引用了一次, 则可以去 minio 中将这个文件直接删除
227 | var originFileIds []int64
228 | f.db.Table("folder_files").
229 | Where("folder_id IN (?)", waitDelFolderIds).
230 | Pluck("DISTINCT origin_file_id", &originFileIds)
231 | for _, id := range originFileIds {
232 | var count int8
233 | f.db.Table("folder_files").
234 | Where("`folder_id` NOT IN (?)", waitDelFolderIds).
235 | Where("`origin_file_id` = ?", id).
236 | Limit(1).Count(&count)
237 | // 如果源文件被引用超过一次则表示别的目录或者别的用户也使用了这个文件, 就不用删除该文件, 只要删除该目录和该文件之间的关联即可
238 | if count <= 0 {
239 | f.db.Table("files").Where("`id` = ?", id).Pluck("`hash`", &allowDelFileHashList)
240 | }
241 | }
242 |
243 | // 删除父目录下面所有子目录中的文件
244 | f.db.Exec("DELETE FROM `folder_files` WHERE folder_id IN (?)", waitDelFolderIds)
245 |
246 | if len(allowDelFileHashList) > 0 {
247 | // 在数据库中删除所有被引用了一次的文件
248 | f.db.Exec("DELETE FROM `files` WHERE `hash` IN (?)", allowDelFileHashList)
249 | }
250 | return
251 | }
252 |
253 | func (f *dbFolder) ExistFolder(userId, parentId int64, folderName string) (isExist bool) {
254 | var (
255 | count uint8
256 | )
257 | f.db.Model(model.Folder{}).
258 | Where("user_id = ? AND folder_name = ? AND parent_id = ?", userId, folderName, parentId).
259 | Limit(1).
260 | Count(&count)
261 | if count > 0 {
262 | isExist = true
263 | }
264 | return
265 | }
266 |
267 | func (f *dbFolder) CreateFolder(folder *model.Folder) (err error) {
268 | err = f.db.Create(&folder).Error
269 | return
270 | }
271 |
272 | func (f *dbFolder) LoadFolder(id, userId int64, isLoadRelated bool) (folder *model.Folder, err error) {
273 | var (
274 | files []*model.File
275 | )
276 | folder = &model.Folder{}
277 | files = make([]*model.File, 0, 1)
278 | q := f.db.Model(model.Folder{})
279 | if isLoadRelated {
280 | q = q.Preload("Folders", "user_id = ?", userId) // 此语句是在 #232 行时才执行的
281 | }
282 | q = q.Where("user_id = ?", userId)
283 | // 如果没有传目录id表示加载根目录
284 | if id == 0 {
285 | q = q.Where("level = 1")
286 | } else {
287 | q = q.Where("id = ?", id)
288 | }
289 | err = q.First(&folder).Error
290 | if err != nil {
291 | if gorm.IsRecordNotFoundError(err) {
292 | err = errors.RecordNotFound("目录不存在")
293 | }
294 | return nil, err
295 | }
296 | if isLoadRelated {
297 | f.db.Table("folders fo").
298 | Select("ff.file_id as id, ff.filename, f.hash, f.format, f.extra, f.size, f.created_at, f.updated_at").
299 | Joins("INNER JOIN `folder_files` ff ON ff.folder_id = fo.id").
300 | Joins("INNER JOIN `files` f ON f.id = ff.origin_file_id").
301 | Where("fo.id = ?", folder.Id).Find(&files)
302 | }
303 | folder.Files = files
304 | return
305 | }
306 |
307 | func (f *dbFolder) LoadSimpleFolder(id, userId int64) (folder *model.SimpleFolder, err error) {
308 | folder = &model.SimpleFolder{}
309 | files := make([]*model.SimpleFile, 0, 1)
310 |
311 | if id == 0 {
312 | return nil, errors.NotFound("目录 ID 不能为空")
313 | }
314 | if userId == 0 {
315 | return nil, errors.NotFound("用户 ID 不能为空")
316 | }
317 | q := f.db.Model(model.Folder{})
318 | f.db.Table("folders fo").
319 | Select("ff.file_id as id, ff.filename").
320 | Joins("LEFT JOIN `folder_files` ff ON ff.folder_id = fo.id").
321 | Where("fo.id = ?", id).Scan(&files)
322 |
323 | q = q.Where("id = ? AND user_id = ?", id, userId)
324 | err = q.Scan(&folder).Error
325 | if err != nil {
326 | if gorm.IsRecordNotFoundError(err) {
327 | err = errors.RecordNotFound("目录不存在")
328 | }
329 | return nil, err
330 | }
331 | folder.Files = files
332 | return
333 | }
334 |
335 | func updateKey(parentKey, key, startId string) string {
336 | keys := strings.Split(key, "-")
337 | for index, key := range keys {
338 | if key == startId {
339 | return parentKey + strings.Join(keys[index:], "-")
340 | }
341 | }
342 | return ""
343 | }
344 |
345 | func NewDBFolder(db *gorm.DB) model.FolderStore {
346 | return &dbFolder{db}
347 | }
348 |
--------------------------------------------------------------------------------
/store/db_store/folder_file.go:
--------------------------------------------------------------------------------
1 | package db_store
2 |
3 | import (
4 | "fmt"
5 | "github.com/jinzhu/gorm"
6 | "github.com/wq1019/cloud_disk/model"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | type dbFolderFile struct {
12 | db *gorm.DB
13 | }
14 |
15 | func (f *dbFolderFile) LoadFolderFilesByFolderIdAndFileIds(folderId int64, fileIds []int64, userId int64) (folderFiles []*model.WrapFolderFile, err error) {
16 | folderFiles = make([]*model.WrapFolderFile, 0, 10)
17 | err = f.db.Table("folders fo").
18 | Select("ff.folder_id,ff.file_id,ff.filename, f.size as file_size,format").
19 | Joins("LEFT JOIN `folder_files` ff ON ff.folder_id = fo.id").
20 | Joins("LEFT JOIN `files` f ON ff.origin_file_id = f.id").
21 | Where("fo.id = ? AND fo.user_id = ? AND ff.file_id IN (?)", folderId, userId, fileIds).
22 | Find(&folderFiles).Error
23 | return
24 | }
25 |
26 | func (f *dbFolderFile) LoadFolderFilesByFolderIds(folderIds []int64, userId int64) (folderFiles []*model.WrapFolderFile, err error) {
27 | var (
28 | allFolderId []int64
29 | likeSql string
30 | )
31 | folderFiles = make([]*model.WrapFolderFile, 0, 10)
32 | for _, v := range folderIds {
33 | parent := model.Folder{}
34 | conditions := fmt.Sprintf("id = %d AND user_id = %d", v, userId)
35 | err := f.db.First(&parent, conditions).Error
36 | if err != nil {
37 | if gorm.IsRecordNotFoundError(err) {
38 | continue
39 | }
40 | return nil, err
41 | }
42 | // 将父目录的 ID 放到目录列表
43 | allFolderId = append(allFolderId, parent.Id)
44 | // 在数据库中列出所有子目录 ID
45 | id2Str := strconv.FormatInt(parent.Id, 10)
46 | likeSql += fmt.Sprintf(" `key` LIKE %s OR", "'"+parent.Key+id2Str+"-%'")
47 | }
48 | likeSql = strings.TrimRight(likeSql, "OR")
49 | f.db.Model(model.Folder{}).
50 | Where(likeSql).
51 | Pluck("DISTINCT id", &allFolderId)
52 | // 查找父目录下面所有子目录中的文件ID
53 | f.db.Table("folder_files ff").
54 | Select("ff.folder_id,ff.file_id,ff.filename, f.size as file_size,f.format").
55 | Joins("LEFT JOIN `files` f ON ff.origin_file_id = f.id").
56 | Where("ff.folder_id IN (?)", allFolderId).
57 | Find(&folderFiles)
58 |
59 | return folderFiles, err
60 | }
61 |
62 | func (f *dbFolderFile) ExistFile(filename string, folderId, userId int64) (isExist bool, err error) {
63 | var count int
64 | err = f.db.Table("folders fo").
65 | Joins("LEFT JOIN `folder_files` ff ON ff.folder_id = fo.id").
66 | Where("fo.id = ? AND fo.user_id = ? AND ff.filename = ?", folderId, userId, filename).Limit(1).
67 | Count(&count).Error
68 | if err != nil {
69 | return false, err
70 | }
71 | return count > 0, err
72 | }
73 |
74 | func NewDBFolderFile(db *gorm.DB) model.FolderFileStore {
75 | return &dbFolderFile{db}
76 | }
77 |
--------------------------------------------------------------------------------
/store/db_store/group.go:
--------------------------------------------------------------------------------
1 | package db_store
2 |
3 | import (
4 | "github.com/jinzhu/gorm"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/cloud_disk/model"
7 | )
8 |
9 | type dbGroup struct {
10 | db *gorm.DB
11 | }
12 |
13 | func (g *dbGroup) GroupCreate(group *model.Group) (err error) {
14 | err = g.db.Create(&group).Error
15 | return
16 | }
17 |
18 | func (g *dbGroup) GroupExist(name string) (isExist bool, err error) {
19 | var count int8
20 | err = g.db.Model(model.Group{}).Where("name = ?", name).Limit(1).Count(&count).Error
21 | isExist = count > 0
22 | return
23 | }
24 |
25 | func (g *dbGroup) GroupDelete(id int64) (err error) {
26 | group := model.Group{}
27 | err = g.db.Where("id = ?", id).First(&group).Error
28 | if err != nil {
29 | if gorm.IsRecordNotFoundError(err) {
30 | err = errors.RecordNotFound("用户组不存在")
31 | }
32 | return err
33 | }
34 | userCount := g.db.Model(&group).Association("Users").Count()
35 | if userCount > 0 {
36 | err = errors.GroupNotAllowBeDelete("该组不允许删除, 因为组里面有用户")
37 | return
38 | }
39 | err = g.db.Delete(&group).Error
40 | return err
41 | }
42 |
43 | func (g *dbGroup) GroupUpdate(id int64, data map[string]interface{}) (err error) {
44 | if id <= 0 {
45 | return model.ErrGroupNotExist
46 | }
47 | return g.db.Model(model.Group{Id: id}).Select("name", "max_storage", "allow_share").Updates(data).Error
48 | }
49 |
50 | func (g *dbGroup) GroupList(offset, limit int64) (groups []*model.WrapGroupList, count int64, err error) {
51 | groups = make([]*model.WrapGroupList, 0, 10)
52 | err = g.db.Table("`groups` g").
53 | Select("g.*,count(u.id) as user_count").
54 | Joins("LEFT JOIN `users` u ON u.group_id = g.id").
55 | Group("g.id").
56 | Offset(offset).
57 | Limit(limit).
58 | Scan(&groups).
59 | Count(&count).
60 | Error
61 | return
62 | }
63 |
64 | func NewDBGroup(db *gorm.DB) model.GroupStore {
65 | return &dbGroup{db}
66 | }
67 |
--------------------------------------------------------------------------------
/store/db_store/share.go:
--------------------------------------------------------------------------------
1 | package db_store
2 |
3 | import (
4 | "github.com/jinzhu/gorm"
5 | "github.com/wq1019/cloud_disk/model"
6 | )
7 |
8 | type dbShare struct {
9 | db *gorm.DB
10 | }
11 |
12 | func NewDBShare(db *gorm.DB) model.ShareStore {
13 | return &dbShare{db}
14 | }
15 |
--------------------------------------------------------------------------------
/store/db_store/ticket.go:
--------------------------------------------------------------------------------
1 | package db_store
2 |
3 | import (
4 | "github.com/jinzhu/gorm"
5 | "github.com/wq1019/cloud_disk/model"
6 | )
7 |
8 | type dbTicket struct {
9 | db *gorm.DB
10 | }
11 |
12 | func (dt *dbTicket) TicketIsNotExistErr(err error) bool {
13 | return model.TicketIsNotExistErr(err)
14 | }
15 |
16 | func (dt *dbTicket) TicketLoad(id string) (ticket *model.Ticket, err error) {
17 | if id == "" {
18 | return nil, model.ErrTicketNotExist
19 | }
20 | ticket = &model.Ticket{}
21 | err = dt.db.Where(model.Ticket{Id: id}).First(ticket).Error
22 | if gorm.IsRecordNotFoundError(err) {
23 | err = model.ErrTicketNotExist
24 | }
25 | return
26 | }
27 |
28 | func (dt *dbTicket) TicketCreate(ticket *model.Ticket) error {
29 | return dt.db.Create(ticket).Error
30 | }
31 |
32 | func (dt *dbTicket) TicketDelete(id string) error {
33 | return dt.db.Delete(model.Ticket{Id: id}).Error
34 | }
35 |
36 | func NewDBTicket(db *gorm.DB) model.TicketStore {
37 | return &dbTicket{db: db}
38 | }
39 |
--------------------------------------------------------------------------------
/store/db_store/user.go:
--------------------------------------------------------------------------------
1 | package db_store
2 |
3 | import (
4 | "github.com/jinzhu/gorm"
5 | "github.com/wq1019/cloud_disk/errors"
6 | "github.com/wq1019/cloud_disk/model"
7 | )
8 |
9 | type dbUser struct {
10 | db *gorm.DB
11 | }
12 |
13 | func (u *dbUser) UserUpdateUsedStorage(userId int64, storage uint64, operator string) (err error) {
14 | if userId <= 0 {
15 | return model.ErrUserNotExist
16 | }
17 | switch operator {
18 | case "+":
19 | case "-":
20 | default:
21 | return model.ErrorOperatorNotValid
22 | }
23 | return u.db.Model(model.User{Id: userId}).
24 | UpdateColumn("used_storage", gorm.Expr("used_storage "+operator+" ?", storage)).Error
25 | }
26 |
27 | func (u *dbUser) UserExist(id int64) (bool, error) {
28 | var count uint8
29 | err := u.db.Model(model.User{}).Where(model.User{Id: id}).Count(&count).Error
30 | if err != nil {
31 | return false, err
32 | }
33 | return count > 0, nil
34 | }
35 |
36 | func (u *dbUser) UserIsNotExistErr(err error) bool {
37 | return model.UserIsNotExistErr(err)
38 | }
39 |
40 | func (u *dbUser) UserLoad(id int64) (user *model.User, err error) {
41 | if id <= 0 {
42 | return nil, model.ErrUserNotExist
43 | }
44 | user = &model.User{}
45 | err = u.db.Where(model.User{Id: id}).First(user).Error
46 | if gorm.IsRecordNotFoundError(err) {
47 | err = model.ErrUserNotExist
48 | }
49 | return
50 | }
51 |
52 | func (u *dbUser) UserLoadAndRelated(userId int64) (user *model.User, err error) {
53 | user, err = u.UserLoad(userId)
54 | if err != nil {
55 | return
56 | }
57 | group := &model.Group{}
58 | err = u.db.Where("id = ?", user.GroupId).First(&group).Error
59 | if gorm.IsRecordNotFoundError(err) {
60 | err = errors.RecordNotFound("用户组不存在")
61 | }
62 | user.Group = group
63 | return
64 | }
65 |
66 | func (u *dbUser) UserUpdate(userId int64, data map[string]interface{}) error {
67 | if userId <= 0 {
68 | return model.ErrUserNotExist
69 | }
70 | return u.db.Model(model.User{Id: userId}).
71 | Select(
72 | "name", "gender", "password", "is_ban", "group_id",
73 | "is_admin", "nickname", "email", "avatar_hash", "profile",
74 | ).
75 | Updates(data).Error
76 | }
77 |
78 | func (u *dbUser) UserCreate(user *model.User) (err error) {
79 | err = u.db.Create(&user).Error
80 | return
81 | }
82 |
83 | func (u *dbUser) UserListByUserIds(userIds []interface{}) (users []*model.User, err error) {
84 | if len(userIds) == 0 {
85 | return
86 | }
87 | users = make([]*model.User, 0, len(userIds))
88 | err = u.db.Where("id in (?)", userIds).
89 | Set("gorm:auto_preload", true).
90 | Find(&users).
91 | Error
92 | return
93 | }
94 |
95 | func (u *dbUser) UserList(offset, limit int64) (users []*model.User, count int64, err error) {
96 | users = make([]*model.User, 0, 10)
97 | err = u.db.Preload("Group").
98 | Offset(offset).
99 | Limit(limit).
100 | Find(&users).
101 | Count(&count).
102 | Error
103 | return
104 | }
105 |
106 | func NewDBUser(db *gorm.DB) model.UserStore {
107 | return &dbUser{db: db}
108 | }
109 |
--------------------------------------------------------------------------------
/store/redis_store/ticket.go:
--------------------------------------------------------------------------------
1 | package redis_store
2 |
3 | import (
4 | "github.com/go-redis/redis"
5 | "github.com/vmihailenco/msgpack"
6 | "github.com/wq1019/cloud_disk/model"
7 | )
8 |
9 | type redisTicket struct {
10 | client *redis.Client
11 | }
12 |
13 | func (rt *redisTicket) id2key(id string) string {
14 | return "ticket:" + id
15 | }
16 |
17 | func (rt *redisTicket) TicketIsNotExistErr(err error) bool {
18 | return model.TicketIsNotExistErr(err)
19 | }
20 |
21 | func (rt *redisTicket) TicketLoad(id string) (ticket *model.Ticket, err error) {
22 |
23 | if id == "" {
24 | return nil, model.ErrTicketNotExist
25 | }
26 |
27 | res, err := rt.client.Get(rt.id2key(id)).Result()
28 | if err != nil {
29 | if err == redis.Nil {
30 | err = model.ErrTicketNotExist
31 | }
32 | return nil, err
33 | }
34 | ticket = &model.Ticket{}
35 | if err = msgpack.Unmarshal([]byte(res), ticket); err != nil {
36 | return nil, err
37 | }
38 | return
39 | }
40 |
41 | func (rt *redisTicket) TicketCreate(ticket *model.Ticket) error {
42 | key := rt.id2key(ticket.Id)
43 | if res, err := rt.client.Exists(key).Result(); err != nil {
44 | return err
45 | } else if res != 0 {
46 | return model.ErrTicketExisted
47 | }
48 |
49 | b, err := msgpack.Marshal(ticket)
50 | if err != nil {
51 | return err
52 | }
53 | return rt.client.Set(key, b, ticket.ExpiredAt.Sub(ticket.CreatedAt)).Err()
54 | }
55 |
56 | func (rt *redisTicket) TicketDelete(id string) error {
57 | return rt.client.Del(rt.id2key(id)).Err()
58 | }
59 |
60 | func NewRedisTicket(client *redis.Client) model.TicketStore {
61 | return &redisTicket{client: client}
62 | }
63 |
--------------------------------------------------------------------------------
/store/store.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "github.com/go-redis/redis"
5 | "github.com/jinzhu/gorm"
6 | "github.com/wq1019/cloud_disk/model"
7 | "github.com/wq1019/cloud_disk/store/db_store"
8 | "github.com/wq1019/cloud_disk/store/redis_store"
9 | )
10 |
11 | type Store interface {
12 | model.TicketStore
13 | model.UserStore
14 | model.CertificateStore
15 | model.FileStore
16 | model.ShareStore
17 | model.FolderStore
18 | model.GroupStore
19 | model.FolderFileStore
20 | }
21 |
22 | type store struct {
23 | model.TicketStore
24 | model.UserStore
25 | model.CertificateStore
26 | model.FileStore
27 | model.ShareStore
28 | model.FolderStore
29 | model.GroupStore
30 | model.FolderFileStore
31 | }
32 |
33 | func NewStore(db *gorm.DB, redisClient *redis.Client) Store {
34 | return &store{
35 | redis_store.NewRedisTicket(redisClient),
36 | db_store.NewDBUser(db),
37 | db_store.NewDBCertificate(db),
38 | db_store.NewDBFile(db),
39 | db_store.NewDBShare(db),
40 | db_store.NewDBFolder(db),
41 | db_store.NewDBGroup(db),
42 | db_store.NewDBFolderFile(db),
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/bytesize_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "github.com/wq1019/cloud_disk/pkg/bytesize"
5 | "testing"
6 | )
7 |
8 | func TestByteSize(t *testing.T) {
9 | var tests = []struct {
10 | size uint64
11 | result string
12 | }{
13 | // Basic power tests.
14 | {0, "0B"},
15 | {1024, "1.00KB"},
16 | {1024 * 1024, "1.00MB"},
17 | {1024 * 1024 * 1024, "1.00GB"},
18 | {1024 * 1024 * 1024 * 1024, "1.00TB"},
19 | {1024 * 1024 * 1024 * 1024 * 1024, "1.00PB"},
20 | {1024 * 1024 * 1024 * 1024 * 1024 * 1024, "1.00EB"},
21 |
22 | {500, "500B"},
23 | {1000, "1000B"},
24 | {1030, "1.01KB"}, // Test for rounding. 1030B =~ 1.00586KB
25 | {2000, "1.95KB"},
26 | {1.5 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024, "1.50EB"},
27 | }
28 |
29 | for i, test := range tests {
30 | result := bytesize.ByteSize(test.size)
31 | if result != test.result {
32 | t.Errorf("#%d: byteSize(%d)=%s; want %s",
33 | i, test.size, result, test.result)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/example_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import "testing"
4 |
5 | func TestSum(t *testing.T) {
6 | numbers := []int{1, 2, 3, 4, 5}
7 | expected := 15
8 | actual := Sum(numbers)
9 |
10 | if actual != expected {
11 | t.Errorf("Expected the sum of %v to be %d but instead got %d!", numbers, expected, actual)
12 |
13 | }
14 | }
15 |
16 | func Sum(numbers []int) int {
17 | sum := 0
18 | for _, n := range numbers {
19 | sum += n
20 | }
21 | return sum
22 | }
23 |
--------------------------------------------------------------------------------