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