You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Open-IM-Server/pkg/common/db/controller/storage.go

487 lines
13 KiB

2 years ago
package controller
import "C"
import (
"context"
"crypto/md5"
"encoding/hex"
2 years ago
"encoding/json"
2 years ago
"errors"
"fmt"
2 years ago
"github.com/OpenIMSDK/Open-IM-Server/pkg/common/config"
"github.com/OpenIMSDK/Open-IM-Server/pkg/common/db/obj"
"github.com/OpenIMSDK/Open-IM-Server/pkg/common/db/table/relation"
"github.com/OpenIMSDK/Open-IM-Server/pkg/common/log"
"github.com/OpenIMSDK/Open-IM-Server/pkg/errs"
"github.com/OpenIMSDK/Open-IM-Server/pkg/proto/third"
"github.com/OpenIMSDK/Open-IM-Server/pkg/utils"
2 years ago
"github.com/google/uuid"
"path"
"strconv"
"time"
)
2 years ago
const (
hashPrefix = "hash"
tempPrefix = "temp"
fragmentPrefix = "fragment_"
)
2 years ago
type S3Database interface {
ApplyPut(ctx context.Context, req *third.ApplyPutReq) (*third.ApplyPutResp, error)
GetPut(ctx context.Context, req *third.GetPutReq) (*third.GetPutResp, error)
ConfirmPut(ctx context.Context, req *third.ConfirmPutReq) (*third.ConfirmPutResp, error)
2 years ago
GetUrl(ctx context.Context, req *third.GetUrlReq) (*third.GetUrlResp, error)
2 years ago
GetHashInfo(ctx context.Context, req *third.GetHashInfoReq) (*third.GetHashInfoResp, error)
2 years ago
CleanExpirationObject(ctx context.Context, t time.Time)
2 years ago
}
func NewS3Database(obj obj.Interface, hash relation.ObjectHashModelInterface, info relation.ObjectInfoModelInterface, put relation.ObjectPutModelInterface) S3Database {
return &s3Database{
obj: obj,
hash: hash,
info: info,
put: put,
}
}
type s3Database struct {
obj obj.Interface
hash relation.ObjectHashModelInterface
info relation.ObjectInfoModelInterface
put relation.ObjectPutModelInterface
}
// today 今天的日期
func (c *s3Database) today() string {
return time.Now().Format("20060102")
}
// fragmentName 根据序号生成文件名
func (c *s3Database) fragmentName(index int) string {
2 years ago
return fragmentPrefix + strconv.Itoa(index+1)
2 years ago
}
// getFragmentNum 获取分片大小和分片数量
func (c *s3Database) getFragmentNum(fragmentSize int64, objectSize int64) (int64, int) {
if size := c.obj.MinFragmentSize(); fragmentSize < size {
fragmentSize = size
}
if fragmentSize <= 0 || objectSize <= fragmentSize {
return objectSize, 1
} else {
num := int(objectSize / fragmentSize)
if objectSize%fragmentSize > 0 {
num++
}
if n := c.obj.MaxFragmentNum(); num > n {
num = n
}
return fragmentSize, num
}
}
func (c *s3Database) CheckHash(hash string) error {
val, err := hex.DecodeString(hash)
if err != nil {
return err
}
if len(val) != md5.Size {
2 years ago
return errs.ErrArgs.Wrap("invalid hash")
2 years ago
}
return nil
}
func (c *s3Database) urlName(name string) string {
return config.Config.Object.ApiURL + name
2 years ago
}
func (c *s3Database) UUID() string {
return uuid.New().String()
}
func (c *s3Database) HashName(hash string) string {
2 years ago
return path.Join(hashPrefix, hash+"_"+c.today()+"_"+c.UUID())
2 years ago
}
func (c *s3Database) isNotFound(err error) bool {
2 years ago
return relation.IsNotFound(err)
2 years ago
}
func (c *s3Database) ApplyPut(ctx context.Context, req *third.ApplyPutReq) (*third.ApplyPutResp, error) {
if err := c.CheckHash(req.Hash); err != nil {
return nil, err
}
if err := c.obj.CheckName(req.Name); err != nil {
return nil, err
}
2 years ago
if req.ValidTime != 0 && req.ValidTime <= time.Now().UnixMilli() {
return nil, errors.New("invalid ValidTime")
2 years ago
}
var expirationTime *time.Time
2 years ago
if req.ValidTime != 0 {
expirationTime = utils.ToPtr(time.UnixMilli(req.ValidTime))
2 years ago
}
if hash, err := c.hash.Take(ctx, req.Hash, c.obj.Name()); err == nil {
o := relation.ObjectInfoModel{
2 years ago
Name: req.Name,
Hash: hash.Hash,
ValidTime: expirationTime,
ContentType: req.ContentType,
CreateTime: time.Now(),
2 years ago
}
if err := c.info.SetObject(ctx, &o); err != nil {
return nil, err
}
return &third.ApplyPutResp{Url: c.urlName(o.Name)}, nil // 服务器已存在
} else if !c.isNotFound(err) {
return nil, err
}
// 新上传
var pack int
const effective = time.Hour * 24 * 2
req.FragmentSize, pack = c.getFragmentNum(req.FragmentSize, req.Size)
put := relation.ObjectPutModel{
2 years ago
PutID: c.UUID(),
Hash: req.Hash,
Name: req.Name,
ObjectSize: req.Size,
ContentType: req.ContentType,
FragmentSize: req.FragmentSize,
ValidTime: expirationTime,
EffectiveTime: time.Now().Add(effective),
2 years ago
}
2 years ago
put.Path = path.Join(tempPrefix, c.today(), req.Hash, put.PutID)
2 years ago
putURLs := make([]string, 0, pack)
for i := 0; i < pack; i++ {
url, err := c.obj.PresignedPutURL(ctx, &obj.ApplyPutArgs{
Bucket: c.obj.TempBucket(),
Name: path.Join(put.Path, c.fragmentName(i)),
Effective: effective,
MaxObjectSize: req.FragmentSize,
})
if err != nil {
return nil, err
}
putURLs = append(putURLs, url)
}
2 years ago
urlsJsonData, err := json.Marshal(putURLs)
if err != nil {
return nil, err
}
t := md5.Sum(urlsJsonData)
put.PutURLsHash = hex.EncodeToString(t[:])
2 years ago
put.CreateTime = time.Now()
if err := c.put.Create(ctx, []*relation.ObjectPutModel{&put}); err != nil {
return nil, err
}
return &third.ApplyPutResp{
PutID: put.PutID,
FragmentSize: put.FragmentSize,
PutURLs: putURLs,
2 years ago
ValidTime: put.EffectiveTime.UnixMilli(),
2 years ago
}, nil
}
func (c *s3Database) GetPut(ctx context.Context, req *third.GetPutReq) (*third.GetPutResp, error) {
up, err := c.put.Take(ctx, req.PutID)
if err != nil {
return nil, err
}
if up.Complete {
return nil, errors.New("up completed")
}
_, pack := c.getFragmentNum(up.FragmentSize, up.ObjectSize)
fragments := make([]*third.GetPutFragment, pack)
for i := 0; i < pack; i++ {
name := path.Join(up.Path, c.fragmentName(i))
o, err := c.obj.GetObjectInfo(ctx, &obj.BucketObject{
Bucket: c.obj.TempBucket(),
Name: name,
})
if err != nil {
if c.obj.IsNotFound(err) {
fragments[i] = &third.GetPutFragment{}
continue
}
return nil, err
}
fragments[i] = &third.GetPutFragment{Size: o.Size, Hash: o.Hash}
}
2 years ago
var validTime int64
if up.ValidTime != nil {
validTime = up.ValidTime.UnixMilli()
2 years ago
}
return &third.GetPutResp{
FragmentSize: up.FragmentSize,
Size: up.ObjectSize,
Name: up.Name,
Hash: up.Hash,
Fragments: fragments,
2 years ago
PutURLsHash: up.PutURLsHash,
2 years ago
ContentType: up.ContentType,
ValidTime: validTime,
2 years ago
}, nil
}
func (c *s3Database) ConfirmPut(ctx context.Context, req *third.ConfirmPutReq) (_ *third.ConfirmPutResp, _err error) {
2 years ago
put, err := c.put.Take(ctx, req.PutID)
2 years ago
if err != nil {
return nil, err
}
2 years ago
_, pack := c.getFragmentNum(put.FragmentSize, put.ObjectSize)
2 years ago
defer func() {
if _err == nil {
// 清理上传的碎片
2 years ago
err := c.obj.DeleteObject(ctx, &obj.BucketObject{Bucket: c.obj.TempBucket(), Name: put.Path})
2 years ago
if err != nil {
2 years ago
log.ZError(ctx, "deleteObject failed", err, "Bucket", c.obj.TempBucket(), "Path", put.Path)
2 years ago
}
}
}()
2 years ago
if put.Complete {
2 years ago
return nil, errors.New("put completed")
}
now := time.Now().UnixMilli()
2 years ago
if put.EffectiveTime.UnixMilli() < now {
2 years ago
return nil, errors.New("upload expired")
}
2 years ago
if put.ValidTime != nil && put.ValidTime.UnixMilli() < now {
2 years ago
return nil, errors.New("object expired")
}
2 years ago
if hash, err := c.hash.Take(ctx, put.Hash, c.obj.Name()); err == nil {
2 years ago
o := relation.ObjectInfoModel{
2 years ago
Name: put.Name,
Hash: hash.Hash,
ValidTime: put.ValidTime,
ContentType: put.ContentType,
CreateTime: time.Now(),
2 years ago
}
if err := c.info.SetObject(ctx, &o); err != nil {
return nil, err
}
2 years ago
defer func() {
2 years ago
err := c.obj.DeleteObject(ctx, &obj.BucketObject{
2 years ago
Bucket: c.obj.TempBucket(),
Name: put.Path,
})
if err != nil {
2 years ago
log.ZError(ctx, "DeleteObject", err, "Bucket", c.obj.TempBucket(), "Path", put.Path)
2 years ago
}
}()
2 years ago
// 服务端已存在
return &third.ConfirmPutResp{
Url: c.urlName(o.Name),
}, nil
2 years ago
} else if !c.isNotFound(err) {
2 years ago
return nil, err
}
src := make([]obj.BucketObject, pack)
for i := 0; i < pack; i++ {
2 years ago
name := path.Join(put.Path, c.fragmentName(i))
2 years ago
o, err := c.obj.GetObjectInfo(ctx, &obj.BucketObject{
Bucket: c.obj.TempBucket(),
Name: name,
})
if err != nil {
return nil, err
}
if i+1 == pack { // 最后一个
2 years ago
size := put.ObjectSize - put.FragmentSize*int64(i)
2 years ago
if size != o.Size {
return nil, fmt.Errorf("last fragment %d size %d not equal to %d hash %s", i, o.Size, size, o.Hash)
}
} else {
2 years ago
if o.Size != put.FragmentSize {
return nil, fmt.Errorf("fragment %d size %d not equal to %d hash %s", i, o.Size, put.FragmentSize, o.Hash)
2 years ago
}
}
src[i] = obj.BucketObject{
Bucket: c.obj.TempBucket(),
Name: name,
}
}
dst := &obj.BucketObject{
Bucket: c.obj.DataBucket(),
2 years ago
Name: c.HashName(put.Hash),
2 years ago
}
if len(src) == 1 { // 未分片直接触发copy
// 检查数据完整性,避免脏数据
o, err := c.obj.GetObjectInfo(ctx, &src[0])
if err != nil {
return nil, err
}
2 years ago
if put.ObjectSize != o.Size {
return nil, fmt.Errorf("size mismatching should %d reality %d", put.ObjectSize, o.Size)
2 years ago
}
2 years ago
if put.Hash != o.Hash {
return nil, fmt.Errorf("hash mismatching should %s reality %s", put.Hash, o.Hash)
2 years ago
}
2 years ago
if err := c.obj.CopyObject(ctx, &src[0], dst); err != nil {
2 years ago
return nil, err
}
} else {
tempBucket := &obj.BucketObject{
Bucket: c.obj.TempBucket(),
2 years ago
Name: path.Join(put.Path, "merge_"+c.UUID()),
2 years ago
}
defer func() { // 清理合成的文件
2 years ago
if err := c.obj.DeleteObject(ctx, tempBucket); err != nil {
log.ZError(ctx, "DeleteObject", err, "Bucket", tempBucket.Bucket, "Path", tempBucket.Name)
2 years ago
}
}()
err := c.obj.ComposeObject(ctx, src, tempBucket)
if err != nil {
return nil, err
}
info, err := c.obj.GetObjectInfo(ctx, tempBucket)
if err != nil {
return nil, err
}
2 years ago
if put.ObjectSize != info.Size {
return nil, fmt.Errorf("size mismatch should %d reality %d", put.ObjectSize, info.Size)
2 years ago
}
2 years ago
if put.Hash != info.Hash {
return nil, fmt.Errorf("hash mismatch should %s reality %s", put.Hash, info.Hash)
2 years ago
}
2 years ago
if err := c.obj.CopyObject(ctx, tempBucket, dst); err != nil {
2 years ago
return nil, err
}
}
h := &relation.ObjectHashModel{
2 years ago
Hash: put.Hash,
2 years ago
Engine: c.obj.Name(),
2 years ago
Size: put.ObjectSize,
2 years ago
Bucket: c.obj.DataBucket(),
2 years ago
Name: dst.Name,
2 years ago
CreateTime: time.Now(),
}
2 years ago
if err := c.hash.Create(ctx, []*relation.ObjectHashModel{h}); err != nil {
return nil, err
}
2 years ago
o := &relation.ObjectInfoModel{
2 years ago
Name: put.Name,
Hash: put.Hash,
ContentType: put.ContentType,
ValidTime: put.ValidTime,
CreateTime: time.Now(),
2 years ago
}
2 years ago
if err := c.info.SetObject(ctx, o); err != nil {
return nil, err
}
2 years ago
if err := c.put.SetCompleted(ctx, put.PutID); err != nil {
2 years ago
log.ZError(ctx, "SetCompleted", err, "PutID", put.PutID)
2 years ago
}
return &third.ConfirmPutResp{
Url: c.urlName(o.Name),
}, nil
}
2 years ago
2 years ago
func (c *s3Database) GetUrl(ctx context.Context, req *third.GetUrlReq) (*third.GetUrlResp, error) {
info, err := c.info.Take(ctx, req.Name)
if err != nil {
return nil, err
}
2 years ago
if info.ValidTime != nil && info.ValidTime.Before(time.Now()) {
2 years ago
return nil, errs.ErrRecordNotFound.Wrap("object expired")
}
hash, err := c.hash.Take(ctx, info.Hash, c.obj.Name())
if err != nil {
return nil, err
}
2 years ago
opt := obj.HeaderOption{ContentType: info.ContentType}
if req.Attachment {
opt.Filename = info.Name
}
2 years ago
u, err := c.obj.PresignedGetURL(ctx, hash.Bucket, hash.Name, time.Duration(req.Expires)*time.Millisecond, &opt)
2 years ago
if err != nil {
return nil, err
}
2 years ago
return &third.GetUrlResp{
2 years ago
Url: u,
2 years ago
Size: hash.Size,
Hash: hash.Hash,
}, nil
}
2 years ago
func (c *s3Database) CleanExpirationObject(ctx context.Context, t time.Time) {
// 清理上传产生的临时文件
c.cleanPutTemp(ctx, t, 10)
// 清理hash引用全过期的文件
c.cleanExpirationObject(ctx, t)
// 清理没有引用的hash对象
c.clearNoCitation(ctx, c.obj.Name(), 10)
}
func (c *s3Database) cleanPutTemp(ctx context.Context, t time.Time, num int) {
for {
puts, err := c.put.FindExpirationPut(ctx, t, num)
if err != nil {
2 years ago
log.ZError(ctx, "FindExpirationPut", err, "Time", t, "Num", num)
2 years ago
return
}
if len(puts) == 0 {
return
}
for _, put := range puts {
2 years ago
err := c.obj.DeleteObject(ctx, &obj.BucketObject{Bucket: c.obj.TempBucket(), Name: put.Path})
2 years ago
if err != nil {
2 years ago
log.ZError(ctx, "DeleteObject", err, "Bucket", c.obj.TempBucket(), "Path", put.Path)
2 years ago
return
}
}
ids := utils.Slice(puts, func(e *relation.ObjectPutModel) string { return e.PutID })
err = c.put.DelPut(ctx, ids)
if err != nil {
2 years ago
log.ZError(ctx, "DelPut", err, "PutID", ids)
2 years ago
return
}
}
}
func (c *s3Database) cleanExpirationObject(ctx context.Context, t time.Time) {
err := c.info.DeleteExpiration(ctx, t)
if err != nil {
2 years ago
log.ZError(ctx, "DeleteExpiration", err, "Time", t)
2 years ago
}
}
func (c *s3Database) clearNoCitation(ctx context.Context, engine string, limit int) {
for {
list, err := c.hash.DeleteNoCitation(ctx, engine, limit)
if err != nil {
2 years ago
log.ZError(ctx, "DeleteNoCitation", err, "Engine", engine, "Limit", limit)
2 years ago
return
}
if len(list) == 0 {
return
}
var hasErr bool
for _, h := range list {
2 years ago
err := c.obj.DeleteObject(ctx, &obj.BucketObject{Bucket: h.Bucket, Name: h.Name})
2 years ago
if err != nil {
hasErr = true
2 years ago
log.ZError(ctx, "DeleteObject", err, "Bucket", h.Bucket, "Path", h.Name)
2 years ago
continue
}
}
if hasErr {
return
}
}
}
2 years ago
func (c *s3Database) GetHashInfo(ctx context.Context, req *third.GetHashInfoReq) (*third.GetHashInfoResp, error) {
2 years ago
if err := c.CheckHash(req.Hash); err != nil {
return nil, err
}
2 years ago
o, err := c.hash.Take(ctx, req.Hash, c.obj.Name())
if err != nil {
return nil, err
}
return &third.GetHashInfoResp{
Hash: o.Hash,
Size: o.Size,
}, nil
}