Files
waf-platform/EdgeNode/internal/oss/provider_amzon_s3.go
2026-02-04 20:27:13 +08:00

309 lines
9.0 KiB
Go

// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package oss
import (
"bytes"
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ossconfigs"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
"strings"
"time"
)
type AmazonS3Provider struct {
bucketName string
client *s3.S3
}
func NewAmazonS3Provider() ProviderInterface {
return &AmazonS3Provider{}
}
func (this *AmazonS3Provider) Init(options ossconfigs.OSSOptions, bucketName string) error {
realOptions, ok := options.(*ossconfigs.AmazonS3ProviderOptions)
if !ok {
return errors.New("invalid options for 'AmazonS3Provider'")
}
this.bucketName = bucketName
var config = &aws.Config{
Credentials: credentials.NewCredentials(NewAmazonS3CredentialProvider(realOptions.AccessKeyId, realOptions.AccessKeySecret)),
Region: aws.String(realOptions.Region),
}
if len(realOptions.Endpoint) > 0 {
config.Endpoint = aws.String(realOptions.Endpoint)
}
switch realOptions.BucketAddressStyle {
case ossconfigs.AmazonS3BucketAddressStylePath, "":
config.S3ForcePathStyle = aws.Bool(true)
case ossconfigs.AmazonS3BucketAddressStyleDomain:
// do nothing
}
sess, err := session.NewSession(config)
if err != nil {
return err
}
this.client = s3.New(sess)
return nil
}
func (this *AmazonS3Provider) Head(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
var input = &s3.HeadObjectInput{
Bucket: aws.String(this.bucketName),
Key: aws.String(key),
}
resp, respErr := this.client.HeadObject(input)
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
var statusCode = http.StatusOK
httpResponse = &http.Response{
StatusCode: statusCode,
ContentLength: aws.Int64Value(resp.ContentLength),
Body: io.NopCloser(bytes.NewReader(nil)),
}
if httpResponse.Header == nil {
httpResponse.Header = http.Header{}
}
// response headers
// TODO 可以设置是否显示x-amz-*报头
// TODO 考虑是否将 resp.Metadata 加入到Header
// TODO 考虑将 *bool, *int64 等类型也加入到Header
for _, header := range []*AmazonS3Header{
{"Accept-Ranges", resp.AcceptRanges},
{"Cache-Control", resp.CacheControl},
{"x-amz-checksum-crc32", resp.ChecksumCRC32},
{"x-amz-checksum-crc32c", resp.ChecksumCRC32C},
{"x-amz-checksum-sha1", resp.ChecksumSHA1},
{"x-amz-checksum-sha256", resp.ChecksumSHA256},
{"Content-Disposition", resp.ContentDisposition},
{"Content-Encoding", resp.ContentEncoding},
{"Content-Language", resp.ContentLanguage},
{"Content-Length", resp.ContentLength},
{"Content-Type", resp.ContentType},
{"ETag", resp.ETag},
{"x-amz-expiration", resp.Expiration},
{"Expires", resp.Expires},
{"x-amz-object-lock-legal-hold", resp.ObjectLockLegalHoldStatus},
{"x-amz-object-lock-mode", resp.ObjectLockMode},
{"Last-Modified", resp.LastModified},
{"x-amz-object-lock-retain-until-date", resp.ObjectLockRetainUntilDate},
{"x-amz-replication-status", resp.ReplicationStatus},
{"x-amz-request-charged", resp.RequestCharged},
{"x-amz-restore", resp.Restore},
{"x-amz-server-side-encryption-customer-algorithm", resp.SSECustomerAlgorithm},
{"x-amz-server-side-encryption-customer-key-MD5", resp.SSECustomerKeyMD5},
{"x-amz-server-side-encryption-aws-kms-key-id", resp.SSEKMSKeyId},
{"x-amz-server-side-encryption", resp.ServerSideEncryption},
{"x-amz-storage-class", resp.StorageClass},
{"x-amz-version-id", resp.VersionId},
{"x-amz-website-redirect-location", resp.WebsiteRedirectLocation},
{"x-amz-mp-parts-count", resp.PartsCount},
} {
if header.Value != nil {
switch value := header.Value.(type) {
case *string:
var stringValue = aws.StringValue(value)
if len(stringValue) > 0 {
httpResponse.Header[header.Name] = []string{stringValue}
}
case *int64:
var int64Value = aws.Int64Value(value)
if int64Value >= 0 {
if !strings.HasPrefix(header.Name, "x-") || int64Value > 0 {
httpResponse.Header[header.Name] = []string{types.String(int64Value)}
}
}
case *time.Time:
var t = aws.TimeValue(value)
if t.Year() > 1000 {
httpResponse.Header[header.Name] = []string{t.Format("Mon, 02 Jan 2006 15:04:05") + " GMT"}
}
}
}
}
return
}
func (this *AmazonS3Provider) Get(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
return this.GetRange(key, "")
}
func (this *AmazonS3Provider) GetRange(key string, bytesRange string) (httpResponse *http.Response, nativeErrCode string, err error) {
var input = &s3.GetObjectInput{
Bucket: aws.String(this.bucketName),
Key: aws.String(key),
}
if len(bytesRange) > 0 {
input.Range = aws.String(bytesRange)
}
resp, respErr := this.client.GetObject(input)
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
var statusCode = http.StatusOK
if resp.ContentRange != nil && len(aws.StringValue(resp.ContentRange)) > 0 {
statusCode = http.StatusPartialContent
}
httpResponse = &http.Response{
StatusCode: statusCode,
ContentLength: aws.Int64Value(resp.ContentLength),
Body: resp.Body,
}
if httpResponse.Header == nil {
httpResponse.Header = http.Header{}
}
// response headers
// TODO 可以设置是否显示x-amz-*报头
// TODO 考虑是否将 resp.Metadata 加入到Header
// TODO 考虑将 *bool, *int64 等类型也加入到Header
for _, header := range []*AmazonS3Header{
{"Accept-Ranges", resp.AcceptRanges},
{"Cache-Control", resp.CacheControl},
{"x-amz-checksum-crc32", resp.ChecksumCRC32},
{"x-amz-checksum-crc32c", resp.ChecksumCRC32C},
{"x-amz-checksum-sha1", resp.ChecksumSHA1},
{"x-amz-checksum-sha256", resp.ChecksumSHA256},
{"Content-Disposition", resp.ContentDisposition},
{"Content-Encoding", resp.ContentEncoding},
{"Content-Language", resp.ContentLanguage},
{"Content-Length", resp.ContentLength},
{"Content-Range", resp.ContentRange},
{"Content-Type", resp.ContentType},
{"ETag", resp.ETag},
{"x-amz-expiration", resp.Expiration},
{"Expires", resp.Expires},
{"x-amz-object-lock-legal-hold", resp.ObjectLockLegalHoldStatus},
{"x-amz-object-lock-mode", resp.ObjectLockMode},
{"Last-Modified", resp.LastModified},
{"x-amz-object-lock-retain-until-date", resp.ObjectLockRetainUntilDate},
{"x-amz-replication-status", resp.ReplicationStatus},
{"x-amz-request-charged", resp.RequestCharged},
{"x-amz-restore", resp.Restore},
{"x-amz-server-side-encryption-customer-algorithm", resp.SSECustomerAlgorithm},
{"x-amz-server-side-encryption-customer-key-MD5", resp.SSECustomerKeyMD5},
{"x-amz-server-side-encryption-aws-kms-key-id", resp.SSEKMSKeyId},
{"x-amz-server-side-encryption", resp.ServerSideEncryption},
{"x-amz-storage-class", resp.StorageClass},
{"x-amz-version-id", resp.VersionId},
{"x-amz-website-redirect-location", resp.WebsiteRedirectLocation},
{"x-amz-mp-parts-count", resp.PartsCount},
{"x-amz-tagging-count", resp.TagCount},
} {
if header.Value != nil {
switch value := header.Value.(type) {
case *string:
var stringValue = aws.StringValue(value)
if len(stringValue) > 0 {
httpResponse.Header[header.Name] = []string{stringValue}
}
case *int64:
var int64Value = aws.Int64Value(value)
if int64Value >= 0 {
if !strings.HasPrefix(header.Name, "x-") || int64Value > 0 {
httpResponse.Header[header.Name] = []string{types.String(int64Value)}
}
}
case *time.Time:
var t = aws.TimeValue(value)
if t.Year() > 1000 {
httpResponse.Header[header.Name] = []string{t.Format("Mon, 02 Jan 2006 15:04:05") + " GMT"}
}
}
}
}
return
}
func (this *AmazonS3Provider) parseErr(err error) (errCode string, resultErr error) {
if err == nil {
return
}
resultErr = err
respErr, ok := err.(awserr.Error)
if ok {
errCode = respErr.Code()
}
// 特殊错误
if errCode == s3.ErrCodeNoSuchBucket {
resultErr = errNoBucket
return
}
if errCode == s3.ErrCodeNoSuchKey {
resultErr = errNotFound
return
}
if errCode == request.ErrCodeResponseTimeout {
resultErr = errRequestTimeout
return
}
return
}
// AmazonS3CredentialProvider 认证服务
type AmazonS3CredentialProvider struct {
accessKeyId string
secretAccessKey string
}
func NewAmazonS3CredentialProvider(accessKeyId string, secretAccessKey string) *AmazonS3CredentialProvider {
return &AmazonS3CredentialProvider{
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
}
}
func (this *AmazonS3CredentialProvider) Retrieve() (credentials.Value, error) {
return credentials.Value{
AccessKeyID: this.accessKeyId,
SecretAccessKey: this.secretAccessKey,
SessionToken: "",
ProviderName: "",
}, nil
}
func (this *AmazonS3CredentialProvider) IsExpired() bool {
return false
}
// AmazonS3Header Header组合
type AmazonS3Header struct {
Name string
Value any
}