第一版 后端 go、gin、gorm、jwt
代码仓库
实现的功能:鉴权(jwt、casbin) 注释文档(swagger) MinioSDK(minio)
MinioSDK(minio):实现的接口 ListBuckets 存储桶列表 ListObjects 桶内文件列表 删除桶
PutObject 上传文件 RemoveObject 删除文件 PresignedGetObject 获取URL下载
前端上传和查看功能
Minio的研究与使用,建立对象存储服务,提供附加值服务。
1、后端minio-storage
2、前端minio-
docker安装minio
docker run \
-d \
-p 9000:9000 \
-p 9001:9001 \
--name minio \
--restart=always \
-v /www/minio/data:/data \
-e "MINIO_ROOT_USER=minio123" \
-e "MINIO_ROOT_PASSWORD=minio123" \
minio/minio:latest server /data --console-address ":9001"
查看SDK列表页
MinIO Go Client API Reference — MinIO Object Storage for Linux
minio-go例子
minio-go/examples/s3 at master · minio/minio-go · GitHub
实现以下接口:
ListBuckets 存储桶列表
ListObjects 桶内文件列表
PutObject 上传文件
RemoveObject 删除文件
PresignedGetObject 获取URL下载
前端只需要上传和查看功能
docker安装mysql
docker run \
-p 3306:3306 \
--name mysql \
--privileged=true \
--restart=always \
-v /usr/mysql/local/conf:/etc/mysql/conf.d \
-v /usr/mysql/local/logs:/logs \
-v /usr/mysql/local/data:/var/lib/mysql \
-v /usr/mysql/local/mysql-files:/var/lib/mysql-files \
-e MYSQL_ROOT_PASSWORD=1234 \
-e TZ=Asia/Shanghai \
-d docker.io/mysql:latest
第二版 后端 go、go-zero、xorm、jwt
代码仓库github.com/yunixiangfeng/minio_backend
功能设计
用户模块:密码登录、刷新Authorization、邮箱注册、用户详情、用户容量
存储池模块:
中心存储池资源管理-
文件上传
文件秒传
文件分片上传
对接 MinIO
个人存储池资源管理-
文件关联存储
文件列表
文件名称修改
文件夹创建
文件删除
文件移动
文件分享模块:创建分享记录、获取资源详情、资源保存
项目初始化创建
集成 go-zero
mkdir minio-backend
cd minio-backend
# 创建API服务
goctl api new core
cd core
# 启动服务
go run core.go -f etc/core-api.yaml
# 使用api文件生成接口相关代码
goctl api go -api core.api -dir . -style go_zero
用不到微服务和rpc服务,只需要生成单体服务。
生成的单体 api 服务目录:
|____go.mod
|____go.sum
|____core.api // api接口与类型定义
|____etc // 网关层配置文件
| |____core-api.yaml
|____internal
| |____config // 配置-对应etc下配置文件
| | |____config.go
| |____handler // 视图函数层, 路由与处理器
| | |____routes.go
| | |____core_handler.go
| |____logic // 逻辑处理
| | |____core_logic.go
| |____svc // 依赖资源, 封装 rpc 对象的地方
| | |____service_context.go
| |____types // 中间类型
| | |____types.go
|____core.go // main.go 入口
集成xorm
cd minio-backend/core
mkdir models
创建init.go
C:\Users\Administrator\Desktop\minio-backend\core\models\init.go
package models
import (
"log"
_ "github.com/go-sql-driver/mysql"
"xorm.io/xorm"
)
func Init(c config.Config) *xorm.Engine {
// engine, err := xorm.NewEngine("mysql", "root:1234@tcp(192.168.204.130:3306)/cloud-disk?charset=utf8mb4&parseTime=True&loc=Local")
// engine, err := xorm.NewEngine(c.DataBase.Type, c.DataBase.Url)
engine, err := xorm.NewEngine("mysql", c.Mysql.DataSource)
if err != nil {
log.Printf("Xorm New Engine Error:%v", err)
return nil
}
return engine
}
数据库分析
创建数据库minio-backend
数据表设计
用户信息表user_basic:存储用户基本信息,用于登录
公共文件存储池表repository_pool:存储文件信息
用户存储池表user_repository:对公共文件存储池中文件信息的引用
文件分享表share_basic
创建user_basic.go
C:\Users\Administrator\Desktop\minio-backend\core\models\user_basic.go
package models
type UserBasic struct {
Id int
Identity string
Name string
Password string
Email string
}
func (table UserBasic) TableName() string {
return "user_basic"
}
测试代码core\test\xorm_test.go
package test
import (
"bytes"
"core/models"
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
"testing"
"xorm.io/xorm"
)
func TestXormTest(t *testing.T) {
engine, err := xorm.NewEngine("mysql", "root:1234@tcp(192.168.204.130:3306)/minio-backend?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
t.Fatal(err)
}
data := make([]*models.UserBasic, 0)
err = engine.Find(&data)
if err != nil {
t.Fatal(err)
}
b, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}
dst := new(bytes.Buffer)
err = json.Indent(dst, b, "", " ")
if err != nil {
t.Fatal(err)
}
fmt.Println(dst.String())
}
用户模块
密码登录
core\core.api
service core-api {
// 用户登录
@handler UserLogin
post /user/login(LoginRequest) returns(LoginReply)
}
type LoginRequest {
Name string `json:"name"`
Password string `json:"password"`
}
type LoginReply {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
}
业务逻辑:
-
根据用户名、密码(MD5 加密过)去数据库中查询用户
- 查询不到,则返回错误
- 查询到数据,则生成 token 返回
core\internal\logic\user_login_logic.go
func (l *UserLoginLogic) UserLogin(req *types.LoginRequest) (resp *types.LoginReply, err error) {
// 1、从数据库中查询当前用户
user := new(models.UserBasic)
has, err := l.svcCtx.Engine.Where("name = ? AND password = ?", req.Name, helper.Md5(req.Password)).Get(user)
if err != nil {
return nil, err
}
if !has {
return nil, errors.New("用户名或密码错误")
}
// 2、生成token
token, err := helper.GenerateToken(user.Id, user.Identity, user.Name, define.TokenExpire)
if err != nil {
return nil, err
}
// 3、生成用于刷新token的token
refreshToken, err := helper.GenerateToken(user.Id, user.Identity, user.Name, define.RefreshTokenExpire)
if err != nil {
return nil, err
}
resp = new(types.LoginReply)
resp.Token = token
resp.RefreshToken = refreshToken
return
}
core\helper\helper.go
package helper
import (
"context"
"core/define"
"crypto/md5"
"crypto/tls"
"errors"
"fmt"
"log"
"math/rand"
"net/http"
"net/smtp"
"path"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/jordan-wright/email"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
uuid "github.com/satori/go.uuid"
)
func Md5(s string) string {
return fmt.Sprintf("%x", md5.Sum([]byte(s)))
}
func GenerateToken(id int, identity, name string, second int) (string, error) {
uc := define.UserClaim{
Id: id,
Identity: identity,
Name: name,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Second * time.Duration(second)).Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, uc)
tokenString, err := token.SignedString([]byte(define.JwtKey))
if err != nil {
return "", err
}
return tokenString, nil
}
// AnalyzeToken
// Token 解析
func AnalyzeToken(token string) (*define.UserClaim, error) {
uc := new(define.UserClaim)
claims, err := jwt.ParseWithClaims(token, uc, func(token *jwt.Token) (interface{}, error) {
return []byte(define.JwtKey), nil
})
if err != nil {
return nil, err
}
if !claims.Valid {
return uc, errors.New("token is invalid")
}
return uc, err
}
// MailSendCode
// 邮箱验证码发送
func MailSendCode(mail, code string) error {
e := email.NewEmail()
e.From = "云盘 <535504958@qq.com>"
e.To = []string{mail}
e.Subject = "验证码发送测试"
e.HTML = []byte(fmt.Sprintf("<pre style=\"font-family:Helvetica,arial,sans-serif;font-size:13px;color:#747474;text-align:left;line-height:18px\">欢迎使用水牛云盘,您的验证码为:<span style=\"font-size:block\">%s</span></pre>", code))
err := e.SendWithTLS("smtp.163.com:465", smtp.PlainAuth("", "535504958@qq.com", define.MailPassword, "smtp.163.com"),
&tls.Config{InsecureSkipVerify: true, ServerName: "smtp.163.com"})
if err != nil {
log.Print(err)
return err
}
return nil
}
func RandCode() string {
s := "1234567890"
code := ""
rand.Seed(time.Now().UnixNano())
for i := 0; i < define.CodeLength; i++ {
code += string(s[rand.Intn(len(s))])
}
return code
}
func UUID() string {
return uuid.NewV4().String()
}
// MinIOUpload 上传到自建的minio中
func MinIOUpload(r *http.Request) (string, error) {
minioClient, err := minio.New(define.MinIOEndpoint, &minio.Options{
Creds: credentials.NewStaticV4(define.MinIOAccessKeyID, define.MinIOAccessSecretKey, ""),
})
if err != nil {
return "", err
}
// 获取文件信息
file, fileHeader, err := r.FormFile("file")
bucketName := "cloud-disk"
objectName := UUID() + path.Ext(fileHeader.Filename)
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, file, fileHeader.Size,
minio.PutObjectOptions{ContentType: "binary/octet-stream"})
if err != nil {
return "", err
}
return define.MinIOBucket + "/" + bucketName + "/" + objectName, nil
}
接口测试http://127.0.0.1:8888/user/login
请求参数
{
"name": "wu123",
"password": "12345"
}
用户详情
core\core.api
@handler UserDetail
get /user/detail(UserDetailRequest) returns(UserDetailReply)
type UserDetailRequest {
Identity string `json:"identity"`
}
type UserDetailReply {
Name string `json:"name"`
Email string `json:"email"`
}
业务逻辑:
- 根据 identity 去数据库中查询用户信息
core\internal\logic\user_detail_logic.go
func (l *UserLoginLogic) UserLogin(req *types.LoginRequest) (resp *types.LoginReply, err error) {
// 1、从数据库中查询当前用户
user := new(models.UserBasic)
has, err := l.svcCtx.Engine.Where("name = ? AND password = ?", req.Name, helper.Md5(req.Password)).Get(user)
if err != nil {
return nil, err
}
if !has {
return nil, errors.New("用户名或密码错误")
}
// 2、生成token
token, err := helper.GenerateToken(user.Id, user.Identity, user.Name, define.TokenExpire)
if err != nil {
return nil, err
}
// 3、生成用于刷新token的token
refreshToken, err := helper.GenerateToken(user.Id, user.Identity, user.Name, define.RefreshTokenExpire)
if err != nil {
return nil, err
}
resp = new(types.LoginReply)
resp.Token = token
resp.RefreshToken = refreshToken
return
}
接口测试http://127.0.0.1:8888/user/detail
请求参数
{
"identity": "user1"
}
鉴权中间件
在 service 上面添加 @server(middleware: Auth)
则可以让下面的接口都走鉴权中间件
@server (
middleware: Auth
)
service core-api {
// 文件上传
@handler FileUpload
post /file/upload(FileUploadRequest) returns (FileUploadReply)
// 用户文件的关联存储
@handler UserRepositorySave
post /user/repository/save(UserRepositorySaveRequest) returns (UserRepositorySaveReply)
// 用户文件列表
@handler UserFileList
post /user/file/list(UserFileListRequest) returns (UserFileListReply)
// 用户文件夹列表
@handler UserFolderList
post /user/folder/list(UserFolderListRequest) returns (UserFolderListReply)
// 用户文件名称修改
@handler UserFileNameUpdate
post /user/file/name/update(UserFileNameUpdateRequest) returns (UserFileNameUpdateReply)
// 用户-文件夹创建
@handler UserFolderCreate
post /user/folder/create(UserFolderCreateRequest) returns (UserFolderCreateReply)
// 用户-文件删除
@handler UserFileDelete
delete /user/file/delete(UserFileDeleteRequest) returns (UserFileDeleteReply)
// 用户-文件移动
@handler UserFileMove
put /user/file/move(UserFileMoveRequest) returns (UserFileMoveReply)
// 创建分享记录
@handler ShareBasicCreate
post /share/basic/create(ShareBasicCreateRequest) returns (ShareBasicCreateReply)
// 资源保存
@handler ShareBasicSave
post /share/basic/save(ShareBasicSaveRequest) returns (ShareBasicSaveReply)
// 刷新Authorization
@handler RefreshAuthorization
post /refresh/authorization(RefreshAuthorizationRequest) returns (RefreshAuthorizationReply)
// 文件上传前基本信息处理
@handler FileUploadPrepare
post /file/upload/prepare(FileUploadPrepareRequest) returns (FileUploadPrepareReply)
// 文件分片上传
@handler FileUploadChunk
post /file/upload/chunk(FileUploadChunkRequest) returns (FileUploadChunkReply)
// 文件分片上传完成
@handler FileUploadChunkComplete
post /file/upload/chunk/complete(FileUploadChunkCompleteRequest) returns (FileUploadChunkCompleteReply)
}
goctl 会自动生成中间件的基础代码,补充其中的业务逻辑即可。
中间件鉴权逻辑:
- 获取 Header 中的
Authorization
字段 - 为空则返回未授权
-
不为空则进行解析,从中解析出 UserClaim 对象
- 通过
r.Header.Set("xxx", "xxx")
设置到r.Header
上 - 后续逻辑中可以通过
r.Header.Get("")
取出其中的值
- 通过
define\define.go
type UserClaim struct {
Id int
Identity string
Name string
jwt.StandardClaims
}
internal\middleware\auth_middleware.go
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 获取 Header 中的 Authorization
auth := r.Header.Get("Authorization")
// 为空则返回未授权
if auth == "" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
// 解析, 解析失败返回错误
uc, err := helper.AnalyzeToken(auth)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(err.Error()))
return
}
r.Header.Set("UserId", string(rune(uc.Id)))
r.Header.Set("UserIdentity", uc.Identity)
r.Header.Set("UserName", uc.Name)
next(w, r)
}
}
刷新 Authorization
core.api
service core-api {
// 刷新Authorization
@handler RefreshAuthorization
post /refresh/authorization(RefreshAuthorizationRequest) returns (RefreshAuthorizationReply)
}
type RefreshAuthorizationRequest {}
type RefreshAuthorizationReply {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
}
两个 Token:
- Token 有效期较短,用于对用户做鉴权操作
- RefreshToken 有效期比较长,用于刷新上面那个 Token
刷新 Token 业务逻辑:
- 解析本次请求 Header 中的 Authorization,解析出 UserClaim 信息
- 根据 UserClaim 中的信息生成新的 Token 和 RefreshToken,返回给前端
internal\handler\refresh_authorization_handler.go
func RefreshAuthorizationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RefreshAuthorizationRequest
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewRefreshAuthorizationLogic(r.Context(), svcCtx)
resp, err := l.RefreshAuthorization(&req, r.Header.Get("Authorization"))
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}
internal\logic\refresh_authorization_logic.go
func (l *RefreshAuthorizationLogic) RefreshAuthorization(req *types.RefreshAuthorizationRequest, authorization string) (resp *types.RefreshAuthorizationReply, err error) {
// 解析 Authorization 获取 UserClaim
uc, err := helper.AnalyzeToken(authorization)
if err != nil {
return
}
// 根据 UserClaim 中的信息,生成新的 Token
token, err := helper.GenerateToken(uc.Id, uc.Identity, uc.Name, define.TokenExpire)
if err != nil {
return
}
// 生成新的 Refresh Token
refreshToken, err := helper.GenerateToken(uc.Id, uc.Identity, uc.Name, define.RefreshTokenExpire)
if err != nil {
return
}
resp = new(types.RefreshAuthorizationReply)
resp.Token = token
resp.RefreshToken = refreshToken
return
}
internal\svc\service_context.go
package svc
import (
"core/internal/config"
"core/internal/middleware"
"core/models"
"github.com/go-redis/redis/v8"
"github.com/zeromicro/go-zero/rest"
"xorm.io/xorm"
)
type ServiceContext struct {
Config config.Config
Engine *xorm.Engine
RDB *redis.Client
Auth rest.Middleware
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
Engine: models.Init(c),
RDB: models.InitRedis(c),
Auth: middleware.NewAuthMiddleware().Handle,
}
}
验证码发送
测试代码:mail_test.go
func TestSendMail(t *testing.T) {
e := email.NewEmail()
e.From = "云盘 <535504958@qq.com>"
e.To = []string{"the.wu@qq.com"}
e.Subject = "验证码发送测试"
e.HTML = []byte("<pre style=\"font-family:Helvetica,arial,sans-serif;font-size:13px;color:#747474;text-align:left;line-height:18px\">欢迎使用云盘,您的验证码为:<span style=\"font-size:block\">123456</span></pre>")
err := e.SendWithTLS("smtp.163.com:465", smtp.PlainAuth("", "535504958@qq.com", define.MailPassword, "smtp.163.com"),
&tls.Config{InsecureSkipVerify: true, ServerName: "smtp.163.com"})
if err != nil {
t.Fatal(err)
}
}
测试代码:redis_test.go
package test
import (
"context"
"testing"
"time"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
var rdb = redis.NewClient(&redis.Options{
Addr: "192.168.204.130:6379",
Password: "mima", //
PoolSize: 10,
DB: 0, // use default DB
})
func TestSetValue(t *testing.T) {
err := rdb.Set(ctx, "key", "value", time.Second*10).Err()
if err != nil {
t.Error(err)
}
}
func TestGetValue(t *testing.T) {
val, err := rdb.Get(ctx, "key").Result()
if err != nil {
t.Error(err)
}
t.Log(val)
}
测试uuid test\uuid_test.go
func TestGenerateUUID(t *testing.T) {
v4 := uuid.NewV4()
fmt.Println(v4)
}
邮箱发送验证码模块:
// 验证码发送
@handler MailCodeSendRegister
post /mail/code/send/register(MailCodeSendRequest) returns(MailCodeSendReply)
type MailCodeSendRequest {
Email string `json:"email"`
}
type MailCodeSendReply {
Code string `json:"code"`
}
邮箱发送验证码逻辑:
- 随机生成验证码,并存储到 Redis 中(设置一个过期时间)
- 往 Email 发送邮件
internal\logic\mail_code_send_register_logic.go
func (l *MailCodeSendRegisterLogic) MailCodeSendRegister(req *types.MailCodeSendRequest) (resp *types.MailCodeSendReply, err error) {
// 该邮箱未被注册
cnt, err := l.svcCtx.Engine.Where("email = ?", req.Email).Count(new(models.UserBasic))
if err != nil {
return
}
if cnt > 0 {
err = errors.New("该邮箱已被注册")
return
}
// 获取验证码
code := helper.RandCode()
// 存储验证码
l.svcCtx.RDB.Set(l.ctx, req.Email, code, time.Second*time.Duration(define.CodeExpire))
// 发送验证码
err = helper.MailSendCode(req.Email, code)
return
}
用户注册模块:
core.api
// 用户注册
@handler UserRegister
post /user/register(UserRegisterRequest) returns(UserRegisterReply)
type UserRegisterRequest {
// 用户名
Name string `json:"name"`
// 密码
Password string `json:"password"`
// 邮箱
Email string `json:"email"`
// 验证码
Code string `json:"code"`
}
type UserRegisterReply {}
用户注册逻辑:
- 判断 code 是否和 Redis 中存的一致
- 判断用户是否已经存在
- 往数据库插入用户数据(生成 UUID,密码加密)
internal\logic\user_register_logic.go
func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *types.UserRegisterReply, err error) {
// 判断 code 是否一致
code, err := l.svcCtx.RDB.Get(l.ctx, req.Email).Result()
if err != nil {
return nil, errors.New("该邮箱的验证码为空,请重新发送验证码")
}
if code != req.Code {
return nil, errors.New("验证码错误")
}
// 判断用户是否已经存在
cnt, err := l.svcCtx.Engine.Where("name = ?", req.Name).Count(new(models.UserBasic))
if err != nil {
return
}
if cnt > 0 {
return nil, errors.New("用户名已存在")
}
// 数据入库
user := &models.UserBasic{
Identity: helper.UUID(),
Name: req.Name,
Password: helper.Md5(req.Password),
Email: req.Email,
}
_, err = l.svcCtx.Engine.Insert(user)
if err != nil {
return
}
return
}
存储池模块
公共存储池管理
用户-文件上传
core.api
@server(
middleware: Auth
)
service core-api {
@handler FileUpload
post /file/upload(FileUploadRequest) returns(FileUploadReply)
}
type FileUploadRequest {
Hash string `json:"hash,optional"`
Name string `json:"name,optional"`
Ext string `json:"ext,optional"`
Size int64 `json:"size,optional"`
Path string `json:"path,optional"`
}
type FileUploadReply {
Identity string `json:"identity"`
Ext string `json:"ext"`
Name string `json:"name"`
}
文件上传业务逻辑:
- 抽取出上传文件到minio 的方法放到工具类中
-
在 handler 层做一些业务处理:在数据库中根据 hash 查询文件信息
- 文件已经存在,则直接返回其信息
- 文件不存在,则往minio中存储文件,将 request 传递到 logic 层
- 在 logic 层往数据库中插入文件记录,并返回文件信息
file_upload_handler
中 handler 层的代码:internal\handler\file_upload_handler.go
package handler
import (
"crypto/md5"
"fmt"
"net/http"
"path"
"core/helper"
"core/internal/logic"
"core/models"
"core/internal/svc"
"core/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func FileUploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.FileUploadRequest
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
// 获取上传的文件(FormData)
file, fileHeader, err := r.FormFile("file")
if err != nil {
return
}
// 判断文件在数据库中是否已经存在
b := make([]byte, fileHeader.Size)
_, err = file.Read(b)
if err != nil {
return
}
hash := fmt.Sprintf("%x", md5.Sum(b))
rp := new(models.RepositoryPool)
has, err := svcCtx.Engine.Where("hash = ?", hash).Get(rp)
if err != nil {
return
}
if has {
// 文件已经存在,直接返回信息
httpx.OkJson(w, &types.FileUploadReply{
Identity: rp.Identity,
Ext: rp.Ext,
Name: rp.Name,
})
return
}
// 往 minio 中存储文件
minioPath, err := helper.MinIOUpload(r)
if err != nil {
return
}
// 往 logic 传递 request
req.Name = fileHeader.Filename
req.Ext = path.Ext(fileHeader.Filename)
req.Size = fileHeader.Size
req.Hash = hash
req.Path = minioPath
l := logic.NewFileUploadLogic(r.Context(), svcCtx)
resp, err := l.FileUpload(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}
models\repository_pool.go
package models
import "time"
type RepositoryPool struct {
Id int
Identity string
Hash string
Name string
Ext string
Size int64
Path string
CreatedAt time.Time `xorm:"created"`
UpdatedAt time.Time `xorm:"updated"`
DeletedAt time.Time `xorm:"deleted"`
}
func (table RepositoryPool) TableName() string {
return "repository_pool"
}
file_upload_logic
中 logic 层的代码:internal\logic\file_upload_logic.go
func (l *FileUploadLogic) FileUpload(req *types.FileUploadRequest) (resp *types.FileUploadReply, err error) {
// 数据入库
rp := &models.RepositoryPool{
Identity: helper.UUID(),
Hash: req.Hash,
Name: req.Name,
Ext: req.Ext,
Size: req.Size,
Path: req.Path,
}
_, err = l.svcCtx.Engine.Insert(rp)
if err != nil {
return
}
resp = new(types.FileUploadReply)
resp.Identity = rp.Identity
resp.Ext = rp.Ext
resp.Name = rp.Name
return
}
个人存储池资源管理
用户 - 文件关联存储
core.api
service core-api {
@handler FileUpload
post /file/upload(FileUploadRequest) returns(FileUploadReply)
@handler UserRepositorySave
post /user/repository/save(UserRepositorySaveRequest) returns(UserRepositorySaveReply)
}
type UserRepositorySaveRequest {
ParentId int64 `json:"parentId"`
RepositoryIdentity string `json:"repositoryIdentity"`
Ext string `json:"ext"`
Name string `json:"name"`
}
type UserRepositorySaveReply {}
用户文件关联存储逻辑:
- 在 handler 层,将 Header 中的 UserIdentity 取出传到 logic 层
r.Header.Get("UserIdentity")
- 在 logic 层,新增用户文件关联存储数据到数据库
handler 层传递鉴权中间件中存储的数据:internal\handler\user_repository_save_handler.go
resp, err := l.UserRepositorySave(&req, r.Header.Get("UserIdentity"))
func UserRepositorySaveHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UserRepositorySaveRequest
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewUserRepositorySaveLogic(r.Context(), svcCtx)
resp, err := l.UserRepositorySave(&req, r.Header.Get("UserIdentity"))
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}
logic 层执行业务:internal\logic\user_repository_save_logic.go
func (l *UserRepositorySaveLogic) UserRepositorySave(req *types.UserRepositorySaveRequest, userIdentity string) (resp *types.UserRepositorySaveReply, err error) {
// 数据入库
ur := &models.UserRepository{
Identity: helper.UUID(),
UserIdentity: userIdentity,
ParentId: req.ParentId,
RepositoryIdentity: req.RepositoryIdentity,
Ext: req.Ext,
Name: req.Name,
}
_, err = l.svcCtx.Engine.Insert(ur)
return
}
models\user_repository.go
package models
import "time"
type UserRepository struct {
Id int
Identity string
UserIdentity string
ParentId int64
RepositoryIdentity string
Ext string
Name string
CreatedAt time.Time `xorm:"created"`
UpdatedAt time.Time `xorm:"updated"`
DeletedAt time.Time `xorm:"deleted"`
}
func (table UserRepository) TableName() string {
return "user_repository"
}
个人存储池资源管理
用户 - 文件关联存储
@handler UserRepositorySave
post /user/repository/save(UserRepositorySaveRequest) returns(UserRepositorySaveReply)
type UserRepositorySaveRequest {
ParentId int64 `json:"parentId"`
RepositoryIdentity string `json:"repositoryIdentity"`
Ext string `json:"ext"`
Name string `json:"name"`
}
type UserRepositorySaveReply {}
用户文件关联存储逻辑:
- 在 handler 层,将 Header 中的 UserIdentity 取出传到 logic 层
r.Header.Get("UserIdentity")
- 在 logic 层,新增用户文件关联存储数据到数据库
handler 层传递鉴权中间件中存储的数据:internal\handler\user_repository_save_handler.go
resp, err := l.UserRepositorySave(&req, r.Header.Get("UserIdentity"))
logic 层执行业务: internal\logic\user_repository_save_logic.go
func (l *UserRepositorySaveLogic) UserRepositorySave(req *types.UserRepositorySaveRequest, userIdentity string) (resp *types.UserRepositorySaveReply, err error) {
// 数据入库
ur := &models.UserRepository{
Identity: helper.UUID(),
UserIdentity: userIdentity,
ParentId: req.ParentId,
RepositoryIdentity: req.RepositoryIdentity,
Ext: req.Ext,
Name: req.Name,
}
_, err = l.svcCtx.Engine.Insert(ur)
return
}
用户 - 文件列表
查看文件列表的逻辑:core.api
// 用户文件列表
@handler UserFileList
get /user/file/list(UserFileListRequest) returns (UserFileReply)
type UserFileListRequest {
Id int64 `json:"id,optional"`
Page int `json:"page,optional"`
Size int `json:"size,optional"`
}
type UserFileListReply {
List []*UserFile `json:"list"`
Count int64 `json:"count"`
}
type UserFile {
Id int64 `json:"id"`
Identity string `json:"identity"`
RepositoryIdentity string `json:"repository_identity"`
Name string `json:"name"`
Ext string `json:"ext"`
Path string `json:"path"`
Size int64 `json:"size"`
}
业务逻辑:
- handler 层中取出 UserIdentity 传递给 logic 层
- 根据 page、size 计算 Mysql 分页参数
- 在数据库查询用户文件列表数据,以及 总数,返回给前端
func (l *UserFileListLogic) UserFileList(req *types.UserFileListRequest, userIdentity string) (resp *types.UserFileListReply, err error) {
uf := make([]*types.UserFile, 0)
resp = new(types.UserFileListReply)
// 分页参数
size := req.Size
if size <= 0 {
size = define.PageSize
}
page := req.Page
if page <= 0 {
page = 1
}
offset := (page - 1) * size
// 去数据库查询用户文件列表
err = l.svcCtx.Engine.Table("user_repository").
Where("parent_id = ? AND user_identity = ? ", req.Id, userIdentity).
Select("user_repository.id, user_repository.identity, user_repository.repository_identity, "+
"user_repository.ext, user_repository.name, repository_pool.path, repository_pool.size").
Join("LEFT", "repository_pool", "user_repository.repository_identity = repository_pool.identity").
Where("user_repository.deleted_at = ? OR user_repository.deleted_at IS NULL", time.Time{}.Format(define.Datetime)).
Limit(size, offset).
Find(&uf)
if err != nil {
return
}
// 查询用户文件总数
cnt, err := l.svcCtx.Engine.
Where("parent_id = ? AND user_identity = ? ", req.Id, userIdentity).
Count(new(models.UserRepository))
if err != nil {
return
}
resp.List = uf
resp.Count = cnt
return
}
用户 - 文件名称修改
@handler UserFileNameUpdate
post /user/file/name/update(UserFileNameUpdatedRequest) returns(UserFieNameUpdateReply)
type UserFileNameUpdatedRequest {
Identity string `json:"identity"`
Name string `json:"name"`
}
type UserFieNameUpdateReply {}
业务逻辑:
- 判断当前名称在该层级下是否存在,存在则返回错误 “该名称已存在”
- 根据 identity 去 user_repository 表修改为传过来的 name
func (l *UserFileNameUpdateLogic) UserFileNameUpdate(req *types.UserFileNameUpdatedRequest, userIdentity string) (resp *types.UserFieNameUpdateReply, err error) {
// 判断当前名称在该层级下是否存在
cnt, err := l.svcCtx.Engine.Where("name = ? AND parent_id = (SELECT parent_id FROM user_repository ur WHERE ur.identity = ?)",
req.Name, req.Identity).Count(new(models.UserRepository))
if err != nil {
return
}
if cnt > 0 {
return nil, errors.New("该名称已存在")
}
// 文件名称修改
data := &models.UserRepository{Name: req.Name}
_, err = l.svcCtx.Engine.Where("identity = ? AND user_identity = ? ", req.Identity, userIdentity).Update(data)
return
}
用户- 文件夹创建
@handler UserFolderCreate
post /user/folder/create(UserFolderCreateRequest) returns(UserFolderCreateReply)
type UserFolderCreateRequest {
ParentId int64 `json:"parent_id"`
Name string `json:"name"`
}
type UserFolderCreateReply {
Identity string `json:"identity"`
}
业务逻辑:
- 判断当前名称在该层级下是否存在,存在则返回错误 “该名称已存在”
- 创建文件夹数据插入到数据库中,并返回其 identity
func (l *UserFolderCreateLogic) UserFolderCreate(req *types.UserFolderCreateRequest, userIdentity string) (resp *types.UserFolderCreateReply, err error) {
// 判断当前名称在该层级下是否存在
cnt, err := l.svcCtx.Engine.Where("name = ? AND parent_id = ?", req.Name, req.ParentId).Count(new(models.UserRepository))
if err != nil {
return nil, err
}
if cnt > 0 {
return nil, errors.New("该名称已存在")
}
// 创建文件夹
data := &models.UserRepository{
Identity: helper.UUID(),
UserIdentity: userIdentity,
ParentId: req.ParentId,
Name: req.Name,
}
_, err = l.svcCtx.Engine.Insert(data)
if err != nil {
return
}
resp = new(types.UserFolderCreateReply)
resp.Identity = data.Identity
return
}
用户 - 文件删除
@handler UserFileDelete
delete /user/file/delete(UserFileDeleteRequest) returns(UserFileDeleteReply)
type UserFileDeleteRequest {
Identity string `json:"identity"`
}
type UserFileDeleteReply {}
业务逻辑:
- 根据 identity 去 user_repository 表中进行文件删除
func (l *UserFileDeleteLogic) UserFileDelete(req *types.UserFileDeleteRequest, userIdentity string) (resp *types.UserFileDeleteReply, err error) {
_, err = l.svcCtx.Engine.
Where("user_identity = ? AND identity = ?", userIdentity, req.Identity).
Delete(new(models.UserRepository))
return
}
用户 - 文件移动
@handler UserFileMove
put /user/file/move(UserFileMoveRequest) returns(UserFileMoveReply)
type UserFileMoveRequest {
ParentIdentity string `json:"parent_identity"`
Identity string `json:"identity"`
}
type UserFileMoveReply {}
- 判断父级文件夹是否存在,不存在返回错误
- 存在则根据 identity 更新它的 parent_identity 为传来的值
func (l *UserFileMoveLogic) UserFileMove(req *types.UserFileMoveRequest, userIdentity string) (resp *types.UserFileMoveReply, err error) {
// 判断父级文件是否存在
parentData := new(models.UserRepository)
has, err := l.svcCtx.Engine.Where("identity = ? AND user_identity = ?", req.ParentIdentity, userIdentity).Get(parentData)
if err != nil {
return nil, err
}
if !has {
return nil, errors.New("文件夹不存在")
}
// 更新记录的 ParentID
_, err = l.svcCtx.Engine.Where("identity = ?", req.Identity).Update(models.UserRepository{ParentId: int64(parentData.Id)})
return
}
文件分享模块
用户 - 创建分享记录
@handler ShareBasicCreate
post /share/basic/create(ShareBasicCreateRequest) returns(ShareBasicCreateReply)
type ShareBasicCreateRequest {
UserRepositoryIdentity string `json:"user_repository_identity"`
ExpiredTime int `json:"expired_time"`
}
type ShareBasicCreateReply {
Identity string `json:"identity"`
}
业务逻辑:
- 判断用户存储池中是否有该文件
- 生成分享记录插入到数据库中,并返回该数据
func (l *ShareBasicCreateLogic) ShareBasicCreate(req *types.ShareBasicCreateRequest, userIdentity string) (resp *types.ShareBasicCreateReply, err error) {
// 判断用户存储池中文件是否存在
ur := new(models.UserRepository)
has, err := l.svcCtx.Engine.Where("identity = ?", req.UserRepositoryIdentity).Get(ur)
if err != nil {
return
}
if !has {
return nil, errors.New("user repository not found")
}
// 生成分享记录
uuid := helper.UUID()
data := &models.ShareBasic{
Identity: uuid,
UserIdentity: userIdentity,
RepositoryIdentity: ur.RepositoryIdentity,
UserRepositoryIdentity: req.UserRepositoryIdentity,
ExpiredTime: req.ExpiredTime,
}
_, err = l.svcCtx.Engine.Insert(data)
if err != nil {
return
}
resp = &types.ShareBasicCreateReply{Identity: uuid}
return
}
获取资源详情
该接口无需鉴权,所有人都可以访问
@handler ShareBasicDetail
get /share/basic/detail(ShareBasicDetailRequest) returns(ShareBasicDetailReply)
type ShareBasicDetailRequest {
Identity string `json:"identity"`
}
type ShareBasicDetailReply {
RepositoryIdentity string `json:"repository_identity"`
Name string `json:"name"`
Ext string `json:"ext"`
Size int64 `json:"size"`
Path string `json:"path"`
}
业务逻辑:
- 对分享记录的点击次数进行 + 1
- 根据 identity 关联 repository_pool 和 user_repository 查询出文件详细信息
func (l *ShareBasicDetailLogic) ShareBasicDetail(req *types.ShareBasicDetailRequest) (resp *types.ShareBasicDetailReply, err error) {
// 对分享记录的点击次数进行 + 1
_, err = l.svcCtx.Engine.Exec("UPDATE share_basic SET click_num = click_num + 1 WHERE identity = ?", req.Identity)
if err != nil {
return
}
// 获取文件的详细信息
resp = new(types.ShareBasicDetailReply)
_, err = l.svcCtx.Engine.Table("share_basic").
Select("share_basic.repository_identity, user_repository.name, repository_pool.ext, repository_pool.size, repository_pool.path").
Join("LEFT", "repository_pool", "share_basic.repository_identity = repository_pool.identity").
Join("LEFT", "user_repository", "user_repository.identity = share_basic.user_repository_identity").
Where("share_basic.identity = ?", req.Identity).Get(resp)
return
}
用户 - 资源保存
@handler ShareBasicSave
post /share/basic/save(ShareBasicSaveRequest) returns(ShareBasicSaveReply)
type ShareBasicSaveRequest {
RepositoryIdentity string `json:"repository_identity"`
ParentId int64 `json:"parent_id"`
}
type ShareBasicSaveReply {
Identity string `json:"identity"`
}
业务逻辑:
- 根据 repository_identity 获取资源详情
- 将上面拿到的数据保存到 user_repository 中
func (l *ShareBasicSaveLogic) ShareBasicSave(req *types.ShareBasicSaveRequest, userIdentity string) (resp *types.ShareBasicSaveReply, err error) {
// 获取资源详情
rp := new(models.RepositoryPool)
has, err := l.svcCtx.Engine.Where("identity = ?", req.RepositoryIdentity).Get(rp)
if err != nil {
return
}
if !has {
return nil, errors.New("资源不存在")
}
// user_repository 资源保存
ur := &models.UserRepository{
Identity: helper.UUID(),
UserIdentity: userIdentity,
ParentId: req.ParentId,
RepositoryIdentity: req.RepositoryIdentity,
Ext: rp.Ext,
Name: rp.Name,
}
_, err = l.svcCtx.Engine.Insert(ur)
resp = new(types.ShareBasicSaveReply)
resp.Identity = ur.Identity
return
}
MinIO 分片上传
GitHub - yuyuanshifu/minio-breakpoint-upload: 实现minio断点续传功能
文件分片上传
流程测试:文件分片、合并分片、校验一致性
文件分片:
func TestGenerateChunkFile(t *testing.T) {
// 读取文件
fileInfo, err := os.Stat("test.mp4")
if err != nil {
t.Fatal(err)
}
// 分片个数 = 文件大小 / 分片大小
// 390 / 100 ==> 4, 向上取整
chunkNum := math.Ceil(float64(fileInfo.Size()) / chunkSize)
// 只读方式打开文件
myFile, err := os.OpenFile("test.mp4", os.O_RDONLY, 0666)
if err != nil {
t.Fatal(err)
}
// 存放每一次的分片数据
b := make([]byte, chunkSize)
// 遍历所有分片
for i := 0; i < int(chunkNum); i++ {
// 指定读取文件的起始位置
myFile.Seek(int64(i*chunkSize), 0)
// 最后一次的分片数据不一定是整除下来的数据
// 例如: 文件 120M, 第一次读了 100M, 剩下只有 20M
if chunkSize > fileInfo.Size()-int64(i*chunkSize) {
b = make([]byte, fileInfo.Size()-int64(i*chunkSize))
}
myFile.Read(b)
f, err := os.OpenFile("./"+strconv.Itoa(i)+".chunk", os.O_CREATE|os.O_WRONLY, os.ModePerm)
if err != nil {
t.Fatal(err)
}
f.Write(b)
f.Close()
}
myFile.Close()
}
分片文件的合并:
func TestMergeChunkFile(t *testing.T) {
myFile, err := os.OpenFile("test2.mp4", os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
if err != nil {
t.Fatal(err)
}
// 计算分片个数, 正常应该由前端传来, 这里测试时自行计算
fileInfo, err := os.Stat("test.mp4")
if err != nil {
t.Fatal(err)
}
// 分片个数 = 文件大小 / 分片大小
chunkNum := math.Ceil(float64(fileInfo.Size()) / chunkSize)
// 合并分片
for i := 0; i < int(chunkNum); i++ {
f, err := os.OpenFile("./"+strconv.Itoa(i)+".chunk", os.O_RDONLY, os.ModePerm)
if err != nil {
t.Fatal(err)
}
b, err := ioutil.ReadAll(f)
if err != nil {
t.Fatal(err)
}
myFile.Write(b)
f.Close()
}
myFile.Close()
}
文件一致性校验:
func TestCheck(t *testing.T) {
// 获取第一个文件的信息
f1, err := os.OpenFile("test.mp4", os.O_RDONLY, 0666)
if err != nil {
t.Fatal(err)
}
b1, err := ioutil.ReadAll(f1)
if err != nil {
t.Fatal(err)
}
// 获取第二个文件的信息
f2, err := os.OpenFile("test2.mp4", os.O_RDONLY, 0666)
if err != nil {
t.Fatal(err)
}
b2, err := ioutil.ReadAll(f2)
if err != nil {
t.Fatal(err)
}
s1 := fmt.Sprintf("%x", md5.Sum(b1))
s2 := fmt.Sprintf("%x", md5.Sum(b2))
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s1 == s2)
}
minio分片上传流程(测试)
文件分片上传
封装方法
helper.go 中关于文件分片上传 minio 的封装的函数:
文件上传前的准备
@handler FileUploadPrepare
post /file/upload/prepare(FileUploadPrepareRequest) returns (FileUploadPrepareReply)
type FileUploadPrepareRequest {
Md5 string `json:"md5"`
Name string `json:"name"`
Ext string `json:"ext"`
}
type FileUploadPrepareReply {
Identity string `json:"identity"`
UploadId string `json:"upload_id"`
Key string `json:"key"`
}
业务逻辑:
-
根据 md5 值去数据库中查询,是否存在对应的文件
- 存在则可以秒传,直接返回 Identity
- 不存在则获取 Key,UploadId 并返回,用于进行分片上传
文件分片上传
@handler FileUploadChunk
post /file/upload/chunk(FileUploadChunkRequest) returns (FileUploadChunkReply)
type FileUploadChunkRequest { // formdata
// upload_id
// part_number
}
type FileUploadChunkReply {
Etag string `json:"etag"` // MD5
}
文件分片上传完成
@handler FileUploadChunkComplete
post /file/upload/chunk/complete(FileUploadChunkCompleteRequest) returns (FileUploadChunkCompleteReply)
type FileUploadChunkCompleteRequest {
Md5 string `json:"md5"`
Name string `json:"name"`
Ext string `json:"ext"`
Size int64 `json:"size"`
Key string `json:"key"`
UploadId string `json:"upload_id"`
MinioObjects []MinioObject `json:"Minio_objects"`
}
type MinioObject {
PartNumber int `json:"part_number"`
Etag string `json:"etag"`
}
type FileUploadChunkCompleteReply {
Identity string `json:"identity"` // 存储池identity
}
D:\Workspace\Go\src\minio_backend\core\internal\handler\file_upload_prepare_handler.go
D:\Workspace\Go\src\minio_backend\core\internal\handler\file_upload_chunk_handler.go
D:\Workspace\Go\src\minio_backend\core\internal\handler\file_upload_chunk_complete_handler.go