storage-tencentyuncos/tencentyuncos.go (246 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 tencentyuncos
import (
"crypto/rand"
"embed"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/apache/answer/pkg/checker"
"github.com/apache/answer-plugins/storage-tencentyuncos/i18n"
"github.com/apache/answer-plugins/util"
"github.com/apache/answer/plugin"
"github.com/tencentyun/cos-go-sdk-v5"
)
//go:embed info.yaml
var Info embed.FS
type Storage struct {
Config *StorageConfig
}
type StorageConfig struct {
Region string `json:"region"`
BucketName string `json:"bucket_name"`
ObjectKeyPrefix string `json:"object_key_prefix"`
SecretID string `json:"secret_id"`
SecretKey string `json:"secret_key"`
VisitUrlPrefix string `json:"visit_url_prefix"`
ACL string `json:"acl"`
}
func init() {
plugin.Register(&Storage{
Config: &StorageConfig{},
})
}
func (s *Storage) Info() plugin.Info {
info := &util.Info{}
info.GetInfo(Info)
return plugin.Info{
Name: plugin.MakeTranslator(i18n.InfoName),
SlugName: info.SlugName,
Description: plugin.MakeTranslator(i18n.InfoDescription),
Author: info.Author,
Version: info.Version,
Link: info.Link,
}
}
func (s *Storage) UploadFile(ctx *plugin.GinContext, condition plugin.UploadFileCondition) (resp plugin.UploadFileResponse) {
resp = plugin.UploadFileResponse{}
BucketURL, _ := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", s.Config.BucketName, s.Config.Region))
BaseURL := &cos.BaseURL{BucketURL: BucketURL}
client := cos.NewClient(BaseURL, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: s.Config.SecretID,
SecretKey: s.Config.SecretKey,
},
})
_, err := client.Bucket.IsExist(ctx)
if err != nil {
resp.OriginalError = fmt.Errorf("head bucket failed: %v", err)
resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrMisStorageConfig)
return resp
}
file, err := ctx.FormFile("file")
if err != nil {
resp.OriginalError = fmt.Errorf("get upload file failed: %v", err)
resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrFileNotFound)
return resp
}
if s.IsUnsupportedFileType(file.Filename, condition) {
resp.OriginalError = fmt.Errorf("file type not allowed")
resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrUnsupportedFileType)
return resp
}
if s.ExceedFileSizeLimit(file.Size, condition) {
resp.OriginalError = fmt.Errorf("file size too large")
resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrOverFileSizeLimit)
return resp
}
openFile, err := file.Open()
if err != nil {
resp.OriginalError = fmt.Errorf("get file failed: %v", err)
resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrFileNotFound)
return resp
}
defer openFile.Close()
objectKey := s.createObjectKey(file.Filename, condition.Source)
var options *cos.ObjectPutOptions
if s.Config.ACL == "public-read" {
options = &cos.ObjectPutOptions{
ACLHeaderOptions: &cos.ACLHeaderOptions{
XCosACL: "public-read",
},
}
}
_, err = client.Object.Put(ctx, objectKey, openFile, options)
if err != nil {
resp.OriginalError = fmt.Errorf("upload file failed: %v", err)
resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrUploadFileFailed)
return resp
}
resp.FullURL = s.Config.VisitUrlPrefix + objectKey
return resp
}
func (s *Storage) IsUnsupportedFileType(originalFilename string, condition plugin.UploadFileCondition) bool {
if condition.Source == plugin.AdminBranding || condition.Source == plugin.UserAvatar {
ext := strings.ToLower(filepath.Ext(originalFilename))
if _, ok := plugin.DefaultFileTypeCheckMapping[condition.Source][ext]; ok {
return false
}
return true
}
// check the post image and attachment file type check
if condition.Source == plugin.UserPost {
return checker.IsUnAuthorizedExtension(originalFilename, condition.AuthorizedImageExtensions)
}
return checker.IsUnAuthorizedExtension(originalFilename, condition.AuthorizedAttachmentExtensions)
}
func (s *Storage) ExceedFileSizeLimit(fileSize int64, condition plugin.UploadFileCondition) bool {
if condition.Source == plugin.UserPostAttachment {
return fileSize > int64(condition.MaxAttachmentSize)*1024*1024
}
return fileSize > int64(condition.MaxImageSize)*1024*1024
}
func (s *Storage) createObjectKey(originalFilename string, source plugin.UploadSource) string {
ext := strings.ToLower(filepath.Ext(originalFilename))
randomString := s.randomObjectKey()
switch source {
case plugin.UserAvatar:
return s.Config.ObjectKeyPrefix + "avatar/" + randomString + ext
case plugin.UserPost:
return s.Config.ObjectKeyPrefix + "post/" + randomString + ext
case plugin.UserPostAttachment:
return s.Config.ObjectKeyPrefix + "attachment/" + randomString + ext
case plugin.AdminBranding:
return s.Config.ObjectKeyPrefix + "branding/" + randomString + ext
default:
return s.Config.ObjectKeyPrefix + "other/" + randomString + ext
}
}
func (s *Storage) randomObjectKey() string {
bytes := make([]byte, 4)
_, _ = rand.Read(bytes)
return fmt.Sprintf("%d", time.Now().UnixNano()) + hex.EncodeToString(bytes)
}
func (s *Storage) CheckFileType(originalFilename string, source plugin.UploadSource) bool {
ext := strings.ToLower(filepath.Ext(originalFilename))
if _, ok := plugin.DefaultFileTypeCheckMapping[source][ext]; ok {
return true
}
return false
}
func (s *Storage) ConfigFields() []plugin.ConfigField {
return []plugin.ConfigField{
{
Name: "region",
Type: plugin.ConfigTypeInput,
Title: plugin.MakeTranslator(i18n.ConfigRegionTitle),
Description: plugin.MakeTranslator(i18n.ConfigRegionDescription),
Required: true,
UIOptions: plugin.ConfigFieldUIOptions{
InputType: plugin.InputTypeText,
},
Value: s.Config.Region,
},
{
Name: "bucket_name",
Type: plugin.ConfigTypeInput,
Title: plugin.MakeTranslator(i18n.ConfigBucketNameTitle),
Description: plugin.MakeTranslator(i18n.ConfigBucketNameDescription),
Required: true,
UIOptions: plugin.ConfigFieldUIOptions{
InputType: plugin.InputTypeText,
},
Value: s.Config.BucketName,
},
{
Name: "object_key_prefix",
Type: plugin.ConfigTypeInput,
Title: plugin.MakeTranslator(i18n.ConfigObjectKeyPrefixTitle),
Description: plugin.MakeTranslator(i18n.ConfigObjectKeyPrefixDescription),
Required: false,
UIOptions: plugin.ConfigFieldUIOptions{
InputType: plugin.InputTypeText,
},
Value: s.Config.ObjectKeyPrefix,
},
{
Name: "secret_id",
Type: plugin.ConfigTypeInput,
Title: plugin.MakeTranslator(i18n.ConfigSecretIdTitle),
Description: plugin.MakeTranslator(i18n.ConfigSecretIdDescription),
Required: true,
UIOptions: plugin.ConfigFieldUIOptions{
InputType: plugin.InputTypeText,
},
Value: s.Config.SecretID,
},
{
Name: "secret_key",
Type: plugin.ConfigTypeInput,
Title: plugin.MakeTranslator(i18n.ConfigSecretKeyTitle),
Description: plugin.MakeTranslator(i18n.ConfigSecretKeyDescription),
Required: true,
UIOptions: plugin.ConfigFieldUIOptions{
InputType: plugin.InputTypeText,
},
Value: s.Config.SecretKey,
},
{
Name: "visit_url_prefix",
Type: plugin.ConfigTypeInput,
Title: plugin.MakeTranslator(i18n.ConfigVisitUrlPrefixTitle),
Description: plugin.MakeTranslator(i18n.ConfigVisitUrlPrefixDescription),
Required: true,
UIOptions: plugin.ConfigFieldUIOptions{
InputType: plugin.InputTypeText,
},
Value: s.Config.VisitUrlPrefix,
},
{
Name: "acl",
Type: plugin.ConfigTypeSelect,
Title: plugin.MakeTranslator(i18n.ConfigACLTitle),
Description: plugin.MakeTranslator(i18n.ConfigACLDescription),
Required: true,
Options: []plugin.ConfigFieldOption{
{
Label: plugin.MakeTranslator(i18n.ConfigACLOptionsDefault),
Value: "default",
},
{
Label: plugin.MakeTranslator(i18n.ConfigACLOptionsPublicRead),
Value: "public-read",
},
},
Value: s.Config.ACL,
},
}
}
func (s *Storage) ConfigReceiver(config []byte) error {
c := &StorageConfig{}
_ = json.Unmarshal(config, c)
s.Config = c
return nil
}