diff --git a/pkg/common/db/s3/cos/cos.go b/pkg/common/db/s3/cos/cos.go index cfdaaa5e4..958fa0dc4 100644 --- a/pkg/common/db/s3/cos/cos.go +++ b/pkg/common/db/s3/cos/cos.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "net/url" + "path/filepath" "strconv" "strings" "time" @@ -36,6 +37,19 @@ const ( maxNumSize = 1000 ) +const ( + imagePng = "png" + imageJpg = "jpg" + imageJpeg = "jpeg" + imageGif = "gif" + imageWebp = "webp" +) + +const ( + videoSnapshotImagePng = "png" + videoSnapshotImageJpg = "jpg" +) + func NewCos() (s3.Interface, error) { conf := config.Config.Object.Cos u, err := url.Parse(conf.BucketURL) @@ -248,9 +262,61 @@ func (c *Cos) ListUploadedParts(ctx context.Context, uploadID string, name strin } func (c *Cos) AccessURL(ctx context.Context, name string, expire time.Duration, opt *s3.AccessURLOption) (string, error) { + var imageMogr string + snapshot := make(url.Values) var option *cos.PresignedURLOptions if opt != nil { query := make(url.Values) + if opt.Image != nil { + // https://cloud.tencent.com/document/product/436/44880 + style := make([]string, 0, 2) + wh := make([]string, 2) + if opt.Image.Width > 0 { + wh[0] = strconv.Itoa(opt.Image.Width) + } + if opt.Image.Height > 0 { + wh[1] = strconv.Itoa(opt.Image.Height) + } + if opt.Image.Width > 0 || opt.Image.Height > 0 { + style = append(style, strings.Join(wh, "x")) + } + switch opt.Image.Format { + case + imagePng, + imageJpg, + imageJpeg, + imageGif, + imageWebp: + opt.ContentType = "image/" + opt.Image.Format + if opt.Filename == "" { + opt.Filename = filepath.Base(name) + "." + opt.Image.Format + } else if filepath.Ext(opt.Filename) != "."+opt.Image.Format { + opt.Filename += "." + opt.Image.Format + } + style = append(style, "format/"+opt.Image.Format) + } + if len(style) > 0 { + imageMogr = "&imageMogr2/thumbnail/" + strings.Join(style, "/") + "/ignore-error/1" + } + } + if opt.Video != nil { + snapshot.Set("ci-process", "snapshot") + snapshot.Set("time", strconv.FormatFloat(float64(opt.Video.Time/time.Millisecond)/1000, 'f', 3, 64)) + switch opt.Video.ImageFormat { + case + videoSnapshotImagePng, + videoSnapshotImageJpg: + default: + opt.Video.ImageFormat = videoSnapshotImageJpg + } + snapshot.Set("format", opt.Video.ImageFormat) + opt.ContentType = "image/" + opt.Video.ImageFormat + if opt.Filename == "" { + opt.Filename = filepath.Base(name) + "." + opt.Video.ImageFormat + } else if filepath.Ext(opt.Filename) != "."+opt.Video.ImageFormat { + opt.Filename += "." + opt.Video.ImageFormat + } + } if opt.ContentType != "" { query.Set("response-content-type", opt.ContentType) } @@ -272,5 +338,20 @@ func (c *Cos) AccessURL(ctx context.Context, name string, expire time.Duration, if err != nil { return "", err } - return rawURL.String(), nil + urlStr := rawURL.String() + if len(snapshot) > 0 { + r, err := url.Parse(urlStr) + if err != nil { + return "", err + } + query := r.Query() + for key, values := range snapshot { + query[key] = values + } + r.RawQuery = query.Encode() + } + if imageMogr != "" { + urlStr += imageMogr + } + return urlStr, nil } diff --git a/pkg/common/db/s3/minio/minio.go b/pkg/common/db/s3/minio/minio.go index bb6e6ce24..19918d41a 100644 --- a/pkg/common/db/s3/minio/minio.go +++ b/pkg/common/db/s3/minio/minio.go @@ -18,6 +18,9 @@ import ( "context" "errors" "fmt" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/minio/minio-go/v7/pkg/signer" "net/http" "net/url" "strconv" @@ -25,10 +28,6 @@ import ( "sync" "time" - "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" - "github.com/minio/minio-go/v7/pkg/signer" - "github.com/OpenIMSDK/Open-IM-Server/pkg/common/config" "github.com/OpenIMSDK/Open-IM-Server/pkg/common/db/s3" ) diff --git a/pkg/common/db/s3/oss/oss.go b/pkg/common/db/s3/oss/oss.go index 774150389..13ed9900c 100644 --- a/pkg/common/db/s3/oss/oss.go +++ b/pkg/common/db/s3/oss/oss.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "net/url" + "path/filepath" "strconv" "strings" "time" @@ -36,6 +37,14 @@ const ( maxNumSize = 10000 ) +const ( + imagePng = "png" + imageJpg = "jpg" + imageJpeg = "jpeg" + imageGif = "gif" + imageWebp = "webp" +) + func NewOSS() (s3.Interface, error) { conf := config.Config.Object.Oss if conf.BucketURL == "" { @@ -139,7 +148,7 @@ func (o *OSS) AuthSign(ctx context.Context, uploadID string, name string, expire } for i, partNumber := range partNumbers { rawURL := fmt.Sprintf(`%s%s?partNumber=%d&uploadId=%s`, o.bucketURL, name, partNumber, uploadID) - request, err := http.NewRequestWithContext(ctx, http.MethodPut, rawURL, nil) + request, err := http.NewRequest(http.MethodPut, rawURL, nil) if err != nil { return nil, err } @@ -150,12 +159,7 @@ func (o *OSS) AuthSign(ctx context.Context, uploadID string, name string, expire request.Header.Set(oss.HTTPHeaderHost, request.Host) request.Header.Set(oss.HTTPHeaderDate, now) request.Header.Set(oss.HttpHeaderOssDate, now) - authorization := fmt.Sprintf( - `OSS %s:%s`, - o.credentials.GetAccessKeyID(), - o.getSignedStr(request, fmt.Sprintf(`/%s/%s?partNumber=%d&uploadId=%s`, o.bucket.BucketName, name, partNumber, uploadID), o.credentials.GetAccessKeySecret()), - ) - request.Header.Set(oss.HTTPHeaderAuthorization, authorization) + ossSignHeader(o.bucket.Client.Conn, request, fmt.Sprintf(`/%s/%s?partNumber=%d&uploadId=%s`, o.bucket.BucketName, name, partNumber, uploadID)) delete(request.Header, oss.HTTPHeaderDate) result.Parts[i] = s3.SignPart{ PartNumber: partNumber, @@ -266,6 +270,41 @@ func (o *OSS) ListUploadedParts(ctx context.Context, uploadID string, name strin func (o *OSS) AccessURL(ctx context.Context, name string, expire time.Duration, opt *s3.AccessURLOption) (string, error) { var opts []oss.Option if opt != nil { + if opt.Image != nil { + // https://help.aliyun.com/zh/oss/user-guide/resize-images-4?spm=a2c4g.11186623.0.0.4b3b1e4fWW6yji + var format string + switch opt.Image.Format { + case + imagePng, + imageJpg, + imageJpeg, + imageGif, + imageWebp: + format = opt.Image.Format + opt.ContentType = "image/" + format + if opt.Filename == "" { + opt.Filename = filepath.Base(name) + "." + format + } + } + var wh []string + if opt.Image.Width > 0 { + wh = append(wh, "w_"+strconv.Itoa(opt.Image.Width)) + } + if opt.Image.Height > 0 { + wh = append(wh, "h_"+strconv.Itoa(opt.Image.Height)) + } + if len(format)+len(wh) > 0 { + style := make([]string, 0, 3) + style = append(style, "image") + if len(wh) > 0 { + style = append(style, "resize,m_lfit,"+strings.Join(wh, ",")) + } + if format != "" { + style = append(style, "format,"+format) + } + opts = append(opts, oss.Process(strings.Join(style, "/"))) + } + } if opt.ContentType != "" { opts = append(opts, oss.ResponseContentType(opt.ContentType)) } diff --git a/pkg/common/db/s3/oss/sign.go b/pkg/common/db/s3/oss/sign.go index 9811ac476..b0fe30710 100644 --- a/pkg/common/db/s3/oss/sign.go +++ b/pkg/common/db/s3/oss/sign.go @@ -1,96 +1,10 @@ -// Copyright © 2023 OpenIM. All rights reserved. -// -// 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 oss import ( - "crypto/hmac" - "crypto/sha1" - "crypto/sha256" - "encoding/base64" - "hash" - "io" - "net/http" - "sort" - "strings" - "github.com/aliyun/aliyun-oss-go-sdk/oss" + "net/http" + _ "unsafe" ) -func (o *OSS) getAdditionalHeaderKeys(req *http.Request) ([]string, map[string]string) { - var keysList []string - keysMap := make(map[string]string) - srcKeys := make(map[string]string) - - for k := range req.Header { - srcKeys[strings.ToLower(k)] = "" - } - - for _, v := range o.bucket.Client.Config.AdditionalHeaders { - if _, ok := srcKeys[strings.ToLower(v)]; ok { - keysMap[strings.ToLower(v)] = "" - } - } - - for k := range keysMap { - keysList = append(keysList, k) - } - sort.Strings(keysList) - return keysList, keysMap -} - -func (o *OSS) getSignedStr(req *http.Request, canonicalizedResource string, keySecret string) string { - // Find out the "x-oss-"'s address in header of the request - ossHeadersMap := make(map[string]string) - additionalList, additionalMap := o.getAdditionalHeaderKeys(req) - for k, v := range req.Header { - if strings.HasPrefix(strings.ToLower(k), "x-oss-") { - ossHeadersMap[strings.ToLower(k)] = v[0] - } else if o.bucket.Client.Config.AuthVersion == oss.AuthV2 { - if _, ok := additionalMap[strings.ToLower(k)]; ok { - ossHeadersMap[strings.ToLower(k)] = v[0] - } - } - } - hs := newHeaderSorter(ossHeadersMap) - - // Sort the ossHeadersMap by the ascending order - hs.Sort() - - // Get the canonicalizedOSSHeaders - canonicalizedOSSHeaders := "" - for i := range hs.Keys { - canonicalizedOSSHeaders += hs.Keys[i] + ":" + hs.Vals[i] + "\n" - } - - // Give other parameters values - // when sign URL, date is expires - date := req.Header.Get(oss.HTTPHeaderDate) - contentType := req.Header.Get(oss.HTTPHeaderContentType) - contentMd5 := req.Header.Get(oss.HTTPHeaderContentMD5) - - // default is v1 signature - signStr := req.Method + "\n" + contentMd5 + "\n" + contentType + "\n" + date + "\n" + canonicalizedOSSHeaders + canonicalizedResource - h := hmac.New(func() hash.Hash { return sha1.New() }, []byte(keySecret)) - - // v2 signature - if o.bucket.Client.Config.AuthVersion == oss.AuthV2 { - signStr = req.Method + "\n" + contentMd5 + "\n" + contentType + "\n" + date + "\n" + canonicalizedOSSHeaders + strings.Join(additionalList, ";") + "\n" + canonicalizedResource - h = hmac.New(func() hash.Hash { return sha256.New() }, []byte(keySecret)) - } - _, _ = io.WriteString(h, signStr) - signedStr := base64.StdEncoding.EncodeToString(h.Sum(nil)) - - return signedStr -} +//go:linkname ossSignHeader github.com/aliyun/aliyun-oss-go-sdk/oss.(*Conn).signHeader +func ossSignHeader(c *oss.Conn, req *http.Request, canonicalizedResource string) diff --git a/pkg/common/db/s3/oss/sort.go b/pkg/common/db/s3/oss/sort.go deleted file mode 100644 index 667984ffb..000000000 --- a/pkg/common/db/s3/oss/sort.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright © 2023 OpenIM. All rights reserved. -// -// 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 oss - -import ( - "bytes" - "sort" -) - -// headerSorter defines the key-value structure for storing the sorted data in signHeader. -type headerSorter struct { - Keys []string - Vals []string -} - -// newHeaderSorter is an additional function for function SignHeader. -func newHeaderSorter(m map[string]string) *headerSorter { - hs := &headerSorter{ - Keys: make([]string, 0, len(m)), - Vals: make([]string, 0, len(m)), - } - - for k, v := range m { - hs.Keys = append(hs.Keys, k) - hs.Vals = append(hs.Vals, v) - } - return hs -} - -// Sort is an additional function for function SignHeader. -func (hs *headerSorter) Sort() { - sort.Sort(hs) -} - -// Len is an additional function for function SignHeader. -func (hs *headerSorter) Len() int { - return len(hs.Vals) -} - -// Less is an additional function for function SignHeader. -func (hs *headerSorter) Less(i, j int) bool { - return bytes.Compare([]byte(hs.Keys[i]), []byte(hs.Keys[j])) < 0 -} - -// Swap is an additional function for function SignHeader. -func (hs *headerSorter) Swap(i, j int) { - hs.Vals[i], hs.Vals[j] = hs.Vals[j], hs.Vals[i] - hs.Keys[i], hs.Keys[j] = hs.Keys[j], hs.Keys[i] -} diff --git a/pkg/common/db/s3/s3.go b/pkg/common/db/s3/s3.go index fadb09a0b..0fd4ccfe9 100644 --- a/pkg/common/db/s3/s3.go +++ b/pkg/common/db/s3/s3.go @@ -116,9 +116,24 @@ type ListUploadedPartsResult struct { UploadedParts []UploadedPart `xml:"Part"` } +type Image struct { + Format string `json:"format"` + Width int `json:"width"` + Height int `json:"height"` +} + +type Video struct { + Width int `json:"width"` + Height int `json:"height"` + Time time.Duration `json:"time"` + ImageFormat string `json:"format"` +} + type AccessURLOption struct { ContentType string `json:"contentType"` Filename string `json:"filename"` + Image *Image `json:"image"` + Video *Video `json:"video"` } type Interface interface {