Initial commit (code only without large binaries)
This commit is contained in:
1
EdgeNode/internal/oss/README.md
Normal file
1
EdgeNode/internal/oss/README.md
Normal file
@@ -0,0 +1 @@
|
||||
注意事项:在开发OSS Provider时注意要确保 `resp.ContentLength` 必须大于0(如果文件内容不为空的话)。
|
||||
18
EdgeNode/internal/oss/errors.go
Normal file
18
EdgeNode/internal/oss/errors.go
Normal 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
|
||||
}
|
||||
216
EdgeNode/internal/oss/manager.go
Normal file
216
EdgeNode/internal/oss/manager.go
Normal 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()
|
||||
}
|
||||
}
|
||||
65
EdgeNode/internal/oss/provider.go
Normal file
65
EdgeNode/internal/oss/provider.go
Normal 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)
|
||||
}
|
||||
163
EdgeNode/internal/oss/provider_aliyun_oss.go
Normal file
163
EdgeNode/internal/oss/provider_aliyun_oss.go
Normal 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
|
||||
}
|
||||
308
EdgeNode/internal/oss/provider_amzon_s3.go
Normal file
308
EdgeNode/internal/oss/provider_amzon_s3.go
Normal 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
|
||||
}
|
||||
35
EdgeNode/internal/oss/provider_b2.go
Normal file
35
EdgeNode/internal/oss/provider_b2.go
Normal 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)
|
||||
}
|
||||
181
EdgeNode/internal/oss/provider_baidu_bos.go
Normal file
181
EdgeNode/internal/oss/provider_baidu_bos.go
Normal 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
|
||||
}
|
||||
154
EdgeNode/internal/oss/provider_huawei_obs.go
Normal file
154
EdgeNode/internal/oss/provider_huawei_obs.go
Normal 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
|
||||
}
|
||||
19
EdgeNode/internal/oss/provider_interface.go
Normal file
19
EdgeNode/internal/oss/provider_interface.go
Normal 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)
|
||||
}
|
||||
109
EdgeNode/internal/oss/provider_qiniu_kodo.go
Normal file
109
EdgeNode/internal/oss/provider_qiniu_kodo.go
Normal 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
|
||||
}
|
||||
118
EdgeNode/internal/oss/provider_tencent_cos.go
Normal file
118
EdgeNode/internal/oss/provider_tencent_cos.go
Normal 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
|
||||
}
|
||||
74
EdgeNode/internal/oss/utils.go
Normal file
74
EdgeNode/internal/oss/utils.go
Normal 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) {
|
||||
// 参考RFC:https://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
|
||||
}
|
||||
Reference in New Issue
Block a user