You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by ku...@apache.org on 2024/07/08 03:23:24 UTC

(incubator-answer-plugins) 01/01: feat(cdn):Feat #128, Closes #128. Support CDN with Aliyun OSS

This is an automated email from the ASF dual-hosted git repository.

kumfo pushed a commit to branch feat/cdn/oss
in repository https://gitbox.apache.org/repos/asf/incubator-answer-plugins.git

commit fdbd186f2355995ce622b1eb14c973552aff3688
Author: kumfo <ku...@sifou.com>
AuthorDate: Mon Jul 8 11:23:13 2024 +0800

    feat(cdn):Feat #128, Closes #128. Support CDN with Aliyun OSS
---
 cdn-aliyunoss/README.md           |  18 ++
 cdn-aliyunoss/aliyunoss.go        | 383 ++++++++++++++++++++++++++++++++++++++
 cdn-aliyunoss/go.mod              |  47 +++++
 cdn-aliyunoss/i18n/en_US.yaml     |  72 +++++++
 cdn-aliyunoss/i18n/translation.go |  45 +++++
 cdn-aliyunoss/i18n/zh_CN.yaml     |  72 +++++++
 6 files changed, 637 insertions(+)

diff --git a/cdn-aliyunoss/README.md b/cdn-aliyunoss/README.md
new file mode 100644
index 0000000..ceabbfe
--- /dev/null
+++ b/cdn-aliyunoss/README.md
@@ -0,0 +1,18 @@
+# CDN With Aliyun OSS Storage (preview)
+> This plugin can be used to store static files to Aliyun OSS.
+
+## How to use
+
+### Build
+```bash
+./answer build --with github.com/apache/incubator-answer-plugins/cdn-aliyunoss
+```
+
+### Configuration
+- `Endpoint` -  Endpoint of AliCloud OSS storage, such as oss-cn-hangzhou.aliyuncs.com
+- `Bucket Name` - Your bucket name
+- `Object Key Prefix` - Prefix of the object key like 'static/' that ending with '/'
+- `Access Key Id` - AccessKeyID of the AliCloud OSS storage
+- `Access Key Secret` - AccessKeySecret of the AliCloud OSS storage
+- `Visit Url Prefix` - Prefix of access address for the CDN file, ending with '/' such as https://static.example.com/xxx/
+- `Max File Size` - Max file size in MB, default is 10MB
\ No newline at end of file
diff --git a/cdn-aliyunoss/aliyunoss.go b/cdn-aliyunoss/aliyunoss.go
new file mode 100644
index 0000000..9c741b8
--- /dev/null
+++ b/cdn-aliyunoss/aliyunoss.go
@@ -0,0 +1,383 @@
+/*
+ * 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 (
+	"encoding/json"
+	"github.com/aliyun/aliyun-oss-go-sdk/oss"
+	"github.com/apache/incubator-answer-plugins/cdn-aliyunoss/i18n"
+	"github.com/apache/incubator-answer/plugin"
+	"github.com/apache/incubator-answer/ui"
+	"github.com/segmentfault/pacman/log"
+	"io"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+)
+
+var staticPath = os.Getenv("ANSWER_STATIC_PATH")
+
+const (
+	// 10MB
+	defaultMaxFileSize int64 = 10 * 1024 * 1024
+)
+
+type CDN struct {
+	Config *CDNConfig
+}
+
+type CDNConfig 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"`
+	MaxFileSize     string `json:"max_file_size"`
+}
+
+type CustomFile struct {
+	file      io.Reader
+	cdnPrefix string
+	old       string
+}
+
+func (f *CustomFile) Read(p []byte) (n int, err error) {
+	c := make([]byte, len(p))
+	n, err = f.file.Read(c)
+	if f.old != "" {
+		c = []byte(strings.ReplaceAll(string(c), f.old, "\""+f.cdnPrefix+"/static"))
+	}
+
+	n = copy(p, c)
+	return
+}
+
+func (f *CustomFile) Close() error { return nil }
+
+func init() {
+	plugin.Register(&CDN{
+		Config: &CDNConfig{},
+	})
+}
+
+func (c *CDN) Info() plugin.Info {
+	return plugin.Info{
+		Name:        plugin.MakeTranslator(i18n.InfoName),
+		SlugName:    "aliyunoss_cdn",
+		Description: plugin.MakeTranslator(i18n.InfoDescription),
+		Author:      "answerdev",
+		Version:     "1.0.0",
+		Link:        "https://github.com/apache/incubator-answer-plugins/tree/main/cdn-aliyunoss",
+	}
+}
+
+// GetStaticPrefix get static prefix
+func (c *CDN) GetStaticPrefix() string {
+	return c.Config.VisitUrlPrefix + c.Config.ObjectKeyPrefix
+}
+
+// scanFiles scan all the static files in the build directory
+func (c *CDN) scanFiles() {
+	if staticPath == "" {
+		c.scanEmbedFiles("build")
+		log.Info("complete scan embed files")
+		return
+	}
+	c.scanStaticPathFiles(staticPath)
+	log.Info("complete scan static path files")
+}
+
+// scanStaticPathFiles scan static path files
+func (c *CDN) scanStaticPathFiles(fileName string) {
+	// scan static path files
+	entry, err := os.ReadDir(fileName)
+	if err != nil {
+		log.Error("read static dir failed: %v", err)
+		return
+	}
+	for _, info := range entry {
+		if info.IsDir() {
+			c.scanStaticPathFiles(filepath.Join(fileName, info.Name()))
+			continue
+		}
+
+		filePath := filepath.Join(fileName, info.Name())
+		fi, _ := info.Info()
+		size := fi.Size()
+		file, err := os.Open(filePath)
+		if err != nil {
+			log.Error("open file failed: %v", err)
+			continue
+		}
+
+		suffix := staticPath[:1]
+		if suffix != "/" {
+			suffix = ""
+		}
+		filePath = strings.TrimPrefix(filePath, staticPath+suffix)
+
+		// rebuild custom io.Reader
+		ns := strings.Split(info.Name(), ".")
+		if info.Name() == "asset-manifest.json" {
+			c.Upload(filePath, c.rebuildReader(file, map[string]string{
+				"\"/static": "",
+			}), size)
+			continue
+		}
+		if ns[0] == "main" {
+			ext := strings.ToLower(filepath.Ext(filePath))
+			if ext == ".js" || ext == ".map" {
+				c.Upload(filePath, c.rebuildReader(file, map[string]string{
+					"\"static": "",
+					"=\"/\",":  "=\"\",",
+				}), size)
+			}
+			continue
+		}
+
+		c.Upload(filePath, file, size)
+	}
+}
+
+func (c *CDN) scanEmbedFiles(fileName string) {
+	entry, err := ui.Build.ReadDir(fileName)
+	if err != nil {
+		log.Error("read static dir failed: %v", err)
+		return
+	}
+	for _, info := range entry {
+		if info.IsDir() {
+			c.scanEmbedFiles(filepath.Join(fileName, info.Name()))
+			continue
+		}
+
+		filePath := filepath.Join(fileName, info.Name())
+		fi, _ := info.Info()
+		size := fi.Size()
+		file, err := ui.Build.Open(filePath)
+		defer file.Close()
+		if err != nil {
+			log.Error("open file failed: %v", err)
+			continue
+		}
+
+		filePath = strings.TrimPrefix(filePath, "build/")
+
+		// rebuild custom io.Reader
+		ns := strings.Split(info.Name(), ".")
+		if info.Name() == "asset-manifest.json" {
+			c.Upload(filePath, c.rebuildReader(file, map[string]string{
+				"\"/static": "",
+			}), size)
+			continue
+		}
+		if ns[0] == "main" {
+			ext := strings.ToLower(filepath.Ext(filePath))
+			if ext == ".js" || ext == ".map" {
+				c.Upload(filePath, c.rebuildReader(file, map[string]string{
+					"\"static": "",
+					"=\"/\",":  "=\"\",",
+				}), size)
+			}
+			continue
+		}
+
+		c.Upload(filePath, file, size)
+	}
+}
+
+func (c *CDN) rebuildReader(file io.Reader, replaceMap map[string]string) io.Reader {
+	var (
+		bufr = make([]byte, 0)
+		res  string
+	)
+
+	for {
+		buf := make([]byte, 1024)
+		n, err := file.Read(buf)
+		if err != nil {
+			break
+		}
+		bufr = append(bufr, buf[:n]...)
+	}
+
+	res = string(bufr)
+
+	for oldStr, newStr := range replaceMap {
+		if oldStr != "" {
+			if newStr == "" {
+				newStr = "\"" + c.GetStaticPrefix() + "/static"
+			}
+			res = strings.ReplaceAll(res, oldStr, newStr)
+		}
+	}
+	return strings.NewReader(res)
+}
+
+func (c *CDN) Upload(filePath string, file io.Reader, size int64) {
+	client, err := oss.New(c.Config.Endpoint, c.Config.AccessKeyID, c.Config.AccessKeySecret)
+	if err != nil {
+		log.Error(plugin.MakeTranslator(i18n.ErrMisStorageConfig), err)
+		return
+	}
+	bucket, err := client.Bucket(c.Config.BucketName)
+	if err != nil {
+		log.Error(plugin.MakeTranslator(i18n.ErrMisStorageConfig), err)
+		return
+	}
+
+	if !c.CheckFileType(filePath) {
+		log.Error(plugin.MakeTranslator(i18n.ErrUnsupportedFileType), filePath)
+		return
+	}
+
+	if size > c.maxFileSizeLimit() {
+		log.Error(plugin.MakeTranslator(i18n.ErrOverFileSizeLimit))
+		return
+	}
+
+	objectKey := c.createObjectKey(filePath)
+	request := &oss.PutObjectRequest{
+		ObjectKey: objectKey,
+		Reader:    file,
+	}
+	respBody, err := bucket.DoPutObject(request, nil)
+	if err != nil {
+		log.Error(plugin.MakeTranslator(i18n.ErrUploadFileFailed), err)
+		return
+	}
+	defer respBody.Close()
+}
+
+func (c *CDN) createObjectKey(filePath string) string {
+	return c.Config.ObjectKeyPrefix + filePath
+}
+
+func (c *CDN) CheckFileType(filePath string) bool {
+	ext := strings.ToLower(filepath.Ext(filePath))
+	if _, ok := plugin.DefaultCDNFileType[ext]; ok {
+		return true
+	}
+	return false
+}
+
+func (c *CDN) maxFileSizeLimit() int64 {
+	if len(c.Config.MaxFileSize) == 0 {
+		return defaultMaxFileSize
+	}
+	limit, _ := strconv.Atoi(c.Config.MaxFileSize)
+	if limit <= 0 {
+		return defaultMaxFileSize
+	}
+	return int64(limit) * 1024 * 1024
+}
+
+func (c *CDN) 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: c.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: c.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: c.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: c.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: c.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: c.Config.VisitUrlPrefix,
+		},
+		{
+			Name:        "max_file_size",
+			Type:        plugin.ConfigTypeInput,
+			Title:       plugin.MakeTranslator(i18n.ConfigMaxFileSizeTitle),
+			Description: plugin.MakeTranslator(i18n.ConfigMaxFileSizeDescription),
+			Required:    false,
+			UIOptions: plugin.ConfigFieldUIOptions{
+				InputType: plugin.InputTypeNumber,
+			},
+			Value: c.Config.MaxFileSize,
+		},
+	}
+}
+
+func (c *CDN) ConfigReceiver(config []byte) error {
+	cfg := &CDNConfig{}
+	_ = json.Unmarshal(config, cfg)
+	c.Config = cfg
+
+	go c.scanFiles()
+	return nil
+}
diff --git a/cdn-aliyunoss/go.mod b/cdn-aliyunoss/go.mod
new file mode 100644
index 0000000..140928c
--- /dev/null
+++ b/cdn-aliyunoss/go.mod
@@ -0,0 +1,47 @@
+module github.com/apache/incubator-answer-plugins/cdn-aliyunoss
+
+go 1.19
+
+require (
+	github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
+	github.com/apache/incubator-answer v1.3.6
+)
+
+require (
+	github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect
+	github.com/aymerick/douceur v0.2.0 // indirect
+	github.com/bytedance/sonic v1.9.1 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/gin-gonic/gin v1.9.1 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-playground/validator/v10 v10.14.0 // indirect
+	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/google/wire v0.5.0 // indirect
+	github.com/gorilla/css v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+	github.com/kr/text v0.2.0 // indirect
+	github.com/leodido/go-urn v1.2.4 // indirect
+	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/microcosm-cc/bluemonday v1.0.21 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+	github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f // indirect
+	github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230516093754-b76aef1c1150 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.11 // indirect
+	golang.org/x/arch v0.3.0 // indirect
+	golang.org/x/crypto v0.21.0 // indirect
+	golang.org/x/net v0.21.0 // indirect
+	golang.org/x/sys v0.18.0 // indirect
+	golang.org/x/text v0.14.0 // indirect
+	golang.org/x/time v0.5.0 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	sigs.k8s.io/yaml v1.3.0 // indirect
+)
diff --git a/cdn-aliyunoss/i18n/en_US.yaml b/cdn-aliyunoss/i18n/en_US.yaml
new file mode 100644
index 0000000..2aa7e27
--- /dev/null
+++ b/cdn-aliyunoss/i18n/en_US.yaml
@@ -0,0 +1,72 @@
+# 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.
+
+plugin:
+  aliyunoss_storage:
+    backend:
+      info:
+        name:
+          other: Aliyun CDN OSS storage
+        description:
+          other: Upload files to AliCloud CDN OSS storage
+      config:
+        endpoint:
+          title:
+            other: Endpoint
+          description:
+            other: Endpoint of AliCloud OSS storage
+        bucket_name:
+          title:
+            other: Bucket name
+          description:
+            other: Bucket name of AliCloud OSS storage
+        object_key_prefix:
+          title:
+            other: Object Key prefix
+          description:
+            other: prefix of the object key like 'answer/data/' that ending with '/'
+        access_key_id:
+          title:
+            other: AccessKeyID
+          description:
+            other: AccessKeyID of the AliCloud OSS storage
+        access_key_secret:
+          title:
+            other: AccessKeySecret
+          description:
+            other: AccessKeySecret of AliCloud OSS storage
+        visit_url_prefix:
+          title:
+            other: Access URL prefix
+          description:
+            other: prefix of the final access address of the CDN, ending with '/' https://example.com/xxx/
+        max_file_size:
+          title:
+            other: Maximum file size(MB)
+          description:
+            other: Limit the maximum size of uploaded files, in MB, default is 10MB
+      err:
+        mis_storage_config:
+          other: Wrong storage configuration causes upload failure.
+        file_not_found:
+          other: File not found.
+        unsupported_file_type:
+          other: Unsupported file type.
+        over_file_size_limit:
+          other: File size limit exceeded.
+        upload_file_failed:
+          other: Failed to upload a file.
\ No newline at end of file
diff --git a/cdn-aliyunoss/i18n/translation.go b/cdn-aliyunoss/i18n/translation.go
new file mode 100644
index 0000000..773fe17
--- /dev/null
+++ b/cdn-aliyunoss/i18n/translation.go
@@ -0,0 +1,45 @@
+/*
+ * 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 i18n
+
+const (
+	InfoName        = "plugin.aliyunoss_cdn.backend.info.name"
+	InfoDescription = "plugin.aliyunoss_cdn.backend.info.description"
+
+	ConfigEndpointTitle              = "plugin.aliyunoss_cdn.backend.config.endpoint.title"
+	ConfigEndpointDescription        = "plugin.aliyunoss_cdn.backend.config.endpoint.description"
+	ConfigBucketNameTitle            = "plugin.aliyunoss_cdn.backend.config.bucket_name.title"
+	ConfigBucketNameDescription      = "plugin.aliyunoss_cdn.backend.config.bucket_name.description"
+	ConfigObjectKeyPrefixTitle       = "plugin.aliyunoss_cdn.backend.config.object_key_prefix.title"
+	ConfigObjectKeyPrefixDescription = "plugin.aliyunoss_cdn.backend.config.object_key_prefix.description"
+	ConfigAccessKeyIdTitle           = "plugin.aliyunoss_cdn.backend.config.access_key_id.title"
+	ConfigAccessKeyIdDescription     = "plugin.aliyunoss_cdn.backend.config.access_key_id.description"
+	ConfigAccessKeySecretTitle       = "plugin.aliyunoss_cdn.backend.config.access_key_secret.title"
+	ConfigAccessKeySecretDescription = "plugin.aliyunoss_cdn.backend.config.access_key_secret.description"
+	ConfigVisitUrlPrefixTitle        = "plugin.aliyunoss_cdn.backend.config.visit_url_prefix.title"
+	ConfigVisitUrlPrefixDescription  = "plugin.aliyunoss_cdn.backend.config.visit_url_prefix.description"
+	ConfigMaxFileSizeTitle           = "plugin.aliyunoss_cdn.backend.config.max_file_size.title"
+	ConfigMaxFileSizeDescription     = "plugin.aliyunoss_cdn.backend.config.max_file_size.description"
+
+	ErrMisStorageConfig    = "plugin.aliyunoss_cdn.backend.err.mis_storage_config"
+	ErrUnsupportedFileType = "plugin.aliyunoss_cdn.backend.err.unsupported_file_type"
+	ErrOverFileSizeLimit   = "plugin.aliyunoss_cdn.backend.err.over_file_size_limit"
+	ErrUploadFileFailed    = "plugin.aliyunoss_cdn.backend.err.upload_file_failed"
+)
diff --git a/cdn-aliyunoss/i18n/zh_CN.yaml b/cdn-aliyunoss/i18n/zh_CN.yaml
new file mode 100644
index 0000000..7044a64
--- /dev/null
+++ b/cdn-aliyunoss/i18n/zh_CN.yaml
@@ -0,0 +1,72 @@
+# 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.
+
+plugin:
+  aliyunoss_storage:
+    backend:
+      info:
+        name:
+          other: 阿里云CDN OSS
+        description:
+          other: 上传文件到阿里云CDN OSS
+      config:
+        endpoint:
+          title:
+            other: Endpoint
+          description:
+            other: 阿里云OSS存储的Endpoint
+        bucket_name:
+          title:
+            other: Bucket名称
+          description:
+            other: 阿里云OSS存储的Bucket名称
+        object_key_prefix:
+          title:
+            other: 对象Key前缀
+          description:
+            other: 对象键的前缀,如'answer/data/',以'/'结尾
+        access_key_id:
+          title:
+            other: AccessKeyID
+          description:
+            other: 阿里云OSS存储的AccessKeyID
+        access_key_secret:
+          title:
+            other: AccessKeySecret
+          description:
+            other: 阿里云OSS存储的AccessKeySecret
+        visit_url_prefix:
+          title:
+            other: 访问URL前缀
+          description:
+            other: CDN最终访问地址的前缀,以 '/' 结尾 https://example.com/xxx/
+        max_file_size:
+          title:
+            other: 最大文件大小(MB)
+          description:
+            other: 限制上传文件的最大大小,单位为MB,默认为 10MB
+      err:
+        mis_storage_config:
+          other: 错误的存储配置导致上传失败
+        file_not_found:
+          other: 文件未找到
+        unsupported_file_type:
+          other: 不支持的文件类型
+        over_file_size_limit:
+          other: 超过文件大小限制
+        upload_file_failed:
+          other: 上传文件失败
\ No newline at end of file