710 lines
20 KiB
Go
710 lines
20 KiB
Go
/*
|
||
* Copyright 2025 coze-dev Authors
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
package upload
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/xml"
|
||
"errors"
|
||
"fmt"
|
||
"hash/crc32"
|
||
"image"
|
||
_ "image/gif"
|
||
_ "image/jpeg"
|
||
_ "image/png"
|
||
"io"
|
||
"math"
|
||
"mime"
|
||
"mime/multipart"
|
||
"path"
|
||
"regexp"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
_ "golang.org/x/image/tiff"
|
||
_ "golang.org/x/image/webp"
|
||
|
||
"github.com/google/uuid"
|
||
|
||
"github.com/coze-dev/coze-studio/backend/api/model/file/upload"
|
||
"github.com/coze-dev/coze-studio/backend/api/model/flow/dataengine/dataset"
|
||
"github.com/coze-dev/coze-studio/backend/api/model/ocean/cloud/developer_api"
|
||
"github.com/coze-dev/coze-studio/backend/api/model/ocean/cloud/playground"
|
||
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
|
||
"github.com/coze-dev/coze-studio/backend/domain/upload/entity"
|
||
"github.com/coze-dev/coze-studio/backend/infra/contract/cache"
|
||
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
|
||
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
|
||
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
|
||
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
|
||
"github.com/coze-dev/coze-studio/backend/pkg/logs"
|
||
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
|
||
"github.com/coze-dev/coze-studio/backend/types/consts"
|
||
"github.com/coze-dev/coze-studio/backend/types/errno"
|
||
)
|
||
|
||
func InitService(oss storage.Storage, cache cache.Cmdable) {
|
||
SVC.cache = cache
|
||
SVC.oss = oss
|
||
}
|
||
|
||
var SVC = &UploadService{}
|
||
|
||
type UploadService struct {
|
||
oss storage.Storage
|
||
cache cache.Cmdable
|
||
}
|
||
|
||
const (
|
||
uploadKey = "UploadServiceUpload:%s"
|
||
uploadPartKey = "UploadServiceUpload:%s/parts"
|
||
partKey = "UploadServiceUpload/%s/part-%s"
|
||
)
|
||
|
||
func (u *UploadService) PartUploadFileInit(ctx context.Context, objKey string) (uploadID string, err error) {
|
||
uploadID = uuid.NewString()
|
||
key := fmt.Sprintf(uploadKey, uploadID)
|
||
err = u.cache.HSet(ctx,
|
||
key,
|
||
"objkey", objKey,
|
||
).Err()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
err = u.cache.Expire(ctx, key, time.Minute*10).Err()
|
||
return
|
||
}
|
||
|
||
type PartUploadFileRequest struct {
|
||
UploadID string
|
||
PartNumber string
|
||
Data []byte
|
||
}
|
||
|
||
type PartUploadFileResponse struct {
|
||
Crc32 string
|
||
}
|
||
|
||
type PartUploadFileCompleteRequest struct {
|
||
UploadID string
|
||
ObjKey string
|
||
Crc32Map map[string]string
|
||
}
|
||
|
||
func (u *UploadService) PartUploadFile(ctx context.Context, req *PartUploadFileRequest) (resp *PartUploadFileResponse, err error) {
|
||
key := fmt.Sprintf(uploadKey, req.UploadID)
|
||
exists, err := u.cache.Exists(ctx, key).Result()
|
||
if err != nil || exists == 0 {
|
||
return nil, fmt.Errorf("upload session invalid: %v", err)
|
||
}
|
||
crc32Val := crc32.ChecksumIEEE(req.Data)
|
||
partTosKey := fmt.Sprintf(partKey, req.UploadID, req.PartNumber)
|
||
err = u.oss.PutObject(ctx, partTosKey, req.Data, storage.WithExpires(time.Now().Add(10*time.Minute)))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
partMeta := map[string]interface{}{
|
||
"tos_key": partTosKey,
|
||
}
|
||
partMetaData, err := sonic.Marshal(partMeta)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
partKey := fmt.Sprintf(uploadPartKey, req.UploadID)
|
||
err = u.cache.HSet(ctx, partKey, req.PartNumber, string(partMetaData)).Err()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
err = u.cache.Expire(ctx, partKey, time.Minute*10).Err()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &PartUploadFileResponse{
|
||
Crc32: fmt.Sprintf("%08x", crc32Val),
|
||
}, nil
|
||
}
|
||
|
||
type tosPart struct {
|
||
PartNum int
|
||
Data []byte
|
||
}
|
||
|
||
func getContentType(uri string) (contentType string) {
|
||
_ = mime.AddExtensionType(".svg", "image/svg+xml")
|
||
_ = mime.AddExtensionType(".svgz", "image/svg+xml")
|
||
_ = mime.AddExtensionType(".webp", "image/webp")
|
||
_ = mime.AddExtensionType(".ico", "image/x-icon")
|
||
fileExtension := path.Base(uri)
|
||
ext := path.Ext(fileExtension)
|
||
contentType = mime.TypeByExtension(ext)
|
||
return
|
||
}
|
||
|
||
func (u *UploadService) PartUploadFileComplete(ctx context.Context, req *PartUploadFileCompleteRequest) error {
|
||
partKey := fmt.Sprintf(uploadPartKey, req.UploadID)
|
||
parts, err := u.cache.HGetAll(ctx, partKey).Result()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
tosParts := []*tosPart{}
|
||
for partNumStr, partData := range parts {
|
||
var partMeta map[string]string
|
||
if err := sonic.Unmarshal([]byte(partData), &partMeta); err != nil {
|
||
return fmt.Errorf("failed to parse part metadata: %v", err)
|
||
}
|
||
partNum, err := strconv.ParseInt(partNumStr, 10, 64)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
objKey, exist := partMeta["tos_key"]
|
||
if !exist {
|
||
return errors.New("tos key not exist")
|
||
}
|
||
byteData, err := u.oss.GetObject(ctx, objKey)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
tosParts = append(tosParts, &tosPart{PartNum: int(partNum), Data: byteData})
|
||
}
|
||
if len(tosParts) == 0 {
|
||
return errors.New("tos part is null")
|
||
}
|
||
sort.Slice(tosParts, func(i, j int) bool { return tosParts[i].PartNum < tosParts[j].PartNum })
|
||
if tosParts[len(tosParts)-1].PartNum != len(tosParts) || len(tosParts) != len(req.Crc32Map) {
|
||
return errors.New("check parts fail")
|
||
}
|
||
totalData := []byte{}
|
||
for _, val := range tosParts {
|
||
crc32 := fmt.Sprintf("%08x", crc32.ChecksumIEEE(val.Data))
|
||
crc32Check := req.Crc32Map[strconv.Itoa(val.PartNum)]
|
||
if crc32 != crc32Check {
|
||
return errors.New("crc32 check fail")
|
||
}
|
||
totalData = append(totalData, val.Data...)
|
||
}
|
||
contentType := getContentType(req.ObjKey)
|
||
if len(contentType) != 0 {
|
||
err = u.oss.PutObject(ctx, req.ObjKey, totalData, storage.WithContentType(contentType))
|
||
} else {
|
||
err = u.oss.PutObject(ctx, req.ObjKey, totalData)
|
||
}
|
||
|
||
return err
|
||
}
|
||
|
||
func (u *UploadService) GetIcon(ctx context.Context, req *developer_api.GetIconRequest) (
|
||
resp *developer_api.GetIconResponse, err error,
|
||
) {
|
||
iconURI := map[developer_api.IconType]string{
|
||
developer_api.IconType_Bot: consts.DefaultAgentIcon,
|
||
developer_api.IconType_User: consts.DefaultUserIcon,
|
||
developer_api.IconType_Plugin: consts.DefaultPluginIcon,
|
||
developer_api.IconType_Dataset: consts.DefaultDatasetIcon,
|
||
developer_api.IconType_Workflow: consts.DefaultWorkflowIcon,
|
||
developer_api.IconType_Imageflow: consts.DefaultPluginIcon,
|
||
developer_api.IconType_Society: consts.DefaultPluginIcon,
|
||
developer_api.IconType_Connector: consts.DefaultPluginIcon,
|
||
developer_api.IconType_ChatFlow: consts.DefaultPluginIcon,
|
||
developer_api.IconType_Voice: consts.DefaultPluginIcon,
|
||
developer_api.IconType_Enterprise: consts.DefaultTeamIcon,
|
||
}
|
||
|
||
uri := iconURI[req.GetIconType()]
|
||
if uri == "" {
|
||
return nil, errorx.New(errno.ErrUploadInvalidType,
|
||
errorx.KV("type", conv.Int64ToStr(int64(req.GetIconType()))))
|
||
}
|
||
|
||
url, err := u.oss.GetObjectUrl(ctx, iconURI[req.GetIconType()])
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &developer_api.GetIconResponse{
|
||
Data: &developer_api.GetIconResponseData{
|
||
IconList: []*developer_api.Icon{
|
||
{
|
||
URL: url,
|
||
URI: uri,
|
||
},
|
||
},
|
||
},
|
||
}, nil
|
||
}
|
||
func stringToMap(input string) map[string]string {
|
||
result := make(map[string]string)
|
||
|
||
pairs := strings.Split(input, ",")
|
||
|
||
for _, pair := range pairs {
|
||
parts := strings.Split(pair, ":")
|
||
if len(parts) == 2 {
|
||
key := strings.TrimSpace(parts[0])
|
||
value := strings.TrimSpace(parts[1])
|
||
result[key] = value
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
func (u *UploadService) UploadFileCommon(ctx context.Context, req *upload.CommonUploadRequest, fullPath string) (*upload.CommonUploadResponse, error) {
|
||
resp := upload.NewCommonUploadResponse()
|
||
re := regexp.MustCompile(consts.UploadURI + `/([^?]+)`)
|
||
match := re.FindStringSubmatch(fullPath)
|
||
if len(match) == 0 {
|
||
return nil, errorx.New(errno.ErrUploadInvalidParamCode, errorx.KV("msg", "tos key not found"))
|
||
}
|
||
objKey := match[1]
|
||
if strings.Contains(fullPath, "?uploads") {
|
||
uploadID, err := u.PartUploadFileInit(ctx, objKey)
|
||
if err != nil {
|
||
return resp, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", err.Error()))
|
||
}
|
||
resp.Error = &upload.Error{Code: 200}
|
||
resp.Payload = &upload.Payload{UploadID: uploadID}
|
||
return resp, nil
|
||
}
|
||
if len(ptr.From(req.PartNumber)) != 0 {
|
||
_, err := u.PartUploadFile(ctx, &PartUploadFileRequest{
|
||
UploadID: ptr.From(req.UploadID),
|
||
PartNumber: ptr.From(req.PartNumber),
|
||
Data: req.ByteData,
|
||
})
|
||
if err != nil {
|
||
return resp, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", err.Error()))
|
||
}
|
||
resp.Error = &upload.Error{Code: 200}
|
||
return resp, nil
|
||
}
|
||
if len(ptr.From(req.UploadID)) != 0 {
|
||
mp := stringToMap(string(req.ByteData))
|
||
err := u.PartUploadFileComplete(ctx, &PartUploadFileCompleteRequest{
|
||
UploadID: ptr.From(req.UploadID),
|
||
ObjKey: objKey,
|
||
Crc32Map: mp,
|
||
})
|
||
if err != nil {
|
||
return resp, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", err.Error()))
|
||
}
|
||
resp.Error = &upload.Error{Code: 200}
|
||
resp.Payload = &upload.Payload{Key: uuid.NewString()}
|
||
return resp, nil
|
||
}
|
||
var err error
|
||
contentType := getContentType(objKey)
|
||
if len(contentType) != 0 {
|
||
err = u.oss.PutObject(ctx, objKey, req.ByteData, storage.WithContentType(contentType))
|
||
} else {
|
||
err = u.oss.PutObject(ctx, objKey, req.ByteData)
|
||
}
|
||
if err != nil {
|
||
return resp, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", err.Error()))
|
||
}
|
||
resp.Error = &upload.Error{Code: 200}
|
||
resp.Payload = &upload.Payload{Key: uuid.NewString()}
|
||
return resp, err
|
||
}
|
||
|
||
func (u *UploadService) UploadFile(ctx context.Context, data []byte, objKey string) (*developer_api.UploadFileResponse, error) {
|
||
err := u.oss.PutObject(ctx, objKey, data)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
url, err := u.oss.GetObjectUrl(ctx, objKey)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &developer_api.UploadFileResponse{
|
||
Data: &developer_api.UploadFileData{
|
||
UploadURL: url,
|
||
UploadURI: objKey,
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
func (u *UploadService) GetShortcutIcons(ctx context.Context) ([]*playground.FileInfo, error) {
|
||
shortcutIcons := entity.GetDefaultShortcutIconURI()
|
||
fileList := make([]*playground.FileInfo, 0, len(shortcutIcons))
|
||
for _, uri := range shortcutIcons {
|
||
url, err := u.oss.GetObjectUrl(ctx, uri)
|
||
if err == nil {
|
||
fileList = append(fileList, &playground.FileInfo{
|
||
URL: url,
|
||
URI: uri,
|
||
})
|
||
}
|
||
}
|
||
return fileList, nil
|
||
}
|
||
|
||
func parseMultipartFormData(ctx context.Context, req *playground.UploadFileOpenRequest) (*multipart.Form, error) {
|
||
_, params, err := mime.ParseMediaType(req.ContentType)
|
||
if err != nil {
|
||
return nil, errorx.New(errno.ErrUploadInvalidContentTypeCode, errorx.KV("content-type", req.ContentType))
|
||
}
|
||
br := bytes.NewReader(req.Data)
|
||
mr := multipart.NewReader(br, params["boundary"])
|
||
|
||
form, err := mr.ReadForm(maxFileSize)
|
||
if errors.Is(err, multipart.ErrMessageTooLarge) {
|
||
return nil, errorx.New(errno.ErrUploadInvalidFileSizeCode)
|
||
} else if err != nil {
|
||
return nil, errorx.New(errno.ErrUploadMultipartFormDataReadFailedCode)
|
||
}
|
||
return form, nil
|
||
}
|
||
|
||
func genObjName(name string, id string) string {
|
||
|
||
return fmt.Sprintf("%s/%s/%s",
|
||
"bot_files",
|
||
id,
|
||
name,
|
||
)
|
||
}
|
||
|
||
func (u *UploadService) UploadFileOpen(ctx context.Context, req *playground.UploadFileOpenRequest) (*playground.UploadFileOpenResponse, error) {
|
||
resp := playground.UploadFileOpenResponse{}
|
||
resp.File = new(playground.File)
|
||
uid := ctxutil.MustGetUIDFromApiAuthCtx(ctx)
|
||
if uid == 0 {
|
||
return nil, errorx.New(errno.ErrKnowledgePermissionCode, errorx.KV("msg", "session required"))
|
||
}
|
||
form, err := parseMultipartFormData(ctx, req)
|
||
if err != nil {
|
||
logs.CtxErrorf(ctx, "parse multipart form data failed, err: %v", err)
|
||
return nil, err
|
||
}
|
||
if len(form.File["file"]) == 0 {
|
||
return nil, errorx.New(errno.ErrUploadEmptyFileCode)
|
||
} else if len(form.File["file"]) > 1 {
|
||
return nil, errorx.New(errno.ErrUploadFileUploadGreaterOneCode)
|
||
}
|
||
fileHeader := form.File["file"][0]
|
||
|
||
// open file
|
||
file, err := fileHeader.Open()
|
||
if err != nil {
|
||
return nil, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", "fileHeader open failed"))
|
||
}
|
||
defer file.Close()
|
||
data, err := io.ReadAll(file)
|
||
if err != nil {
|
||
return nil, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", "file upload io read failed"))
|
||
}
|
||
resp.File.Bytes = int64(len(data))
|
||
randID := uuid.NewString()
|
||
objName := genObjName(fileHeader.Filename, randID)
|
||
resp.File.FileName = fileHeader.Filename
|
||
resp.File.URI = objName
|
||
err = u.oss.PutObject(ctx, objName, data)
|
||
if err != nil {
|
||
return nil, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", "file upload to oss failed"))
|
||
}
|
||
url, err := u.oss.GetObjectUrl(ctx, objName)
|
||
if err != nil {
|
||
return nil, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", "get object url failed"))
|
||
}
|
||
resp.File.CreatedAt = time.Now().Unix()
|
||
resp.File.URL = url
|
||
return &resp, nil
|
||
}
|
||
|
||
func (u *UploadService) GetIconForDataset(ctx context.Context, req *dataset.GetIconRequest) (*dataset.GetIconResponse, error) {
|
||
resp := dataset.NewGetIconResponse()
|
||
var uri string
|
||
switch req.FormatType {
|
||
case dataset.FormatType_Text:
|
||
uri = TextKnowledgeDefaultIcon
|
||
case dataset.FormatType_Table:
|
||
uri = TableKnowledgeDefaultIcon
|
||
case dataset.FormatType_Image:
|
||
uri = ImageKnowledgeDefaultIcon
|
||
case dataset.FormatType_Database:
|
||
uri = DatabaseDefaultIcon
|
||
default:
|
||
uri = TextKnowledgeDefaultIcon
|
||
}
|
||
|
||
iconUrl, err := u.oss.GetObjectUrl(ctx, uri)
|
||
if err != nil {
|
||
return resp, err
|
||
}
|
||
resp.Icon = &dataset.Icon{
|
||
URL: iconUrl,
|
||
URI: uri,
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
func (u *UploadService) UploadSessionKey(ctx context.Context, sessionKey string, tosKey string) error {
|
||
return u.cache.Set(ctx, sessionKey, tosKey, time.Minute*30).Err()
|
||
}
|
||
|
||
type GetObjInfoBySessionKey struct {
|
||
ObjKey string
|
||
Width int32
|
||
Height int32
|
||
}
|
||
|
||
func isImageUri(uri string) bool {
|
||
if uri == "" {
|
||
return false
|
||
}
|
||
uri = strings.ToLower(uri)
|
||
fileExtension := path.Base(uri)
|
||
ext := path.Ext(fileExtension)
|
||
ext = ext[1:]
|
||
imageExtensions := map[string]bool{
|
||
"jpg": true,
|
||
"jpeg": true,
|
||
"png": true,
|
||
"gif": true,
|
||
"bmp": true,
|
||
"webp": true,
|
||
"tiff": true,
|
||
"svg": true,
|
||
"ico": true,
|
||
}
|
||
|
||
// 检查扩展名是否在图片扩展名列表中
|
||
return imageExtensions[ext]
|
||
}
|
||
func (u *UploadService) GetObjInfoBySessionKey(ctx context.Context, sessionKey string) (*GetObjInfoBySessionKey, error) {
|
||
resp := GetObjInfoBySessionKey{}
|
||
objKey, err := u.cache.Get(ctx, sessionKey).Result()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp.ObjKey = objKey
|
||
if isImageUri(objKey) {
|
||
content, err := u.oss.GetObject(ctx, objKey)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if isSVG(objKey) {
|
||
width, height, err := getSVGDimensions(content)
|
||
if err != nil {
|
||
logs.CtxErrorf(ctx, "get svg dimensions failed, err: %v", err)
|
||
// default val
|
||
resp.Width = 100
|
||
resp.Height = 100
|
||
return &resp, nil
|
||
}
|
||
resp.Width = width
|
||
resp.Height = height
|
||
} else {
|
||
img, _, err := image.Decode(bytes.NewReader(content))
|
||
if err != nil {
|
||
logs.CtxErrorf(ctx, "decode image failed, err: %v", err)
|
||
// default val
|
||
resp.Width = 100
|
||
resp.Height = 100
|
||
return &resp, nil
|
||
}
|
||
resp.Width = int32(img.Bounds().Dx())
|
||
resp.Height = int32(img.Bounds().Dy())
|
||
}
|
||
}
|
||
return &resp, nil
|
||
}
|
||
|
||
type SVG struct {
|
||
Width string `xml:"width,attr"`
|
||
Height string `xml:"height,attr"`
|
||
ViewBox string `xml:"viewBox,attr"`
|
||
}
|
||
|
||
// 获取 SVG 尺寸
|
||
func getSVGDimensions(content []byte) (width, height int32, err error) {
|
||
decoder := xml.NewDecoder(bytes.NewReader(content))
|
||
|
||
var svg SVG
|
||
if err := decoder.Decode(&svg); err != nil {
|
||
return 100, 100, nil
|
||
}
|
||
|
||
// 尝试从width属性获取
|
||
if svg.Width != "" {
|
||
w, err := parseDimension(svg.Width)
|
||
if err == nil {
|
||
width = w
|
||
}
|
||
}
|
||
|
||
// 尝试从height属性获取
|
||
if svg.Height != "" {
|
||
h, err := parseDimension(svg.Height)
|
||
if err == nil {
|
||
height = h
|
||
}
|
||
}
|
||
|
||
// 如果width或height未设置,尝试从viewBox获取
|
||
if width == 0 || height == 0 {
|
||
if svg.ViewBox != "" {
|
||
parts := strings.Fields(svg.ViewBox)
|
||
if len(parts) >= 4 {
|
||
if width == 0 {
|
||
w, err := strconv.ParseInt(parts[2], 10, 32)
|
||
if err == nil {
|
||
width = int32(w)
|
||
}
|
||
}
|
||
if height == 0 {
|
||
h, err := strconv.ParseInt(parts[3], 10, 32)
|
||
if err == nil {
|
||
height = int32(h)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if width == 0 || height == 0 {
|
||
return 100, 100, nil
|
||
}
|
||
|
||
return width, height, nil
|
||
}
|
||
func parseDimension(dim string) (int32, error) {
|
||
// 去除单位(px, pt, em, %等)和空格
|
||
dim = strings.TrimSpace(dim)
|
||
dim = strings.TrimRightFunc(dim, func(r rune) bool {
|
||
return (r < '0' || r > '9') && r != '.' && r != '-' && r != '+'
|
||
})
|
||
|
||
// 解析为float64
|
||
value, err := strconv.ParseFloat(dim, 64)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
// 四舍五入转换为int32
|
||
if value > math.MaxInt32 {
|
||
return math.MaxInt32, nil
|
||
}
|
||
if value < math.MinInt32 {
|
||
return math.MinInt32, nil
|
||
}
|
||
return int32(math.Round(value)), nil
|
||
}
|
||
func isSVG(uri string) bool {
|
||
uri = strings.ToLower(uri)
|
||
fileExtension := path.Base(uri)
|
||
ext := path.Ext(fileExtension)
|
||
ext = ext[1:]
|
||
return ext == "svg"
|
||
}
|
||
func (u *UploadService) ApplyImageUpload(ctx context.Context, req *upload.ApplyUploadActionRequest, host string) (*upload.ApplyUploadActionResponse, error) {
|
||
resp := upload.ApplyUploadActionResponse{}
|
||
storeUri := "tos-cn-i-v4nquku3lp/" + uuid.NewString() + ptr.From(req.FileExtension)
|
||
sessionKey := uuid.NewString()
|
||
auth := uuid.NewString()
|
||
uploadID := uuid.NewString()
|
||
uploadHost := string(host) + consts.UploadURI
|
||
resp.ResponseMetadata = &upload.ResponseMetadata{
|
||
RequestId: uuid.NewString(),
|
||
Action: "ApplyImageUpload",
|
||
Version: "",
|
||
Service: "",
|
||
Region: "",
|
||
}
|
||
resp.Result = &upload.ApplyUploadActionResult{
|
||
UploadAddress: &upload.UploadAddress{
|
||
StoreInfos: []*upload.StoreInfo{
|
||
{
|
||
StoreUri: storeUri,
|
||
Auth: auth,
|
||
UploadID: uploadID,
|
||
},
|
||
},
|
||
UploadHosts: []string{uploadHost},
|
||
SessionKey: sessionKey,
|
||
},
|
||
InnerUploadAddress: &upload.InnerUploadAddress{
|
||
UploadNodes: []*upload.UploadNode{
|
||
{
|
||
StoreInfos: []*upload.StoreInfo{
|
||
{
|
||
StoreUri: storeUri,
|
||
Auth: auth,
|
||
UploadID: uploadID,
|
||
},
|
||
},
|
||
UploadHost: uploadHost,
|
||
SessionKey: sessionKey,
|
||
},
|
||
},
|
||
},
|
||
RequestId: ptr.Of(uuid.NewString()),
|
||
}
|
||
err := u.UploadSessionKey(ctx, sessionKey, storeUri)
|
||
if err != nil {
|
||
return &resp, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", err.Error()))
|
||
}
|
||
return &resp, nil
|
||
}
|
||
|
||
func (u *UploadService) CommitImageUpload(ctx context.Context, req *upload.ApplyUploadActionRequest, host string) (*upload.ApplyUploadActionResponse, error) {
|
||
resp := upload.ApplyUploadActionResponse{}
|
||
type ssKey struct {
|
||
SessionKey string `json:"SessionKey"`
|
||
}
|
||
sskey := ssKey{}
|
||
err := sonic.Unmarshal(req.ByteData, &sskey)
|
||
if err != nil {
|
||
return &resp, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", err.Error()))
|
||
}
|
||
objInfo, err := u.GetObjInfoBySessionKey(ctx, sskey.SessionKey)
|
||
if err != nil {
|
||
return &resp, errorx.New(errno.ErrUploadSystemErrorCode, errorx.KV("msg", err.Error()))
|
||
}
|
||
resp.ResponseMetadata = &upload.ResponseMetadata{
|
||
RequestId: uuid.NewString(),
|
||
Action: "ApplyImageUpload",
|
||
Version: "",
|
||
Service: "",
|
||
Region: "",
|
||
}
|
||
resp.Result = &upload.ApplyUploadActionResult{
|
||
Results: []*upload.UploadResult{
|
||
{
|
||
Uri: objInfo.ObjKey,
|
||
UriStatus: 2000,
|
||
},
|
||
},
|
||
RequestId: ptr.Of(uuid.NewString()),
|
||
PluginResult: []*upload.PluginResult{
|
||
{
|
||
FileName: objInfo.ObjKey,
|
||
SourceUri: objInfo.ObjKey,
|
||
ImageUri: objInfo.ObjKey,
|
||
ImageWidth: objInfo.Width,
|
||
ImageHeight: objInfo.Height,
|
||
},
|
||
},
|
||
}
|
||
|
||
return &resp, nil
|
||
}
|