Go语言实现网盘系统(未完)

该项目将基于go-zero和xorm

go-zero中文文档: https://legacy.go-zero.dev/cn/

Xorm中文文档: http://xorm.topgoer.com/

功能划分

整个项目可以分为3个模块: 用户模块、存储池模块和文件共享模块

数据库设计

用户是一个实体,建立对应的表user_basic,存储了用户信息,DDL如下:

create table users_basic
(
    id         int auto_increment
        primary key,
    identity   varchar(36) null,
    name       varchar(60) null,
    password   varchar(32) null,
    created_at datetime    null,
    updated_at datetime    null,
    deleted_at datetime    null,
    constraint id
        unique (id)
);

第一个就是id,是这张表的主键,identify字段表示用户的身份,name表示用户名,password表示密码,email表示邮箱,created_at表示创建时间,updated_at表示更新时间,deleted_at表示删除的时间

存储池对应了一张表,DDL如下:

create table repository_pool
(
    id       int auto_increment
        primary key,
    identity varchar(36)  null,
    hash     varchar(32)  null,
    name     varchar(255) null,
    ext      varchar(30)  null, # 文件扩展名
    size     double       null,
    path     varchar(255) null,
    constraint id
        unique (id)
);

用户存储库,和存储池的多对一的关系,DDL如下:

create table user_repository
(
    id                  int auto_increment
        primary key,
    identify            varchar(36)  null,
    user_identity       varchar(36)  null,
    parent_id           int          null,
    repository_identity varchar(36)  null,
    ext                 varchar(255) null,
    name                varchar(255) null,
    created_at          datetime     null,
    updated_at          datetime     null,
    deleted_at          datetime     null,
    constraint id
        unique (id)
);

共享文件有一张表,DDL如下:

create table share_basic
(
    id                  int auto_increment
        primary key,
    identity            varchar(36) null,
    user_identity       varchar(36) null,  # 分享者唯一标识
    repository_identity varchar(36) null,
    expired_time        int         null,  # 失效时间
    updated_at          datetime    null,
    created_at          datetime    null,
    deleted_at          datetime    null,
    constraint id
        unique (id)
);

在user_basic中添加数据,创建一些基本的用户

mysql> select * from users_basic;
+----+----------+------+----------+---------------------+---------------------+------------+
| id | identify | name | password | created_at          | updated_at          | deleted_at |
+----+----------+------+----------+---------------------+---------------------+------------+
|  1 | USER_1   | test | 123456   | 2022-09-12 11:47:54 | 2022-09-12 11:48:02 | NULL       |
+----+----------+------+----------+---------------------+---------------------+------------+

下面使用Xorm,安装方法: $ go get xorm.io/xorm

创建与一个models文件夹,编写与数据表对应的结构体和方法

package models

type UserBasic struct {
	Id       int
	Identity string
	Name     string
	Password string
	Email    string
}

func (table UserBasic) TableName() string {
	return "users_basic"
}

在项目中创建一个test文件夹,编写测试代码,尝试使用Xorm连接数据库:

package test

import (
	"bytes"
	"cloud-disk/models"
	"encoding/json"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"log"
	"testing"
	"xorm.io/xorm"
)

var engine *xorm.Engine

func TestXormTest(t *testing.T) {
	var err error
	engine, err = xorm.NewEngine("mysql", "golang:123456@/cloud_disk?charset=utf8")
	if err != nil {
		log.Fatalln(err)
	}
	data := make([]*models.UserBasic, 0)
	err = engine.Find(&data)
	if err != nil {
		t.Fatal(err)
	}
	b, err := json.Marshal(data)
	if err != nil {
		log.Fatalln(err)
	}
	dst := new(bytes.Buffer)
	err = json.Indent(dst, b, "", " ")
	if err != nil {
		t.Fatal(err)
	}
	fmt.Println(dst.String())
}

运行代码,获取到了表中数据:

=== RUN   TestXormTest
[
 {
  "Id": 1,
  "Identify": "USER_1",
  "Name": "test",
  "Password": "123456",
  "Email": ""
 }
]
--- PASS: TestXormTest (0.00s)
PASS
ok  	cloud-disk/test	0.008s

项目初始化

安装go-zero和goctl:

$ go get -u github.com/zeromicro/go-zero
$ go install github.com/zeromicro/go-zero/tools/goctl@latest

在项目根目录执行$ goctl api new core,创建一个API服务,这时会创建出一个core文件夹,里面包含了一些文件:

core
├── core.api
├── core.go
├── etc
│   └── core-api.yaml
└── internal
    ├── config
    │   └── config.go
    ├── handler
    │   ├── corehandler.go
    │   └── routes.go
    ├── logic
    │   └── corelogic.go
    ├── svc
    │   └── servicecontext.go
    └── types
        └── types.go

进入core文件夹,执行命令如下,启动服务:

$ go run core.go -f etc/core-api.yaml 
Starting server at 0.0.0.0:8888...

访问服务:

$ curl -i -X GET http://localhost:8888/from/you
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Traceparent: 00-835a1b4d021f755cd09ce2fdcd724afd-64ad02acaf7c22b5-00
Date: Mon, 12 Sep 2022 05:10:12 GMT
Content-Length: 4

基于自动生成的代码,还要编写具体的逻辑

进入internal/logic文件夹,改写如下函数:

func (l *CoreLogic) Core(req *types.Request) (resp *types.Response, err error) {
	// todo: add your logic here and delete this line
	
	return
}

添加如下代码:

func (l *CoreLogic) Core(req *types.Request) (resp *types.Response, err error) {
	resp = new(types.Response)
	resp.Message = "Hello Cloud Disk"
	return
}

再次启动服务并访问:

$ curl -i -X GET http://localhost:8888/from/you
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Traceparent: 00-511aeef2721af5adf8aa40ff2b7d9ce5-a2be678e70c0e2a3-00
Date: Mon, 12 Sep 2022 05:16:22 GMT
Content-Length: 30

{"message":"Hello Cloud Disk"}

服务给出了响应消息

在types.Response中有定义一个Message字段存放数据:

type Response struct {
	Message string `json:"message"`
}

将之前的models目录移动到core目录下,并创建init.go文件:

package models

import (
	"log"
	"xorm.io/xorm"
)

var Engine = Init()

func Init() *xorm.Engine {
	engine, err := xorm.NewEngine("mysql", "golang:123456@/cloud_disk?charset=utf8")
	if err != nil {
		log.Println("Xorm New Engine Error:", err)
		return nil
	}
	return engine
}

编写一段init函数,用于初始化Xorm数据库连接,将返回一个Engine,在logic中的Core函数中继续添加逻辑:

func (l *CoreLogic) Core(req *types.Request) (resp *types.Response, err error) {
	resp = new(types.Response)
	resp.Message = "Hello Cloud Disk"
	// 获取用户列表
	data := make([]*models.UserBasic, 0)
	err = models.Engine.Find(&data)
	if err != nil {
		log.Println("Get UserBasic Error", err)
		return
	}
	b, err := json.Marshal(data)
	if err != nil {
		log.Println("Marshal Error:", err)
		return
	}
	dst := new(bytes.Buffer)
	err = json.Indent(dst, b, "", " ")
	if err != nil {
		log.Println("Json Indent Error", err)
		return
	}
	fmt.Println(dst)

	resp = new(types.Response)
	resp.Message = dst.String()
	return
}

创建了一个data,用于存储查询到的数据,调用Find方法获取到数据表的所有数据,第一个参数为slice的指针或Map指针

现在再次启动服务并访问,可以看到程序取出了数据库中的数据:

$ curl -i -X GET http://localhost:8888/from/you
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Traceparent: 00-e8256a632201c5127e5bed8d44f66111-1d9f959482e3a918-00
Date: Mon, 12 Sep 2022 06:30:22 GMT
Content-Length: 140

{"message":"[\n {\n  \"Id\": 1,\n  \"Identify\": \"USER_1\",\n  \"Name\": \"test\",\n  \"Password\": \"123456\",\n  \"Email\": \"\"\n }\n]"}

实现密码登录

修改core.api文件,与登录功能相对应:

type LoginRequest {
	Name string `json: "name"`
	Password string `json: "password"`
}

type LoginReply {
	Token string `json:"token"`
}

service core-api {
	@handler User
	get /user/login(LoginRequest) returns (LoginReply)
}

LoginRequest封装了请求体结构,LoginRely对应响应体

定义好API文件后可以一键生成代码:

$ goctl api go -api core.api -dir . -style go_zero

会发现handler目录和logic目录下多了两个go文件

handler/user_handler.go:

package handler

import (
	"net/http"

	"cloud-disk/core/internal/logic"
	"cloud-disk/core/internal/svc"
	"cloud-disk/core/internal/types"
	"github.com/zeromicro/go-zero/rest/httpx"
)

func UserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.LoginRequest
		if err := httpx.Parse(r, &req); err != nil {
			httpx.Error(w, err)
			return
		}

		l := logic.NewUserLogic(r.Context(), svcCtx)
		resp, err := l.User(&req)
		if err != nil {
			httpx.Error(w, err)
		} else {
			httpx.OkJson(w, resp)
		}
	}
}

logic/user_logic.go:

package logic

import (
	"context"

	"cloud-disk/core/internal/svc"
	"cloud-disk/core/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

type UserLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserLogic {
	return &UserLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *UserLogic) User(req *types.LoginRequest) (resp *types.LoginReply, err error) {
	// todo: add your logic here and delete this line

	return
}

编写user_logic.go文件,添加具体逻辑:

func (l *UserLogic) User(req *types.LoginRequest) (resp *types.LoginReply, err error) {
	// 从数据库中查询当前用户
	user := new(models.UserBasic)
	get, err := models.Engine.Where("name = ? AND password = ?",
		req.Name, helper.Md5(req.Password)).Get(user)
	if err != nil {
		return nil, err
	}
	if !get {
		return nil, errors.New("wrong username or password")
	}

	// 生成token
	token, err := helper.GenerateToken(user.Id, user.Identify, user.Name)
	if err != nil {
		return nil, err
	}
	resp = new(types.LoginReply)
	resp.Token = token

	return
}

创建user对应的结构体,使用Xorm进行数据查询,密码取md5值,调用Get方法将数据保存到user,之后调用了一个GenerateToken,通过用户的Id、Identify和Name生成Token值,将token赋值到响应中

创建helper包,在helper包中定义一个GenerateToken方法,生成Token值:

package helper

import (
	"cloud-disk/core/define"
	"crypto/md5"
	"fmt"
	"github.com/golang-jwt/jwt/v4"
)

func Md5(s string) string {
	return fmt.Sprintf("%x", md5.Sum([]byte(s)))
}

func GenerateToken(id int, identify, name string) (string, error) {
	uc := define.UserClaim{
		Id:       id,
		Identify: identify,
		Name:     name,
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, uc)
	tokenString, err := token.SignedString([]byte(define.JwtKey))
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

在GenerateToken方法中将三个参数赋值到一个UserClaim结构体中,定义如下:

type UserClaim struct {
	Id       int
	Identify string
	Name     string
	jwt.StandardClaims
}

使用jwt.NewWithClaims基于指定的加密算法和用户信息生成一个token,之后进行类型转换并返回

运行测试:

$ go run core.go -f etc/core-api.yaml
Starting server at 0.0.0.0:8888...

使用Postman进行测试:

服务端成功返回了token值

那么密码登录就实现了

用户详情信息

实现的功能是查询用户详细信息

继续修改api文件:

service core-api {
	// 用户登录
	@handler UserLogin
	get /user/login(LoginRequest) returns (LoginReply)
	
	// 用户详情
	@handler UserDetail
	get /user/detail(UserDetailRequest) returns (UserDetailReply)
}

type LoginRequest {
	Name     string `json:"name"`
	Password string `json:"password"`
}

type LoginReply {
	Token string `json:"token"`
}

type UserDetailRequest {
	Identity string `json:"identity"`
}

type UserDetailReply {
	Name  string `json:"name"`
	Email string `json:"email"`
}

将原来的User修改为UserLogin,添加UserDetail

使用命令,重新生成:

$ goctl api go -api core.api -dir . -style go_zero
etc/core-api.yaml exists, ignored generation
internal/config/config.go exists, ignored generation
core.go exists, ignored generation
internal/svc/service_context.go exists, ignored generation
Done.

将原来的User相关的代码迁移到UserLogin中,此处省略

下面编写UserLogin相关的逻辑,即编写login中user_detail_login的相关函数:

func (l *UserDetailLogic) UserDetail(req *types.UserDetailRequest) (resp *types.UserDetailReply, err error) {
	resp = &types.UserDetailReply{}
	ub := new(models.UserBasic)
	get, err := models.Engine.Where("identity=?", req.Identity).Get(ub)
	if err != nil {
		return nil, err
	}

	if !get {
		return nil, errors.New("user not found")
	}
	resp.Name = ub.Name
	resp.Email = ub.Email

	return
}

现在可以用identity进行用户信息的查询:

查询结果如上

发送验证码

验证码会发送到邮箱,而go程序想要发送邮件,可以安装一个库:

$ go get github.com/jordan-wright/email

编写发送邮件的测试函数:

package test

import (
	"cloud-disk/core/define"
	"crypto/tls"
	"github.com/jordan-wright/email"
	"net/smtp"
	"testing"
)

func TestSendMail(t *testing.T) {
	e := email.NewEmail()
	e.From = "Get <n3ptune@163.com>"
	e.To = []string{"1017042497@qq.com"}
	e.Subject = "test"
	e.HTML = []byte("<h1>您的验证码为: 123456</h1>")
	err := e.SendWithTLS(define.MailServer, smtp.PlainAuth("",
		define.MailUsername, define.MailPassword, define.MailHost),
		&tls.Config{InsecureSkipVerify: true, ServerName: define.MailHost},
	)
	if err != nil {
		t.Fatal(err)
	}
}

调用SendWithTLS即可发送邮件,参数要传入一系列关于认证的信息,这里使用的是163邮箱,定义在了define包下:

var (
	MailPassword = "邮箱授权码"
	MailServer   = "smtp.163.com:465"
	MailHost     = "smtp.163.com"
	MailUsername = "n3ptune@163.com"
)

运行这段代码就可以发送邮件

修改API文件,添加一个handler:

service core-api {
	.....
	@handler MailCodeSend
	post /mail/code/send(MailCodeSendRequest) returns (MailCodeSendReply)
}

添加请求体及其响应:

type MailCodeSendRequest {
	Email string `json:"email"`
}

type MailCodeSendReply {
	Code string `json:"code"`
}

再次使用goctl一键生成代码

在helper包中封装一个函数,用于发送邮件验证码:

func SendCode(mail, code string) error {
	e := email.NewEmail()
	e.From = "Get <n3ptune@163.com>"
	e.To = []string{mail}
	e.Subject = "test"
	e.HTML = []byte("<h1>您的验证码为: " + code + "</h1>")
	err := e.SendWithTLS(define.MailServer, smtp.PlainAuth("",
		define.MailUsername, define.MailPassword, define.MailHost),
		&tls.Config{InsecureSkipVerify: true, ServerName: define.MailHost},
	)
	if err != nil {
		return err
	}
	return nil
}

放到logic文件夹下:

func (l *MailCodeSendLogic) MailCodeSend(req *types.MailCodeSendRequest) (resp *types.MailCodeSendReply, err error) {
	err = helper.SendCode(req.Email, "123456")
	if err != nil {
		return nil, err
	}
	return
}

使用Postman进行测试:

响应回复200,说明邮件发送成功,可以到邮箱进行验证

用户注册

向用户的邮箱发送验证码,用户通过验证码来注册

添加一条handler:

@handler MailCodeSendRegister
post /mail/code/send/register(MailCodeSendRequest) returns (MailCodeSendReply)

使用goctl重新生成代码

下面要利用redis,装对应的包:

$ go get github.com/go-redis/redis/v8

在redis中添加一对键值用于测试:

127.0.0.1:6379> set name "test"
OK

写一段测试代码:

package test

import (
	"context"
	"github.com/go-redis/redis/v8"
	"testing"
	"time"
)

var ctx = context.Background()
var rdb = redis.NewClient(&redis.Options{
	Addr:     "localhost:6379",
	Password: "",
	DB:       0,
})

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)
}

这段代码用于测试redis,TestSetValue的作用是设置redis键值,TestGetValue的作用是获取值

在models包中添加一段初始化redis的代码:

var RDB = InitRedis()

func InitRedis() *redis.Client {
	return redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})
}

这段代码可供其他包来调用,如下在logic目录下调用这段代码,设置一对有效的键值:

func (l *MailCodeSendRegisterLogic) MailCodeSendRegister(req *types.MailCodeSendRequest) (resp *types.MailCodeSendReply, err error) {
	code := helper.RandCode()
	// 发送验证码
	err = helper.SendCode(req.Email, code)
	models.RDB.Set(l.ctx, req.Email, code, time.Second*time.Duration(define.CodeExpire))
	return
}

CodeExpire是验证码过期时间,被定义在了define包中

RandCode用于生成一个随机验证码:

func RandCode() string {
	s := "1234567890"
	var code string
	rand.Seed(time.Now().UnixNano()) // 设置随机数种子
	for i := 0; i < define.CodeLength; i++ {
		code += string(s[rand.Intn(len(s))]) // 取随机数作为下标
	}
	return code
}

测试:

这时指定邮箱就会收到验证码,这个验证码同时也会被存入redis,可用来与用户输入进行校验

同时对于已经注册的邮箱会不予注册,要检查数据库是否已经存在该邮箱:

cnt, err := models.Engine.Where("email=?", req.Email).Count(new(models.UserBasic))
if err != nil {
	return nil, err
}
// 邮箱已经被注册
if cnt > 0 {
    err = errors.New("email exists")
    return
}

通过Where和Count方法,如果返回的个数大于0,说明已经存在了

上述实现的是发送验证码邮件,下面开始实现注册的逻辑

在API文件中添加一条handler:

@handler UserRegister
post /post/register(UserRegisterRequest) returns (UserRegisterReply)

在API文件中添加用户注册的请求体和响应体:

type UserRegisterRequest {
	Name     string `json:"name"`
	Password string `json:"password"`
	Email    string `json:"email"`
	Code     string `json:"code"`
}

type UserRegisterReply {
}

重新生成代码后,编写logic

func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *types.UserRegisterReply, err error) {
	code, err := models.RDB.Get(l.ctx, req.Email).Result()
	if err != nil {
		return nil, errors.New("email verification code not exists")
	}
	if code != req.Code {
		err = errors.New("verification code incorrect")
		return
	}
	// 判断用户名是否存在
	cnt, err := models.Engine.Where("name=?", req.Name).Count(new(models.UserBasic))
	if err != nil {
		return nil, err
	}
	if cnt > 0 {
		err = errors.New("user exists")
		return nil, err
	}
	user := &models.UserBasic{
		Identity: helper.UUID(),
		Name:     req.Name,
		Password: helper.Md5(req.Password),
		Email:    req.Email,
	}  
	n, err := models.Engine.Insert(user)
	if err != nil {
		return nil, err
	}
	log.Println("insert user row:", n)

	return
}

首先从redis中以email为键查询验证码,判断用户发送的验证码是否正确,如果验证码正确则检查该用户名是否已经在数据库中已经存在,如果不存在则注册用户,其中生成一个UUID作为Identity,密码要进行MD5加密,将用户信息插入数据库中,将借助第三方库生成UUID:

func UUID() string {
	return uuid.NewV4().String()
}

测试时首先向mail/code/send/register发送一条POST请求,请求邮箱验证

随后查看邮箱得到验证码,再发送一次POST请求,用于注册

注册成功的话,会返回200

检查数据库,可以看到数据插入成功,就代表用户注册成功了

配置文件

将代码中所需的配置信息放到yaml文件中,在etc下的yaml文件中添加内容

Mysql:
  DataSource: golang:123456@/cloud_disk?charset=utf8
Redis:
 Addr: localhost:6379

上面加上了mysql和redis的相关信息

在config.go中添加一个config结构体:

type Config struct {
	rest.RestConf
	Mysql struct {
		DataSource string
	}
	Redis struct {
		Addr string
	}
}

在service_context.go中初始化两个数据库的连接:

type ServiceContext struct {
	Config config.Config
	Engine *xorm.Engine
	RDB    *redis.Client
}

func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config: c,
		Engine: models.Init(c.Mysql.DataSource),
		RDB:    models.InitRedis(c),
	}
}

重写Init函数:

func Init(dataSource string) *xorm.Engine {
	engine, err := xorm.NewEngine("mysql", "golang:123456@/cloud_disk?charset=utf8")
	if err != nil {
		log.Println("Xorm New Engine Error:", err)
		return nil
	}
	return engine
}

func InitRedis(c config.Config) *redis.Client {
	return redis.NewClient(&redis.Options{
		Addr:     c.Redis.Addr,
		Password: "",
		DB:       0,
	})
}

原来的变量赋值就不需要了,其他代码也要稍作改动

上传文件到COS

注册一个腾讯云COS,创建一个存储桶:

image-20220915105108036

Go SDK文档: https://cloud.tencent.com/document/product/436/31215

下载SDK:

$ go get -u github.com/tencentyun/cos-go-sdk-v5

编写一段测试代码:

package test

import (
	"cloud-disk/core/define"
	"context"
	"fmt"
	"github.com/tencentyun/cos-go-sdk-v5"
	"net/http"
	"net/url"
	"testing"
)

func TestFileUploadByFilepath(t *testing.T) {
	u, _ := url.Parse("https://microdisk-1307366489.cos.ap-shanghai.myqcloud.com")
	b := &cos.BaseURL{BucketURL: u}
	client := cos.NewClient(b, &http.Client{
		Transport: &cos.AuthorizationTransport{
			SecretID:  define.TencentSecretID,  // 腾讯云SecretID
			SecretKey: define.TencentSecretKey, // 腾讯云SecretKey
		},
	})

	key := "mircodisk/apple.jpg"

	_, _, err := client.Object.Upload(
		context.Background(), key, "../file/apple.jpg", nil,
	)
	if err != nil {
		panic(err)
	}
}

根据SDK文档给出的代码稍作修改,实现文件的上传,在Upload函数中传入目标路径和要上传的文件的路径,即可将文件上传

还有一种PUT上传方式,要传入的是一个io.Reader:

func TestFileUploadByReader(t *testing.T) {
	u, _ := url.Parse("https://microdisk-1307366489.cos.ap-shanghai.myqcloud.com")
	b := &cos.BaseURL{BucketURL: u}
	client := cos.NewClient(b, &http.Client{
		Transport: &cos.AuthorizationTransport{
			SecretID:  define.TencentSecretID,
			SecretKey: define.TencentSecretKey,
		},
	})

	key := "mircodisk/test.jpg"

	f, err := os.ReadFile("../file/apple.jpg")
	if err != nil {
		return
	}

	_, err = client.Object.Put(
		context.Background(), key, bytes.NewReader(f), nil,
	)
	if err != nil {
		panic(err)
	}
}

接下来实现文件上传的逻辑

在API文件中添加Handler:

@handler FileUpload
post /file/upload(FileUploadRequest) returns (FileUploadReply)

在API文件中添加请求体:

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"`
}

然后重新生成代码

在helper包中添加一段函数:

func CosUpload(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,
		},
	})

	file, fileHeader, err := r.FormFile("file")
	key := "mircodisk/" + UUID() + path.Ext(fileHeader.Filename)

	_, err = client.Object.Put(
		context.Background(), key, file, nil,
	)
	if err != nil {
		panic(err)
	}
	return define.CosBucket + "/" + key, nil
}

该函数的作用是上传文件到COS,并返回地址,其中的FormFile的方法获取到文件的信息

在models中抽象respository_pool,作用于xorm:

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"
}

编写handler下的与FileUpload相关的代码:

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
		}

		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})
			return
		}

		cosPath, err := helper.CosUpload(r)
		if err != nil {
			return
		}

		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)
		}
	}
}

文件上传后,对文件取哈希值,若数据库中已经存在哈希值,则视为文件重复

在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,
		CreatedAt: time.Time{},
		UpdatedAt: time.Time{},
		DeletedAt: time.Time{},
	}

	_, err = l.svcCtx.Engine.Insert(rp)
	if err != nil {
		return nil, err
	}
	resp = new(types.FileUploadReply)
	resp.Identity = rp.Identity

	return
}

这段代码在文件上传时,将数据插入数据库

posted @ 2022-09-17 14:24  N3ptune  阅读(391)  评论(0编辑  收藏  举报