feat: add independent platform modules and multi-provider storage
This commit is contained in:
101
storage/oss/oss.go
Normal file
101
storage/oss/oss.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Package oss adapts the caller-owned official Alibaba Cloud OSS v2 client.
|
||||
package oss
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/storage"
|
||||
sdk "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
client *sdk.Client
|
||||
bucket string
|
||||
maxBytes int64
|
||||
}
|
||||
|
||||
// New borrows a configured SDK client. This adapter uses simple uploads; large
|
||||
// multipart uploads are a separate provider capability, not buffered in memory.
|
||||
func New(client *sdk.Client, bucket string, maxBytes int64) (*Store, error) {
|
||||
if client == nil || bucket == "" || maxBytes < 0 || maxBytes > 5<<30 {
|
||||
return nil, errors.New("storage/oss: invalid client, bucket or size limit")
|
||||
}
|
||||
if maxBytes == 0 {
|
||||
maxBytes = 32 << 20
|
||||
}
|
||||
return &Store{client: client, bucket: bucket, maxBytes: maxBytes}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Put(ctx context.Context, key string, reader io.Reader, size int64, contentType string) (storage.Info, error) {
|
||||
if err := storage.Check(ctx, key); err != nil {
|
||||
return storage.Info{}, err
|
||||
}
|
||||
if reader == nil || size < 0 || len(contentType) > 256 || strings.ContainsAny(contentType, "\r\n") {
|
||||
return storage.Info{}, errors.New("storage/oss: invalid upload")
|
||||
}
|
||||
if size > s.maxBytes {
|
||||
return storage.Info{}, storage.ErrTooLarge
|
||||
}
|
||||
_, err := s.client.PutObject(ctx, &sdk.PutObjectRequest{Bucket: sdk.Ptr(s.bucket), Key: sdk.Ptr(key), ContentLength: sdk.Ptr(size), ContentType: sdk.Ptr(contentType), Body: storage.ContextReader{Context: ctx, Reader: reader}})
|
||||
if err != nil {
|
||||
return storage.Info{}, mapError(err)
|
||||
}
|
||||
return storage.Info{Key: key, Size: size, ContentType: contentType, ModifiedAt: time.Now().UTC()}, nil
|
||||
}
|
||||
func (s *Store) Get(ctx context.Context, key string) (storage.Object, error) {
|
||||
if err := storage.Check(ctx, key); err != nil {
|
||||
return storage.Object{}, err
|
||||
}
|
||||
r, err := s.client.GetObject(ctx, &sdk.GetObjectRequest{Bucket: sdk.Ptr(s.bucket), Key: sdk.Ptr(key)})
|
||||
if err != nil {
|
||||
return storage.Object{}, mapError(err)
|
||||
}
|
||||
if r.ContentLength < 0 || r.ContentLength > s.maxBytes {
|
||||
r.Body.Close()
|
||||
return storage.Object{}, storage.ErrTooLarge
|
||||
}
|
||||
info := storage.Info{Key: key, Size: r.ContentLength, ContentType: sdk.ToString(r.ContentType)}
|
||||
if r.LastModified != nil {
|
||||
info.ModifiedAt = *r.LastModified
|
||||
}
|
||||
return storage.Object{Info: info, Body: r.Body}, nil
|
||||
}
|
||||
func (s *Store) Delete(ctx context.Context, key string) error {
|
||||
if err := storage.Check(ctx, key); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.client.DeleteObject(ctx, &sdk.DeleteObjectRequest{Bucket: sdk.Ptr(s.bucket), Key: sdk.Ptr(key)})
|
||||
err = mapError(err)
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
func (s *Store) PresignGet(ctx context.Context, key string, ttl time.Duration) (*url.URL, error) {
|
||||
if err := storage.Check(ctx, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ttl < time.Second || ttl > time.Hour {
|
||||
return nil, errors.New("storage/oss: TTL must be 1s..1h")
|
||||
}
|
||||
r, err := s.client.Presign(ctx, &sdk.GetObjectRequest{Bucket: sdk.Ptr(s.bucket), Key: sdk.Ptr(key), ResponseContentDisposition: sdk.Ptr("attachment")}, sdk.PresignExpires(ttl))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return url.Parse(r.URL)
|
||||
}
|
||||
func mapError(err error) error {
|
||||
var serviceError *sdk.ServiceError
|
||||
if errors.As(err, &serviceError) && serviceError.Code == "NoSuchKey" {
|
||||
return storage.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var _ storage.Store = (*Store)(nil)
|
||||
var _ storage.Presigner = (*Store)(nil)
|
||||
31
storage/oss/oss_test.go
Normal file
31
storage/oss/oss_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package oss
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.yuntuo.hk/k2safe/go-omni/internal/storagefixture"
|
||||
"git.yuntuo.hk/k2safe/go-omni/storage/storagetest"
|
||||
sdk "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
|
||||
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSDKProtocolContract(t *testing.T) {
|
||||
server := httptest.NewTLSServer(storagefixture.Handler())
|
||||
defer server.Close()
|
||||
cfg := sdk.LoadDefaultConfig().WithRegion("cn-hangzhou").WithEndpoint(server.URL).WithUsePathStyle(true).WithCredentialsProvider(credentials.NewStaticCredentialsProvider("fixtureAccess", "fixtureSecret")).WithHttpClient(server.Client())
|
||||
store, err := New(sdk.NewClient(cfg), "testbucket", 64)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
storagetest.Run(t, store)
|
||||
u, err := store.PresignGet(context.Background(), "a/file.txt", time.Minute)
|
||||
if err != nil || u.Query().Get("x-oss-signature") == "" || !strings.Contains(u.Path, "a/file.txt") {
|
||||
t.Fatal("invalid signed URL", err)
|
||||
}
|
||||
if _, err = store.PresignGet(context.Background(), "a", 2*time.Hour); err == nil {
|
||||
t.Fatal("TTL accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user