├── .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 | [![Build Status](https://www.travis-ci.org/wq1019/cloud_disk.svg?branch=master)](https://www.travis-ci.org/wq1019/cloud_disk) 6 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsunl888%2Fcloud_disk.svg?type=shield)](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 | 37 | 38 | 39 | 40 | 42 | 43 |
JetBrains
JetBrains 41 |
44 | 45 | 46 | ## ⭐ Star 历史 47 | [![Stargazers over time](https://starchart.cc/sunl888/cloud_disk.svg?variant=adaptive)](https://starchart.cc/sunl888/cloud_disk) 48 | 49 | ## License 50 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsunl888%2Fcloud_disk.svg?type=large&issueType=license)](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 | --------------------------------------------------------------------------------