storage-aliyunoss/aliyunoss.go (236 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 aliyunoss import ( "crypto/rand" "embed" "encoding/hex" "encoding/json" "fmt" "path/filepath" "strings" "time" "github.com/apache/answer/pkg/checker" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/apache/answer-plugins/storage-aliyunoss/i18n" "github.com/apache/answer-plugins/util" "github.com/apache/answer/plugin" ) //go:embed info.yaml var Info embed.FS type Storage struct { Config *StorageConfig } type StorageConfig struct { Endpoint string `json:"endpoint"` BucketName string `json:"bucket_name"` ObjectKeyPrefix string `json:"object_key_prefix"` AccessKeyID string `json:"access_key_id"` AccessKeySecret string `json:"access_key_secret"` 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{} client, err := oss.New(s.Config.Endpoint, s.Config.AccessKeyID, s.Config.AccessKeySecret) if err != nil { resp.OriginalError = err resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrMisStorageConfig) return resp } bucket, err := client.Bucket(s.Config.BucketName) if err != nil { resp.OriginalError = fmt.Errorf("get 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 } open, err := file.Open() if err != nil { resp.OriginalError = fmt.Errorf("get file failed: %v", err) resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrFileNotFound) return resp } defer open.Close() objectKey := s.createObjectKey(file.Filename, condition.Source) request := &oss.PutObjectRequest{ ObjectKey: objectKey, Reader: open, } var options []oss.Option if s.Config.ACL == string(oss.ACLPublicRead) { options = append(options, oss.ObjectACL(oss.ACLPublicRead)) } respBody, err := bucket.DoPutObject(request, options) if err != nil { resp.OriginalError = fmt.Errorf("upload file failed: %v", err) resp.DisplayErrorMsg = plugin.MakeTranslator(i18n.ErrUploadFileFailed) return resp } defer respBody.Close() resp.FullURL = s.Config.VisitUrlPrefix + objectKey return resp } 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) 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) ConfigFields() []plugin.ConfigField { return []plugin.ConfigField{ { Name: "endpoint", Type: plugin.ConfigTypeInput, Title: plugin.MakeTranslator(i18n.ConfigEndpointTitle), Description: plugin.MakeTranslator(i18n.ConfigEndpointDescription), Required: true, UIOptions: plugin.ConfigFieldUIOptions{ InputType: plugin.InputTypeText, }, Value: s.Config.Endpoint, }, { 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: "access_key_id", Type: plugin.ConfigTypeInput, Title: plugin.MakeTranslator(i18n.ConfigAccessKeyIdTitle), Description: plugin.MakeTranslator(i18n.ConfigAccessKeyIdDescription), Required: true, UIOptions: plugin.ConfigFieldUIOptions{ InputType: plugin.InputTypeText, }, Value: s.Config.AccessKeyID, }, { Name: "access_key_secret", Type: plugin.ConfigTypeInput, Title: plugin.MakeTranslator(i18n.ConfigAccessKeySecretTitle), Description: plugin.MakeTranslator(i18n.ConfigAccessKeySecretDescription), Required: true, UIOptions: plugin.ConfigFieldUIOptions{ InputType: plugin.InputTypeText, }, Value: s.Config.AccessKeySecret, }, { 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: string(oss.ACLDefault), }, { Label: plugin.MakeTranslator(i18n.ConfigACLOptionsPublicRead), Value: string(oss.ACLPublicRead), }, }, Value: s.Config.ACL, }, } } func (s *Storage) ConfigReceiver(config []byte) error { c := &StorageConfig{} _ = json.Unmarshal(config, c) s.Config = c return nil }