基于go开发minio对象存储服务

第一版 后端 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断点续传功能

MinIO 分片上传_恋喵大鲤鱼的博客-CSDN博客

https://github.com/minio/minio-go/blob/master/api-put-object-multipart.go

文件分片上传

流程测试:文件分片、合并分片、校验一致性

文件分片:

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值