| /* |
| * 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 |
| } |