基于 go-zero 实现网盘系统
基于 go-zero 实现网盘系统
基于 go-zero 实现网盘系统
文档:
基于 go-zero 实现网盘系统
Apifox 生成的在线接口文档,仅供参考:
https://www.apifox.cn/apidoc/shared-74bcbbbe-b20f-4ed6-afa8-02752683a8c9/api-20729960
go-zero 简单使用
Goctl 安装,参考:Goctl 安装 | go-zero
使用 go-zero 官方的工具生成代码:
# 创建 API 服务
goctl api new core
运行生成的代码:
# 启动服务
go run core.go -f etc/core-api.yaml
在 core/internal/logic/corelogic.go
中修改业务逻辑
访问:http://localhost:8888/from/you
go-zero 业务开发流程总结:
- 在
core.api
中声明接口格式(包括路径、请求参数结构、响应数据结构) - 通过以下指令生成接口相关代码:
goctl api go -api core.api -dir . -style go_zero
- 在生成的代码中
xxx_logic.go
中的 TODO 处补全接口逻辑 - 通过以下指令启动服务:
go run core.go -f etc/core-api.yaml
生成的单体 api 服务目录:
|____go.mod
|____go.sum
|____greet.api // api接口与类型定义
|____etc // 网关层配置文件
| |____greet-api.yaml
|____internal
| |____config // 配置-对应etc下配置文件
| | |____config.go
| |____handler // 视图函数层, 路由与处理器
| | |____routes.go
| | |____greethandler.go
| |____logic // 逻辑处理
| | |____greetlogic.go
| |____svc // 依赖资源, 封装 rpc 对象的地方
| | |____servicecontext.go
| |____types // 中间类型
| | |____types.go
|____greet.go // main.go 入口
数据库分析
用户信息:存储用户基本信息,用于登录
CREATE TABLE `user_basic` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`identity` varchar(36) DEFAULT NULL,
`name` varchar(60) DEFAULT NULL,
`password` varchar(32) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
公共文件存储池:存储文件信息
CREATE TABLE `repository_pool` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`identity` varchar(36) DEFAULT NULL,
`hash` varchar(32) DEFAULT NULL COMMENT '文件的唯一标识',
`name` varchar(255) DEFAULT NULL COMMENT '文件名称',
`ext` varchar(30) DEFAULT NULL COMMENT '文件扩展名',
`size` int(11) DEFAULT NULL COMMENT '文件大小',
`path` varchar(255) DEFAULT NULL COMMENT '文件路径',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
用户存储池:对公共文件存储池中文件信息的引用
CREATE TABLE `user_repository` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`identity` varchar(36) DEFAULT NULL,
`parent_id` int(11) DEFAULT NULL COMMENT '父级文件层级, 0-【文件夹】',
`user_identity` varchar(36) DEFAULT NULL COMMENT '对应用户的唯一标识',
`repository_identity` varchar(36) DEFAULT NULL COMMENT '公共池中文件的唯一标识',
`ext` varchar(255) DEFAULT NULL COMMENT '文件或文件夹类型',
`name` varchar(255) DEFAULT NULL COMMENT '用户定义的文件名',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
文件分享:
CREATE TABLE `share_basic` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`identity` varchar(36) DEFAULT NULL,
`user_identity` varchar(36) DEFAULT NULL COMMENT '对应用户的唯一标识',
`repository_identity` varchar(36) DEFAULT NULL COMMENT '公共池中文件的唯一标识',
`user_repository_identity` varchar(36) DEFAULT NULL COMMENT '用户池子中的唯一标识',
`expired_time` int(11) DEFAULT NULL COMMENT '失效时间,单位秒,【0-永不失效】',
`click_num` int(11) DEFAULT '0' COMMENT '点击次数',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
用户模块
密码登录
@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 返回
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
}
// 用户不存在
if !has {
err = errors.New("用户名或密码错误")
return
}
// 2. 生成 token
token, err := helper.GenerateToken(user.Id, user.Identity, user.Name, define.TokenExpire)
if err != nil {
return
}
// 3. 生成用于刷新 token 的 token
refreshToken, err := helper.GenerateToken(user.Id, user.Identity, user.Name, define.RefreshTokenExpire)
if err != nil {
return
}
resp = new(types.LoginReply)
resp.Token = token
resp.RefreshToken = refreshToken
return
}
用户详情
@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 去数据库中查询用户信息
func (l *UserDetailLogic) UserDetail(req *types.UserDetailRequest) (resp *types.UserDetailReply, err error) {
ub := new(models.UserBasic)
has, err := l.svcCtx.Engine.Where("identity = ?", req.Identity).Get(ub)
if err != nil {
return
}
// 用户不存在
if !has {
return nil, errors.New("user not found")
}
resp = new(types.UserDetailReply)
resp.Name = ub.Name
resp.Email = ub.Email
return
}
鉴权中间件
在 service 上面添加 @server(middleware: Auth)
则可以让下面的接口都走鉴权中间件
@server(
middleware: Auth
)
service core-api {
...
}
goctl 会自动生成中间件的基础代码,补充其中的业务逻辑即可。
中间件鉴权逻辑:
- 获取 Header 中的
Authorization
字段 - 为空则返回未授权
- 不为空则进行解析,从中解析出 UserClaim 对象
- 通过
r.Header.Set("xxx", "xxx")
设置到r.Header
上 - 后续逻辑中可以通过
r.Header.Get("")
取出其中的值
- 通过
type UserClaim struct {
Id int
Identity string
Name string
jwt.StandardClaims
}
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
@handler RefreshAuthorization
get /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,返回给前端
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
}
邮箱注册
测试代码
Go 邮箱库:jordan-wright/email: Robust and flexible email library for Go)
测试代码:mail_test.go
func TestSendMial(t *testing.T) {
e := email.NewEmail()
e.From = "Yusael <xxxx@163.com>"
e.To = []string{"xxxx@qq.com"}
e.Subject = "验证码发送测试"
e.HTML = []byte("您的验证码为:<h1>12345</h1>")
err := e.SendWithTLS("smtp.163.com:465",
// 注意这里的密码不是邮箱登录密码, 是开启 smtp 服务后获取的一串验证码
smtp.PlainAuth("", "xxxx@163.com", "xxx", "smtp.163.com"),
&tls.Config{InsecureSkipVerify: true, ServerName: "smtp.163.com"})
if err != nil {
t.Fatal(err)
}
}
Go Redis 库:go-redis/redis: Type-safe Redis client for Golang
测试代码:redis_test.go
var ctx = context.Background()
var rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
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)
}
Go UUID 库:satori/go.uuid: UUID package for Go
- Version 1,基于 timestamp 和 MAC address (RFC 4122)
- Version 2,基于 timestamp, MAC address 和 POSIX UID/GID (DCE 1.1)
- Version 3, 基于 MD5 hashing (RFC 4122)
- Version 4, 基于 random numbers (RFC 4122)
- Version 5, 基于 SHA-1 hashing (RFC 4122)
import (
"fmt"
"testing"
uuid "github.com/satori/go.uuid"
)
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 发送邮件
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()
// 存储验证码到 Redis(设置过期时间)
l.svcCtx.RDB.Set(l.ctx, req.Email, code, time.Second*time.Duration(define.CodeExpire))
// 发送验证码
err = helper.MailSendCode(req.Email, code)
return
}
用户注册模块:
// 用户注册
@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,密码加密)
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
}
存储池模块
中心存储池资源管理
腾讯云 COS 后台地址:https://console.cloud.tencent.com/cos/bucket
腾讯云 COS 帮助文档:https://cloud.tencent.com/document/product/436/31215
开发好习惯:先跑通测试,然后再开始开发
用户-文件上传
@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"`
}
文件上传业务逻辑:
- 抽取出 上传文件到腾讯云 的方法放到工具类中
- 在 handler 层做一些业务处理:在数据库中根据 hash 查询文件信息
- 文件已经存在,则直接返回其信息
- 文件不存在,则往 Cos 中存储文件,将 request 传递到 logic 层
- 在 logic 层往数据库中插入文件记录,并返回文件信息
file_upload_handler
中 handler 层的代码:
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.Error(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
}
// 往 COS 中存储文件
cosPath, err := helper.CosUpload(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 = cosPath
l := logic.NewFileUploadLogic(r.Context(), svcCtx)
resp, err := l.FileUpload(&req)
if err != nil {
httpx.Error(w, err)
} else {
httpx.OkJson(w, resp)
}
}
}
file_upload_logic
中 logic 层的代码:
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
}
个人存储池资源管理
用户 - 文件关联存储
@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 层传递鉴权中间件中存储的数据:
resp, err := l.UserRepositorySave(&req, r.Header.Get("UserIdentity"))
logic 层执行业务:
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
}
用户 - 文件列表
查看文件列表的逻辑:
// 用户文件列表
@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
}
文件分片上传
流程测试:文件分片、合并分片、校验一致性
文件分片:
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)
}
Cos 分片上传流程(测试)
func TestInitPartUpload(t *testing.T) {
u, _ := url.Parse("https://szluyu99-1259132563.cos.ap-nanjing.myqcloud.com")
b := &cos.BaseURL{BucketURL: u}
client := cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: define.TencentSecretID,
SecretKey: define.TencentSecretKey,
},
})
key := "cloud-disk/exampleobject.jpeg"
v, _, err := client.Object.InitiateMultipartUpload(context.Background(), key, nil)
if err != nil {
t.Fatal(err)
}
UploadID := v.UploadID // 165320975357e514f921b93de385fc75fb5c1f9702a5da1bacd086900c6c1a613805b15df1
fmt.Println(UploadID)
}
// 分片上传
func TestPartUpload(t *testing.T) {
u, _ := url.Parse("https://szluyu99-1259132563.cos.ap-nanjing.myqcloud.com")
b := &cos.BaseURL{BucketURL: u}
client := cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: define.TencentSecretID,
SecretKey: define.TencentSecretKey,
},
})
key := "cloud-disk/exampleobject.jpeg"
UploadID := "165320975357e514f921b93de385fc75fb5c1f9702a5da1bacd086900c6c1a613805b15df1"
f, err := os.ReadFile("0.chunk") // md5 : 8f10a58845f83846ca5aa422edc0087d
if err != nil {
t.Fatal(err)
}
// opt可选
resp, err := client.Object.UploadPart(
context.Background(), key, UploadID, 1, bytes.NewReader(f), nil,
)
if err != nil {
t.Fatal(err)
}
PartETag := resp.Header.Get("ETag")
fmt.Println(PartETag)
}
// 分片上传完成
func TestPartUploadComplete(t *testing.T) {
u, _ := url.Parse("https://szluyu99-1259132563.cos.ap-nanjing.myqcloud.com")
b := &cos.BaseURL{BucketURL: u}
client := cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: define.TencentSecretID,
SecretKey: define.TencentSecretKey,
},
})
key := "cloud-disk/exampleobject.jpeg"
UploadID := "165320975357e514f921b93de385fc75fb5c1f9702a5da1bacd086900c6c1a613805b15df1"
opt := &cos.CompleteMultipartUploadOptions{}
opt.Parts = append(opt.Parts, cos.Object{
PartNumber: 1, ETag: "8f10a58845f83846ca5aa422edc0087d"},
)
_, _, err := client.Object.CompleteMultipartUpload(
context.Background(), key, UploadID, opt,
)
if err != nil {
t.Fatal(err)
}
}
文件分片上传
封装方法
helper.go 中关于文件分片上传 Cos 的封装的函数:
// Cos 分片上传初始化
func CosInitPart(ext string) (string, string, error) {
u, _ := url.Parse(define.CosBucket)
b := &cos.BaseURL{BucketURL: u}
client := cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: define.TencentSecretID,
SecretKey: define.TencentSecretKey,
},
})
key := "cloud-disk/" + UUID() + ext
v, _, err := client.Object.InitiateMultipartUpload(context.Background(), key, nil)
if err != nil {
return "", "", err
}
return key, v.UploadID, nil
}
// 分片上传
func CosPartUpload(r *http.Request) (string, error) {
u, _ := url.Parse(define.CosBucket)
b := &cos.BaseURL{BucketURL: u}
client := cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: define.TencentSecretID,
SecretKey: define.TencentSecretKey,
},
})
key := r.PostForm.Get("key")
UploadID := r.PostForm.Get("upload_id")
partNumber, err := strconv.Atoi(r.PostForm.Get("part_number"))
if err != nil {
return "", err
}
f, _, err := r.FormFile("file")
if err != nil {
return "", nil
}
buf := bytes.NewBuffer(nil)
io.Copy(buf, f)
// opt可选
resp, err := client.Object.UploadPart(
context.Background(), key, UploadID, partNumber, bytes.NewReader(buf.Bytes()), nil,
)
if err != nil {
return "", nil
}
return strings.Trim(resp.Header.Get("ETag"), "\""), nil
}
// 分片上传完成
func CosPartUploadComplete(key, uploadId string, co []cos.Object) error {
u, _ := url.Parse(define.CosBucket)
b := &cos.BaseURL{BatchURL: u}
client := cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: define.TencentSecretID,
SecretKey: define.TencentSecretKey,
},
})
opt := &cos.CompleteMultipartUploadOptions{}
opt.Parts = append(opt.Parts, co...)
_, _, err := client.Object.CompleteMultipartUpload(
context.Background(), key, uploadId, opt,
)
return err
}
文件上传前的准备
@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
// key
// 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"`
CosObjects []CosObject `json:"cos_objects"`
}
type CosObject {
PartNumber int `json:"part_number"`
Etag string `json:"etag"`
}
type FileUploadChunkCompleteReply {
Identity string `json:"identity"` // 存储池identity
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)