309 lines
9.0 KiB
Go
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
|
|
}
|