This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

View File

@@ -0,0 +1 @@
注意事项在开发OSS Provider时注意要确保 `resp.ContentLength` 必须大于0如果文件内容不为空的话

View File

@@ -0,0 +1,18 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package oss
import "errors"
var errNotFound = errors.New("file not found")
var errNoBucket = errors.New("no bucket")
var errRequestTimeout = errors.New("request timeout")
func IsNotFound(err error) bool {
return err == errNotFound || err == errNoBucket
}
func IsTimeout(err error) bool {
return err == errRequestTimeout
}

View File

@@ -0,0 +1,216 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package oss
import (
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ossconfigs"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/TeaOSLab/EdgeNode/internal/utils/goman"
"net/http"
"sync"
"time"
)
var SharedManager = NewManager()
func init() {
if !teaconst.IsMain {
return
}
var ticker = time.NewTicker(24 * time.Hour)
goman.New(func() {
for range ticker.C {
SharedManager.GC()
}
})
}
type Manager struct {
providerMap map[string]*Provider // unique id => *Provider
locker sync.RWMutex
}
func NewManager() *Manager {
return &Manager{
providerMap: map[string]*Provider{},
}
}
func (this *Manager) FindProviderWithConfig(req *http.Request, host string, ossConfig *ossconfigs.OSSConfig) (provider *Provider, objectBucketName string, objectKey string, err error) {
if ossConfig == nil {
return nil, "", "", errors.New("provider 'config' should not be nil")
}
var originOptions = ossConfig.Options
if originOptions == nil {
return nil, "", "", errors.New("provider 'options' should not be nil")
}
options, ok := originOptions.(ossconfigs.OSSOptions)
if !ok {
return nil, "", "", errors.New("provider 'options' should implement 'OSSOptions' interface")
}
bucketName, key, uniqueId := ossConfig.ParseRequest(req, host)
if len(bucketName) == 0 || len(key) == 0 {
err = errNotFound
return
}
objectBucketName = bucketName
objectKey = key
// 查询已有
this.locker.RLock()
provider, ok = this.providerMap[uniqueId]
if ok {
this.locker.RUnlock()
return provider, bucketName, objectKey, nil
}
this.locker.RUnlock()
this.locker.Lock()
defer this.locker.Unlock()
// 再次查询
provider, ok = this.providerMap[uniqueId]
if ok {
return provider, bucketName, objectKey, nil
}
var rawProvider ProviderInterface
switch ossConfig.Type {
case ossconfigs.OSSTypeTencentCOS:
rawProvider = NewTencentCOSProvider()
case ossconfigs.OSSTypeAliyunOSS:
rawProvider = NewAliyunOSSProvider()
case ossconfigs.OSSTypeHuaweiOBS:
rawProvider = NewHuaweiOBSProvider()
case ossconfigs.OSSTypeBaiduBOS:
rawProvider = NewBaiduBOSProvider()
case ossconfigs.OSSTypeQiniuKodo:
rawProvider = NewQiniuKodoProvider()
case ossconfigs.OSSTypeAmazonS3:
rawProvider = NewAmazonS3Provider()
case ossconfigs.OSSTypeB2:
rawProvider = NewB2Provider()
default:
return nil, "", "", errors.New("invalid provider '" + ossConfig.Type + "'")
}
if rawProvider == nil {
return nil, "", "", errors.New("invalid provider '" + ossConfig.Type + "'")
}
// 包装
provider = NewProvider(rawProvider)
provider.SetUniqueId(uniqueId)
// 初始化
err = provider.Init(options, bucketName)
if err != nil {
return nil, "", "", err
}
// 放入缓存
this.providerMap[uniqueId] = provider
return
}
func (this *Manager) Head(req *http.Request, host string, ossConfig *ossconfigs.OSSConfig) (resp *http.Response, nativeErrCode string, nativeBucketName string, err error) {
if ossConfig == nil {
return nil, "", "", errors.New("provider config should not be nil")
}
provider, bucketName, key, err := this.FindProviderWithConfig(req, host, ossConfig)
if err != nil {
return nil, "", "", err
}
nativeBucketName = bucketName
resp, nativeErrCode, err = provider.Head(key)
if err == errNoBucket {
this.locker.Lock()
delete(this.providerMap, provider.UniqueId())
this.locker.Unlock()
}
return
}
func (this *Manager) Get(req *http.Request, host string, ossConfig *ossconfigs.OSSConfig) (resp *http.Response, nativeErrCode string, nativeBucketName string, err error) {
if ossConfig == nil {
return nil, "", "", errors.New("provider config should not be nil")
}
provider, bucketName, key, err := this.FindProviderWithConfig(req, host, ossConfig)
if err != nil {
return nil, "", "", err
}
nativeBucketName = bucketName
resp, nativeErrCode, err = provider.Get(key)
if err == errNoBucket {
this.locker.Lock()
delete(this.providerMap, provider.UniqueId())
this.locker.Unlock()
}
return
}
func (this *Manager) GetRange(req *http.Request, host string, bytesRange string, ossConfig *ossconfigs.OSSConfig) (resp *http.Response, nativeErrCode string, nativeBucketName string, err error) {
if ossConfig == nil {
return nil, "", "", errors.New("provider config should not be nil")
}
provider, bucketName, key, err := this.FindProviderWithConfig(req, host, ossConfig)
if err != nil {
return nil, "", "", err
}
nativeBucketName = bucketName
resp, nativeErrCode, err = provider.GetRange(key, bytesRange)
if err == errNoBucket {
this.locker.Lock()
delete(this.providerMap, provider.UniqueId())
this.locker.Unlock()
}
return
}
func (this *Manager) GC() {
// 查询
this.locker.RLock()
if len(this.providerMap) < 1024 { // 如果数量很小,则不需要清理
this.locker.RUnlock()
return
}
// 查询"过期"的Provider实例
var expiredKeys = []string{}
var currentTime = fasttime.Now().Unix()
for key, provider := range this.providerMap {
if provider.UpdatedAt < currentTime-86400 {
expiredKeys = append(expiredKeys, key)
}
}
this.locker.RUnlock()
// 删除
if len(expiredKeys) > 0 {
this.locker.Lock()
for _, key := range expiredKeys {
delete(this.providerMap, key)
}
this.locker.Unlock()
}
}

View File

@@ -0,0 +1,65 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package oss
import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ossconfigs"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"net/http"
)
type Provider struct {
rawProvider ProviderInterface
uniqueId string
UpdatedAt int64
}
func NewProvider(rawProvider ProviderInterface) *Provider {
return &Provider{
rawProvider: rawProvider,
UpdatedAt: fasttime.Now().Unix(),
}
}
func (this *Provider) Init(options ossconfigs.OSSOptions, bucketName string) error {
return this.rawProvider.Init(options, bucketName)
}
func (this *Provider) SetUniqueId(uniqueId string) {
this.uniqueId = uniqueId
}
func (this *Provider) UniqueId() string {
return this.uniqueId
}
func (this *Provider) Head(key string) (resp *http.Response, nativeErrCode string, err error) {
this.UpdatedAt = fasttime.Now().Unix()
if len(key) == 0 {
return nil, "", errNotFound
}
return this.rawProvider.Head(key)
}
func (this *Provider) Get(key string) (resp *http.Response, nativeErrCode string, err error) {
this.UpdatedAt = fasttime.Now().Unix()
if len(key) == 0 {
return nil, "", errNotFound
}
return this.rawProvider.Get(key)
}
func (this *Provider) GetRange(key string, bytesRange string) (resp *http.Response, nativeErrCode string, err error) {
this.UpdatedAt = fasttime.Now().Unix()
if len(key) == 0 {
return nil, "", errNotFound
}
return this.rawProvider.GetRange(key, bytesRange)
}

View File

@@ -0,0 +1,163 @@
// 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/aliyun/aliyun-oss-go-sdk/oss"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
)
type AliyunOSSProvider struct {
bucket *oss.Bucket
}
func NewAliyunOSSProvider() *AliyunOSSProvider {
return &AliyunOSSProvider{}
}
func (this *AliyunOSSProvider) Init(options ossconfigs.OSSOptions, bucketName string) error {
realOptions, ok := options.(*ossconfigs.AliyunOSSProviderOptions)
if !ok {
return errors.New("invalid options for 'AliyunOSSProvider'")
}
client, err := oss.New(realOptions.Endpoint, realOptions.AccessKeyId, realOptions.AccessKeySecret)
if err != nil {
return err
}
bucket, err := client.Bucket(bucketName)
if err != nil {
return err
}
this.bucket = bucket
return nil
}
func (this *AliyunOSSProvider) Head(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
header, respErr := this.bucket.GetObjectMeta(key)
if err != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
// not found
if len(header) == 0 {
header = http.Header{"Content-Length": []string{"0"}}
httpResponse = &http.Response{
StatusCode: http.StatusNotFound,
Header: header,
Body: io.NopCloser(bytes.NewReader(nil)),
ContentLength: 0,
}
return
}
httpResponse = &http.Response{
StatusCode: http.StatusOK,
Header: header,
Body: io.NopCloser(bytes.NewReader(nil)),
ContentLength: types.Int64(header.Get("Content-Length")),
}
return
}
func (this *AliyunOSSProvider) Get(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
body, respErr := this.bucket.GetObject(key)
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
resp, ok := body.(*oss.Response)
if ok {
httpResponse = &http.Response{
StatusCode: http.StatusOK,
Header: resp.Headers,
Body: resp.Body,
ContentLength: types.Int64(resp.Headers.Get("Content-Length")),
}
return
}
httpResponse = &http.Response{
StatusCode: http.StatusOK,
Body: body,
}
return
}
func (this *AliyunOSSProvider) GetRange(key string, bytesRange string) (httpResponse *http.Response, nativeErrCode string, err error) {
rangeResult, parseOk := httpRequestParseRangeHeader(bytesRange)
if !parseOk || len(rangeResult) == 0 {
return this.Get(key)
}
var firstRange = rangeResult[0]
var start = firstRange.Start()
var end = firstRange.End()
if start < 0 || end < 0 || start > end {
return this.Get(key)
}
body, respErr := this.bucket.GetObject(key, oss.Range(start, end))
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
resp, ok := body.(*oss.Response)
if ok {
httpResponse = &http.Response{
StatusCode: resp.StatusCode,
Header: resp.Headers,
Body: resp.Body,
ContentLength: types.Int64(resp.Headers.Get("Content-Length")),
}
return
}
httpResponse = &http.Response{
StatusCode: http.StatusPartialContent,
Body: body,
}
return
}
func (this *AliyunOSSProvider) parseErr(err error) (errCode string, resultErr error) {
if err == nil {
return
}
resultErr = err
serviceErr, ok := err.(oss.ServiceError)
if ok {
errCode = serviceErr.Code
// 特殊错误
if errCode == "NoSuchBucket" {
resultErr = errNoBucket
return
}
if errCode == "NoSuchKey" {
resultErr = errNotFound
}
if errCode == "RequestTimeout" {
resultErr = errRequestTimeout
return
}
}
return
}

View File

@@ -0,0 +1,308 @@
// 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
}

View File

@@ -0,0 +1,35 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package oss
import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ossconfigs"
"net/http"
)
type B2Provider struct {
s3Provider ProviderInterface
}
func NewB2Provider() *B2Provider {
return &B2Provider{
s3Provider: NewAmazonS3Provider(),
}
}
func (this *B2Provider) Init(options ossconfigs.OSSOptions, bucketName string) error {
return this.s3Provider.Init(options, bucketName)
}
func (this *B2Provider) Head(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
return this.s3Provider.Head(key)
}
func (this *B2Provider) Get(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
return this.s3Provider.Get(key)
}
func (this *B2Provider) GetRange(key string, bytesRange string) (httpResponse *http.Response, nativeErrCode string, err error) {
return this.s3Provider.GetRange(key, bytesRange)
}

View File

@@ -0,0 +1,181 @@
// 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/baidubce/bce-sdk-go/bce"
"github.com/baidubce/bce-sdk-go/services/bos"
"github.com/baidubce/bce-sdk-go/services/bos/api"
"github.com/iwind/TeaGo/types"
"io"
"net/http"
)
type BaiduBOSProvider struct {
bucketName string
client *bos.Client
}
func NewBaiduBOSProvider() *BaiduBOSProvider {
return &BaiduBOSProvider{}
}
func (this *BaiduBOSProvider) Init(options ossconfigs.OSSOptions, bucketName string) error {
realOptions, ok := options.(*ossconfigs.BaiduBOSProviderOptions)
if !ok {
return errors.New("invalid options for 'BaiduOBSProvider'")
}
this.bucketName = bucketName
client, err := bos.NewClient(realOptions.AccessKey, realOptions.SecretKey, "https://"+realOptions.Endpoint)
if err != nil {
return err
}
this.client = client
return nil
}
func (this *BaiduBOSProvider) Head(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
result, respErr := this.client.GetObjectMeta(this.bucketName, key)
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
httpResponse = &http.Response{
StatusCode: http.StatusOK,
Header: this.composeHeader(result.ObjectMeta),
ContentLength: result.ContentLength,
Body: io.NopCloser(bytes.NewReader(nil)),
}
return
}
func (this *BaiduBOSProvider) Get(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
result, respErr := this.client.BasicGetObject(this.bucketName, key)
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
httpResponse = &http.Response{
StatusCode: http.StatusOK,
Header: this.composeHeader(result.ObjectMeta),
ContentLength: result.ContentLength,
Body: result.Body,
}
return
}
func (this *BaiduBOSProvider) GetRange(key string, bytesRange string) (httpResponse *http.Response, nativeErrCode string, err error) {
rangeResult, parseOk := httpRequestParseRangeHeader(bytesRange)
if !parseOk || len(rangeResult) == 0 {
return this.Get(key)
}
var firstRange = rangeResult[0]
var start = firstRange.Start()
var end = firstRange.End()
if start < 0 || end < 0 || start > end {
return this.Get(key)
}
result, respErr := this.client.GetObject(this.bucketName, key, nil, start, end)
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
var statusCode = http.StatusOK
if len(result.ContentRange) > 0 {
statusCode = http.StatusPartialContent
}
httpResponse = &http.Response{
StatusCode: statusCode,
Header: this.composeHeader(result.ObjectMeta),
ContentLength: result.ContentLength,
Body: result.Body,
}
return
}
func (this *BaiduBOSProvider) parseErr(err error) (errCode string, resultErr error) {
if err == nil {
return
}
resultErr = err
respErr, ok := err.(*bce.BceServiceError)
if ok {
errCode = respErr.Code
}
// 特殊错误
if errCode == "NoSuchBucket" {
resultErr = errNoBucket
return
}
if errCode == "NoSuchKey" {
resultErr = errNotFound
return
}
if errCode == "RequestTimeout" {
resultErr = errRequestTimeout
return
}
return
}
func (this *BaiduBOSProvider) composeHeader(result api.ObjectMeta) http.Header {
var header = http.Header{}
if len(result.CacheControl) > 0 {
header.Set("Cache-Control", result.CacheControl)
}
if len(result.ContentDisposition) > 0 {
header.Set("Content-Disposition", result.ContentDisposition)
}
if len(result.ContentEncoding) > 0 {
header.Set("Content-Encoding", result.ContentEncoding)
}
header.Set("Content-Length", types.String(result.ContentLength))
if len(result.ContentRange) > 0 {
header.Set("Content-Range", result.ContentRange)
}
if len(result.ContentType) > 0 {
header.Set("Content-Type", result.ContentType)
}
if len(result.ContentMD5) > 0 {
header["Content-MD5"] = []string{result.ContentMD5}
}
if len(result.ContentSha256) > 0 {
header.Set("Content-Sha256", result.ContentSha256)
}
if len(result.ContentCrc32) > 0 {
header.Set("Content-Crc32", result.ContentCrc32)
}
if len(result.Expires) > 0 {
header.Set("Expires", result.Expires)
}
if len(result.LastModified) > 0 {
header.Set("Last-Modified", result.LastModified)
}
if len(result.ETag) > 0 {
header["ETag"] = []string{result.ETag}
}
return header
}

View File

@@ -0,0 +1,154 @@
// 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/huaweicloud/huaweicloud-sdk-go-obs/obs"
"io"
"net/http"
)
type HuaweiOBSProvider struct {
buckerName string
client *obs.ObsClient
}
func NewHuaweiOBSProvider() *HuaweiOBSProvider {
return &HuaweiOBSProvider{}
}
func (this *HuaweiOBSProvider) Init(options ossconfigs.OSSOptions, bucketName string) error {
realOptions, ok := options.(*ossconfigs.HuaweiOBSProviderOptions)
if !ok {
return errors.New("invalid options for 'HuaweiOBSProvider'")
}
this.buckerName = bucketName
client, err := obs.New(realOptions.AccessKeyId, realOptions.AccessKeySecret, "https://"+realOptions.Endpoint)
if err != nil {
return err
}
this.client = client
return nil
}
func (this *HuaweiOBSProvider) Head(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
var input = &obs.HeadObjectInput{
Bucket: this.buckerName,
Key: key,
}
output, respErr := this.client.HeadObject(input)
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
httpResponse = &http.Response{
StatusCode: output.StatusCode,
ContentLength: 0,
Header: this.formatHeaders(output.ResponseHeaders),
Body: io.NopCloser(bytes.NewReader(nil)),
}
return
}
func (this *HuaweiOBSProvider) Get(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
var input = &obs.GetObjectInput{}
input.Bucket = this.buckerName
input.Key = key
output, respErr := this.client.GetObject(input)
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
httpResponse = &http.Response{
StatusCode: output.StatusCode,
ContentLength: output.ContentLength,
Header: this.formatHeaders(output.ResponseHeaders),
Body: output.Body,
}
return
}
func (this *HuaweiOBSProvider) GetRange(key string, bytesRange string) (httpResponse *http.Response, nativeErrCode string, err error) {
rangeResult, parseOk := httpRequestParseRangeHeader(bytesRange)
if !parseOk || len(rangeResult) == 0 {
return this.Get(key)
}
var firstRange = rangeResult[0]
var start = firstRange.Start()
var end = firstRange.End()
if start < 0 || end < 0 || start > end {
return this.Get(key)
}
var input = &obs.GetObjectInput{}
input.Bucket = this.buckerName
input.Key = key
input.RangeStart = start
input.RangeEnd = end
output, respErr := this.client.GetObject(input)
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
httpResponse = &http.Response{
StatusCode: output.StatusCode,
ContentLength: output.ContentLength,
Header: this.formatHeaders(output.ResponseHeaders),
Body: output.Body,
}
return
}
func (this *HuaweiOBSProvider) parseErr(err error) (errCode string, resultErr error) {
if err == nil {
return
}
resultErr = err
respErr, ok := err.(obs.ObsError)
if ok {
errCode = respErr.Code
}
// 特殊错误
if errCode == "NoSuchBucket" {
resultErr = errNoBucket
return
}
if errCode == "NoSuchKey" {
resultErr = errNotFound
return
}
if errCode == "RequestTimeout" {
resultErr = errRequestTimeout
return
}
return
}
func (this *HuaweiOBSProvider) formatHeaders(rawHeader http.Header) (resultHeader http.Header) {
resultHeader = http.Header{}
for k, v := range rawHeader {
if k == "x-reserved" {
continue
}
resultHeader[http.CanonicalHeaderKey(k)] = v
}
return resultHeader
}

View File

@@ -0,0 +1,19 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package oss
import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ossconfigs"
"net/http"
)
type ProviderInterface interface {
Init(options ossconfigs.OSSOptions, bucketName string) error
Head(key string) (httpResponse *http.Response, nativeErrCode string, err error)
Get(key string) (httpResponse *http.Response, nativeErrCode string, err error)
GetRange(key string, bytesRange string) (httpResponse *http.Response, nativeErrCode string, err error)
}

View File

@@ -0,0 +1,109 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package oss
import (
"crypto/tls"
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ossconfigs"
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
"net/http"
"time"
)
type QiniuKodoProvider struct {
isPublic bool
domain string
mac *qbox.Mac
httpClient *http.Client
}
func NewQiniuKodoProvider() *QiniuKodoProvider {
return &QiniuKodoProvider{}
}
func (this *QiniuKodoProvider) Init(options ossconfigs.OSSOptions, bucketName string) error {
realOptions, ok := options.(*ossconfigs.QiniuKodoProviderOptions)
if !ok {
return errors.New("invalid options for 'QiniuKodoProvider'")
}
this.isPublic = realOptions.IsPublic
this.domain = realOptions.Protocol + "://" + realOptions.Domain
this.mac = qbox.NewMac(realOptions.AccessKey, realOptions.SecretKey)
this.httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
return nil
}
func (this *QiniuKodoProvider) Head(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
var url string
if this.isPublic {
url = storage.MakePublicURLv2(this.domain, key)
} else {
var deadline = time.Now().Add(1 * time.Hour).Unix() // 1小时有效期
url = storage.MakePrivateURLv2(this.mac, this.domain, key, deadline)
}
req, reqErr := http.NewRequest(http.MethodHead, url, nil)
if reqErr != nil {
err = reqErr
return
}
req.Header.Set("User-Agent", teaconst.GlobalProductName+"-Node/"+teaconst.Version)
httpResponse, err = this.httpClient.Do(req)
return
}
func (this *QiniuKodoProvider) Get(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
var url string
if this.isPublic {
url = storage.MakePublicURLv2(this.domain, key)
} else {
var deadline = time.Now().Add(1 * time.Hour).Unix() // 1小时有效期
url = storage.MakePrivateURLv2(this.mac, this.domain, key, deadline)
}
req, reqErr := http.NewRequest(http.MethodGet, url, nil)
if reqErr != nil {
err = reqErr
return
}
req.Header.Set("User-Agent", teaconst.GlobalProductName+"-Node/"+teaconst.Version)
httpResponse, err = this.httpClient.Do(req)
return
}
func (this *QiniuKodoProvider) GetRange(key string, bytesRange string) (httpResponse *http.Response, nativeErrCode string, err error) {
var url string
if this.isPublic {
url = storage.MakePublicURLv2(this.domain, key)
} else {
var deadline = time.Now().Add(1 * time.Hour).Unix() // 1小时有效期
url = storage.MakePrivateURLv2(this.mac, this.domain, key, deadline)
}
req, reqErr := http.NewRequest(http.MethodGet, url, nil)
if reqErr != nil {
err = reqErr
return
}
req.Header.Set("User-Agent", teaconst.GlobalProductName+"-Node/"+teaconst.Version)
req.Header.Set("Range", bytesRange)
httpResponse, err = this.httpClient.Do(req)
return
}

View File

@@ -0,0 +1,118 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package oss
import (
"context"
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/ossconfigs"
"github.com/tencentyun/cos-go-sdk-v5"
"net/http"
"net/url"
)
type TencentCOSProvider struct {
client *cos.Client
}
func NewTencentCOSProvider() ProviderInterface {
return &TencentCOSProvider{}
}
func (this *TencentCOSProvider) Init(options ossconfigs.OSSOptions, bucketName string) error {
realOptions, ok := options.(*ossconfigs.TencentCOSProviderOptions)
if !ok {
return errors.New("invalid options for 'TencentCOSProvider'")
}
// TODO 将此URL组织方式放入到集群设置--对象存储策略中
bucketURL, err := url.Parse("https://" + bucketName + ".cos." + realOptions.Region + ".myqcloud.com")
if err != nil {
return err
}
serviceURL, err := url.Parse("https://cos." + realOptions.Region + ".myqcloud.com")
if err != nil {
return err
}
var baseURL = &cos.BaseURL{
BucketURL: bucketURL,
ServiceURL: serviceURL,
}
this.client = cos.NewClient(baseURL, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: realOptions.SecretId,
SecretKey: realOptions.SecretKey,
},
})
return nil
}
func (this *TencentCOSProvider) Head(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
resp, respErr := this.client.Object.Head(context.Background(), key, &cos.ObjectHeadOptions{})
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
httpResponse = resp.Response
return
}
func (this *TencentCOSProvider) Get(key string) (httpResponse *http.Response, nativeErrCode string, err error) {
resp, respErr := this.client.Object.Get(context.Background(), key, &cos.ObjectGetOptions{})
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
httpResponse = resp.Response
return
}
func (this *TencentCOSProvider) GetRange(key string, bytesRange string) (httpResponse *http.Response, nativeErrCode string, err error) {
resp, respErr := this.client.Object.Get(context.Background(), key, &cos.ObjectGetOptions{
Range: bytesRange,
})
if respErr != nil {
nativeErrCode, err = this.parseErr(respErr)
return
}
httpResponse = resp.Response
return
}
func (this *TencentCOSProvider) parseErr(err error) (errCode string, resultErr error) {
if err == nil {
return
}
resultErr = err
respErr, ok := err.(*cos.ErrorResponse)
if ok {
errCode = respErr.Code
}
// 特殊错误
if errCode == "NoSuchBucket" {
resultErr = errNoBucket
return
}
if cos.IsNotFoundError(err) {
resultErr = errNotFound
return
}
if errCode == "RequestTimeout" {
resultErr = errRequestTimeout
return
}
return
}

View File

@@ -0,0 +1,74 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
//go:build plus
package oss
import (
rangeutils "github.com/TeaOSLab/EdgeNode/internal/utils/ranges"
"strconv"
"strings"
)
func httpRequestParseRangeHeader(rangeValue string) (result []rangeutils.Range, ok bool) {
// 参考RFChttps://tools.ietf.org/html/rfc7233
index := strings.Index(rangeValue, "=")
if index == -1 {
return
}
unit := rangeValue[:index]
if unit != "bytes" {
return
}
var rangeSetString = rangeValue[index+1:]
if len(rangeSetString) == 0 {
ok = true
return
}
var pieces = strings.Split(rangeSetString, ", ")
for _, piece := range pieces {
index = strings.Index(piece, "-")
if index == -1 {
return
}
first := piece[:index]
firstInt := int64(-1)
var err error
last := piece[index+1:]
var lastInt = int64(-1)
if len(first) > 0 {
firstInt, err = strconv.ParseInt(first, 10, 64)
if err != nil {
return
}
if len(last) > 0 {
lastInt, err = strconv.ParseInt(last, 10, 64)
if err != nil {
return
}
if lastInt < firstInt {
return
}
}
} else {
if len(last) == 0 {
return
}
lastInt, err = strconv.ParseInt(last, 10, 64)
if err != nil {
return
}
lastInt = -lastInt
}
result = append(result, [2]int64{firstInt, lastInt})
}
ok = true
return
}