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

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

commit 5ad1687325b471c33b01d402f921f8c12ef8d737
Author: kumfo <[email protected]>
AuthorDate: Mon Jul 8 18:10:18 2024 +0800

    feat(cdn):Feat #129, Closes #129. Support CDN with AWS S3
---
 cdn-s3/README.md           |  19 +++
 cdn-s3/go.mod              |  47 ++++++
 cdn-s3/i18n/en_US.yaml     |  70 ++++++++
 cdn-s3/i18n/translation.go |  32 ++++
 cdn-s3/i18n/zh_CN.yaml     |  70 ++++++++
 cdn-s3/s3.go               | 402 +++++++++++++++++++++++++++++++++++++++++++++
 cdn-s3/s3_client.go        |  67 ++++++++
 7 files changed, 707 insertions(+)

diff --git a/cdn-s3/README.md b/cdn-s3/README.md
new file mode 100644
index 0000000..7dc3f19
--- /dev/null
+++ b/cdn-s3/README.md
@@ -0,0 +1,19 @@
+# CDN With S3 Storage (preview)
+> This plugin can be used to store static files to AWS S3.
+
+## How to use
+
+### Build
+```bash
+./answer build --with github.com/answerdev/plugins/cdn-s3
+```
+
+### Configuration
+- `Endpoint` -  Endpoint of the AWS S3 storage
+- `Bucket Name` - Your bucket name
+- `Object Key Prefix` - Prefix of the object key like 'static/' that ending 
with '/'
+- `Access Key Id` - AccessKeyId of the S3
+- `Access Key Secret` - AccessKeySecret of the S3
+- `Access Token` - AccessToken of the S3
+- `Visit Url Prefix` - Prefix of access address for the static 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-s3/go.mod b/cdn-s3/go.mod
new file mode 100644
index 0000000..ff58617
--- /dev/null
+++ b/cdn-s3/go.mod
@@ -0,0 +1,47 @@
+module github.com/apache/incubator-answer-plugins/cdn-s3
+
+go 1.19
+
+require (
+       github.com/apache/incubator-answer v1.3.6
+       github.com/aws/aws-sdk-go v1.44.314
+)
+
+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/jmespath/go-jmespath v0.4.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
+       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-s3/i18n/en_US.yaml b/cdn-s3/i18n/en_US.yaml
new file mode 100644
index 0000000..17d5c0b
--- /dev/null
+++ b/cdn-s3/i18n/en_US.yaml
@@ -0,0 +1,70 @@
+plugin:
+  s3_cdn:
+    backend:
+      info:
+        name:
+          other: S3 storage CDN
+        description:
+          other: Upload files to S3 storage
+      config:
+        endpoint:
+          title:
+            other: Endpoint
+          description:
+            other: Endpoint of S3 storage
+        bucket_name:
+          title:
+            other: Bucket name
+          description:
+            other: Bucket name of S3 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 S3 storage
+        access_key_secret:
+          title:
+            other: AccessKeySecret
+          description:
+            other: AccessKeySecret of S3 storage
+        access_token:
+          title:
+            other: AccessToken
+          description:
+            other: AccessToken of S3 storage
+        visit_url_prefix:
+          title:
+            other: Access URL prefix
+          description:
+            other: prefix of the final access address of the static file, 
ending with '/' https://static.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
+        region:
+          title:
+            other: Region
+          description:
+            other: Region of S3 storage
+        disable_ssl:
+          title:
+            other: Disable SSL
+          description:
+            other: We recommend that you use SSL to access S3 storage. If you 
want to disable SSL, please check this option.
+      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-s3/i18n/translation.go b/cdn-s3/i18n/translation.go
new file mode 100644
index 0000000..8b71799
--- /dev/null
+++ b/cdn-s3/i18n/translation.go
@@ -0,0 +1,32 @@
+package i18n
+
+const (
+       InfoName        = "plugin.s3_cdn.backend.info.name"
+       InfoDescription = "plugin.s3_cdn.backend.info.description"
+
+       ConfigEndpointTitle              = 
"plugin.s3_cdn.backend.config.endpoint.title"
+       ConfigEndpointDescription        = 
"plugin.s3_cdn.backend.config.endpoint.description"
+       ConfigBucketNameTitle            = 
"plugin.s3_cdn.backend.config.bucket_name.title"
+       ConfigBucketNameDescription      = 
"plugin.s3_cdn.backend.config.bucket_name.description"
+       ConfigObjectKeyPrefixTitle       = 
"plugin.s3_cdn.backend.config.object_key_prefix.title"
+       ConfigObjectKeyPrefixDescription = 
"plugin.s3_cdn.backend.config.object_key_prefix.description"
+       ConfigAccessKeyIdTitle           = 
"plugin.s3_cdn.backend.config.access_key_id.title"
+       ConfigAccessKeyIdDescription     = 
"plugin.s3_cdn.backend.config.access_key_id.description"
+       ConfigAccessKeySecretTitle       = 
"plugin.s3_cdn.backend.config.access_key_secret.title"
+       ConfigAccessKeySecretDescription = 
"plugin.s3_cdn.backend.config.access_key_secret.description"
+       ConfigAccessTokenTitle           = 
"plugin.s3_cdn.backend.config.access_token.title"
+       ConfigAccessTokenDescription     = 
"plugin.s3_cdn.backend.config.access_token.description"
+       ConfigVisitUrlPrefixTitle        = 
"plugin.s3_cdn.backend.config.visit_url_prefix.title"
+       ConfigVisitUrlPrefixDescription  = 
"plugin.s3_cdn.backend.config.visit_url_prefix.description"
+       ConfigMaxFileSizeTitle           = 
"plugin.s3_cdn.backend.config.max_file_size.title"
+       ConfigMaxFileSizeDescription     = 
"plugin.s3_cdn.backend.config.max_file_size.description"
+       ConfigRegionTitle                = 
"plugin.s3_cdn.backend.config.region.title"
+       ConfigRegionDescription          = 
"plugin.s3_cdn.backend.config.region.description"
+       ConfigDisableSSLTitle            = 
"plugin.s3_cdn.backend.config.disable_ssl.title"
+       ConfigDisableSSLDescription      = 
"plugin.s3_cdn.backend.config.disable_ssl.description"
+
+       ErrFileNotFound        = "plugin.s3_cdn.backend.err.file_not_found"
+       ErrUnsupportedFileType = 
"plugin.s3_cdn.backend.err.unsupported_file_type"
+       ErrOverFileSizeLimit   = 
"plugin.s3_cdn.backend.err.over_file_size_limit"
+       ErrUploadFileFailed    = "plugin.s3_cdn.backend.err.upload_file_failed"
+)
diff --git a/cdn-s3/i18n/zh_CN.yaml b/cdn-s3/i18n/zh_CN.yaml
new file mode 100644
index 0000000..737e08b
--- /dev/null
+++ b/cdn-s3/i18n/zh_CN.yaml
@@ -0,0 +1,70 @@
+plugin:
+  s3_cdn:
+    backend:
+      info:
+        name:
+          other: S3 存储CDN
+        description:
+          other: 上传文件到S3存储
+      config:
+        endpoint:
+          title:
+            other: Endpoint
+          description:
+            other: S3存储的Endpoint
+        bucket_name:
+          title:
+            other: Bucket名称
+          description:
+            other: S3存储的Bucket名称
+        object_key_prefix:
+          title:
+            other: 对象Key前缀
+          description:
+            other: 对象键的前缀,如'answer/data/',以'/'结尾
+        access_key_id:
+          title:
+            other: AccessKeyID
+          description:
+            other: S3存储的AccessKeyID
+        access_key_secret:
+          title:
+            other: AccessKeySecret
+          description:
+            other: S3存储的AccessKeySecret
+        access_token:
+          title:
+            other: AccessToken
+          description:
+            other: 访问 S3 所需的 AccessToken
+        visit_url_prefix:
+          title:
+            other: 访问URL前缀
+          description:
+            other: 上传静态文件CDN最终访问地址的前缀,以 '/' 结尾 https://static.example.com/xxx/
+        max_file_size:
+          title:
+            other: 文件最大大小(MB)
+          description:
+            other: 限制上传文件的最大大小,单位MB,默认为10MB
+        region:
+          title:
+            other: 区域(Region)
+          description:
+            other: S3存储区域
+        disable_ssl:
+          title:
+            other: 禁用SSL
+          description:
+            other: 我们建议您使用SSL访问S3存储。如果您想禁用SSL,请选中此选项。
+      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
diff --git a/cdn-s3/s3.go b/cdn-s3/s3.go
new file mode 100644
index 0000000..656eb48
--- /dev/null
+++ b/cdn-s3/s3.go
@@ -0,0 +1,402 @@
+package s3
+
+import (
+       "crypto/rand"
+       "encoding/hex"
+       "encoding/json"
+       "fmt"
+       "github.com/apache/incubator-answer-plugins/cdn-s3/i18n"
+       "github.com/apache/incubator-answer/ui"
+       "github.com/segmentfault/pacman/log"
+       "io"
+       "os"
+       "path/filepath"
+       "strconv"
+       "strings"
+       "time"
+
+       "github.com/apache/incubator-answer/plugin"
+)
+
+var staticPath = os.Getenv("ANSWER_STATIC_PATH")
+
+const (
+       // 10MB
+       defaultMaxFileSize int64 = 10 * 1024 * 1024
+)
+
+type CDN struct {
+       Config *CDNConfig
+       Client *Client
+}
+
+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"`
+       AccessToken     string `json:"access_token"`
+       VisitUrlPrefix  string `json:"visit_url_prefix"`
+       MaxFileSize     string `json:"max_file_size"`
+       Region          string `json:"region"`
+       DisableSSL      bool   `json:"disable_ssl"`
+}
+
+func init() {
+       plugin.Register(&CDN{
+               Config: &CDNConfig{},
+       })
+}
+
+func (c *CDN) Info() plugin.Info {
+       return plugin.Info{
+               Name:        plugin.MakeTranslator(i18n.InfoName),
+               SlugName:    "s3_cdn",
+               Description: plugin.MakeTranslator(i18n.InfoDescription),
+               Author:      "answerdev",
+               Version:     "0.0.1",
+               Link:        
"https://github.com/answerdev/plugins/tree/main/cdn-s3";,
+       }
+}
+
+// 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
+                       }
+
+                       if ext == ".css" {
+                               c.Upload(filePath, c.rebuildReader(file, 
map[string]string{
+                                       "url(/static": "url(../../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
+                       }
+
+                       if ext == ".css" {
+                               c.Upload(filePath, c.rebuildReader(file, 
map[string]string{
+                                       "url(/static": "url(../../static",
+                               }), size)
+                               continue
+                       }
+               }
+
+               c.Upload(filePath, c.rebuildReader(file, nil), size)
+       }
+}
+
+func (c *CDN) rebuildReader(file io.Reader, replaceMap map[string]string) 
io.ReadSeeker {
+       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)
+
+       if replaceMap != nil {
+               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.ReadSeeker, size int64) {
+
+       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)
+
+       err := c.Client.PutObject(objectKey, 
strings.ToLower(filepath.Ext(filePath)), file)
+       if err != nil {
+               log.Error(plugin.MakeTranslator(i18n.ErrUploadFileFailed), err)
+               return
+       }
+       return
+}
+
+func (c *CDN) createObjectKey(filePath string) string {
+       return c.Config.ObjectKeyPrefix + filePath
+}
+
+func (c *CDN) randomObjectKey() string {
+       bytes := make([]byte, 4)
+       _, _ = rand.Read(bytes)
+       return fmt.Sprintf("%d", time.Now().UnixNano()) + 
hex.EncodeToString(bytes)
+}
+
+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:    false,
+                       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:    false,
+                       UIOptions: plugin.ConfigFieldUIOptions{
+                               InputType: plugin.InputTypeText,
+                       },
+                       Value: c.Config.AccessKeySecret,
+               },
+               {
+                       Name:        "access_token",
+                       Type:        plugin.ConfigTypeInput,
+                       Title:       
plugin.MakeTranslator(i18n.ConfigAccessTokenTitle),
+                       Description: 
plugin.MakeTranslator(i18n.ConfigAccessTokenDescription),
+                       Required:    false,
+                       UIOptions: plugin.ConfigFieldUIOptions{
+                               InputType: plugin.InputTypeText,
+                       },
+                       Value: c.Config.AccessToken,
+               },
+               {
+                       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,
+               },
+               {
+                       Name:        "region",
+                       Type:        plugin.ConfigTypeInput,
+                       Title:       
plugin.MakeTranslator(i18n.ConfigRegionTitle),
+                       Description: 
plugin.MakeTranslator(i18n.ConfigRegionDescription),
+                       Required:    true,
+                       UIOptions: plugin.ConfigFieldUIOptions{
+                               InputType: plugin.InputTypeText,
+                       },
+                       Value: c.Config.Region,
+               },
+               {
+                       Name:  "disable_ssl",
+                       Type:  plugin.ConfigTypeSwitch,
+                       Title: 
plugin.MakeTranslator(i18n.ConfigDisableSSLTitle),
+                       Value: c.Config.DisableSSL,
+                       UIOptions: plugin.ConfigFieldUIOptions{
+                               Label: 
plugin.MakeTranslator(i18n.ConfigDisableSSLDescription),
+                       },
+               },
+       }
+}
+
+func (c *CDN) ConfigReceiver(config []byte) error {
+       cfg := &CDNConfig{}
+       _ = json.Unmarshal(config, cfg)
+       c.Config = cfg
+       c.Client = NewS3Client(
+               c.Config.AccessKeyID,
+               c.Config.AccessKeySecret,
+               c.Config.AccessToken,
+               c.Config.Endpoint,
+               c.Config.Region,
+               c.Config.BucketName,
+               c.Config.DisableSSL,
+       )
+       go c.scanFiles()
+       return nil
+}
diff --git a/cdn-s3/s3_client.go b/cdn-s3/s3_client.go
new file mode 100644
index 0000000..aa29095
--- /dev/null
+++ b/cdn-s3/s3_client.go
@@ -0,0 +1,67 @@
+package s3
+
+import (
+       "fmt"
+       "github.com/aws/aws-sdk-go/aws"
+       "github.com/aws/aws-sdk-go/aws/credentials"
+       "github.com/aws/aws-sdk-go/aws/session"
+       "github.com/aws/aws-sdk-go/service/s3"
+       "io"
+       "strings"
+)
+
+type Client struct {
+       s3Config *aws.Config
+       bucket   string
+}
+
+func NewS3Client(id, secret, token, endpoint, region, bucket string, 
disableSSL bool) *Client {
+       s3Client := &Client{
+               s3Config: &aws.Config{
+                       Credentials:      credentials.NewStaticCredentials(id, 
secret, token),
+                       Endpoint:         aws.String(endpoint),
+                       Region:           aws.String(region),
+                       DisableSSL:       aws.Bool(disableSSL),
+                       S3ForcePathStyle: aws.Bool(true),
+               },
+               bucket: bucket,
+       }
+       return s3Client
+}
+
+func (s *Client) PutObject(key, ext string, file io.ReadSeeker) (err error) {
+       newSession, err := session.NewSession(s.s3Config)
+       if err != nil {
+               return fmt.Errorf("failed to create session, %s", err.Error())
+       }
+
+       extType := strings.TrimPrefix(ext, ".")
+       contentType := ""
+       switch extType {
+       case "jpg", "jpeg", "png":
+               contentType = "image/" + extType
+       case "svg":
+               contentType = "image/svg+xml"
+       case "js":
+               contentType = "application/javascript"
+       case "css":
+               contentType = "text/css"
+       case "map":
+               contentType = "application/json"
+       case "woff":
+               contentType = "application/font-woff"
+       case "woff2":
+               contentType = "application/font-woff2"
+       }
+
+       _, err = s3.New(newSession).PutObject(&s3.PutObjectInput{
+               Body:        file,
+               Bucket:      aws.String(s.bucket),
+               Key:         aws.String(key),
+               ContentType: aws.String(contentType),
+       })
+       if err != nil {
+               return fmt.Errorf("failed to put object, %s", err.Error())
+       }
+       return nil
+}

Reply via email to