Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ docs/resources
docs/public

playground
registry/storage/driver/cos/.env
*.env
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
This project inherits from [Distribution](https://github.com/distribution/distribution) and add some new storage drivers. Additional support providers are as follows:

- [Tencent Cloud Object Stroage (COS)](https://www.tencentcloud.com/products/cos?lang=en) ✅
- [AliCloud Object Storage Service (OSS)](https://www.alibabacloud.com/en/product/object-storage-service?_p_lc=1) ⌛️
- [AliCloud Object Storage Service (OSS)](https://www.alibabacloud.com/en/product/object-storage-service?_p_lc=1)

## Provider

Expand All @@ -23,6 +23,16 @@ This project inherits from [Distribution](https://github.com/distribution/distri
| rootdirectory | no | This is a prefix that is applied to all S3 keys to allow you to segment data in your bucket if necessary. The default is empty. |
| serviceurl | no | It is used to get service. The default is `https://service.cos.myqcloud.com`. |

### AliCloud Object Storage Service(OSS)

| Parameter | Required | Description |
| ------------- | -------- | ------------------------------------------------------------ |
| accessid | yes | Your AliCloud Access Key Id. |
| secret | yes | Your AliCloud Access Key Secret. |
| region | yes | The OSS region in which your bucket exists. For example `cn-hangzhou`. |
| bucket | yes | The bucket name registered in OSS. For example `test-registry-1101772061`. |
| rootdirectory | no | This is a prefix that is applied to all object keys to allow you to segment data in your bucket if necessary. The default is empty. |

## Image

[dockerhub](https://hub.docker.com/explore) - [jideaflow/distribution](https://hub.docker.com/repository/docker/jideaflow/distribution/tags)
Expand Down
1 change: 1 addition & 0 deletions cmd/registry/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
_ "github.com/distribution/distribution/v3/registry/storage/driver/middleware/cloudfront"
_ "github.com/distribution/distribution/v3/registry/storage/driver/middleware/redirect"
_ "github.com/distribution/distribution/v3/registry/storage/driver/middleware/rewrite"
_ "github.com/distribution/distribution/v3/registry/storage/driver/oss"
_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"
)

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.2.3
github.com/aws/aws-sdk-go v1.55.5
github.com/bshuster-repo/logrus-logstash-hook v1.0.0
github.com/coreos/go-systemd/v22 v22.5.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.2.3 h1:LyeTJauAchnWdre3sAyterGrzaAtZ4dSNoIvDvaWfo4=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.2.3/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
Expand Down
241 changes: 238 additions & 3 deletions registry/storage/driver/cos/cos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package cos
import (
"context"
"fmt"
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/distribution/distribution/v3/registry/storage/driver/testsuites"
"github.com/joho/godotenv"
"io"
"os"
"strings"
"testing"
"time"

storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/distribution/distribution/v3/registry/storage/driver/testsuites"
"github.com/joho/godotenv"
)

const (
Expand Down Expand Up @@ -60,6 +63,10 @@ func init() {
}
}

now := time.Now()
timestamp := now.Format("20060102_150405")
rootDirectory = fmt.Sprint("test-", timestamp, "/")

cosDriverConstructor = func() (storagedriver.StorageDriver, error) {
parameters := map[string]interface{}{
"secretid": secretID,
Expand Down Expand Up @@ -94,3 +101,231 @@ func BenchmarkCosDriverSuite(b *testing.B) {
skipCheck(b)
testsuites.BenchDriver(b, cosDriverConstructor)
}

func TestPutAndGetContent(t *testing.T) {
skipCheck(t)

driver, err := cosDriverConstructor()
if err != nil {
t.Fatalf("failed to create driver: %v", err)
}

ctx := context.Background()
path := "/main/file1.txt"
content := []byte("hello oss")

// 写入
err = driver.PutContent(ctx, path, content)
if err != nil {
t.Fatalf("PutContent failed: %v", err)
}

// 读取
read, err := driver.GetContent(ctx, path)
if err != nil {
t.Fatalf("GetContent failed: %v", err)
}

if string(read) != string(content) {
t.Fatalf("content mismatch: expected %s, got %s", content, read)
}
}

func TestStatAndList(t *testing.T) {
skipCheck(t)

driver, err := cosDriverConstructor()
if err != nil {
t.Fatalf("failed to create driver: %v", err)
}

ctx := context.Background()

// 测试 Stat:文件存在
filePath := "/main/stat_test.txt"
content := []byte("stat content")
if err := driver.PutContent(ctx, filePath, content); err != nil {
t.Fatalf("PutContent failed: %v", err)
}
fi, err := driver.Stat(ctx, filePath)
if err != nil {
t.Fatalf("Stat failed for existing file: %v", err)
}
if fi.Size() != int64(len(content)) {
t.Errorf("Stat size mismatch: expected %d, got %d", len(content), fi.Size())
}
if fi.Path() != filePath {
t.Errorf("Stat path mismatch: expected %q, got %q", filePath, fi.Path())
}
if fi.IsDir() {
t.Errorf("Stat IsDir should be false for file")
}

// 测试 Stat:文件不存在
missing := "/main/no_such_file.txt"
if _, err := driver.Stat(ctx, missing); err == nil {
t.Errorf("Stat should have failed for missing file")
} else if _, ok := err.(storagedriver.PathNotFoundError); !ok {
t.Errorf("Stat error for missing file must be PathNotFoundError, got %T", err)
}

// 测试 List:在同一目录下创建多个文件
dir := "/main/list_test"
paths := []string{
dir + "/a.txt",
dir + "/b.txt",
}
for _, p := range paths {
if err := driver.PutContent(ctx, p, []byte("x")); err != nil {
t.Fatalf("PutContent failed for %s: %v", p, err)
}
}

// 列出 dir
entries, err := driver.List(ctx, dir)
if err != nil {
t.Fatalf("List failed: %v", err)
}
// 打印 entries
t.Logf("List entries in %s: %v", dir, entries)
// 结果可能无序,转为 map 方便判断
m := make(map[string]struct{}, len(entries))
for _, e := range entries {
m[e] = struct{}{}
}
for _, expected := range paths {
if _, found := m[expected]; !found {
t.Errorf("List missing entry %q in %v", expected, entries)
}
}
}

func TestReader(t *testing.T) {
skipCheck(t)

driver, err := cosDriverConstructor()
if err != nil {
t.Fatalf("failed to create driver: %v", err)
}

ctx := context.Background()
path := "/main/file_reader.txt"
content := []byte("hello oss reader test")

// 写入文件
err = driver.PutContent(ctx, path, content)
if err != nil {
t.Fatalf("PutContent failed: %v", err)
}

// 读取全量, offset = 0
reader, err := driver.Reader(ctx, path, 0)
if err != nil {
t.Fatalf("Reader failed: %v", err)
}
defer reader.Close()

readAll, err := io.ReadAll(reader)
if err != nil {
t.Fatalf("Reader ReadAll failed: %v", err)
}

if string(readAll) != string(content) {
t.Fatalf("Reader content mismatch: expected %s, got %s", content, readAll)
}

// 读取部分, offset = 6
offset := int64(6)
reader2, err := driver.Reader(ctx, path, offset)
if err != nil {
t.Fatalf("Reader with offset failed: %v", err)
}
defer func(reader2 io.ReadCloser) {
_ = reader2.Close()
}(reader2)

readPartial, err := io.ReadAll(reader2)
if err != nil {
t.Fatalf("Reader ReadAll with offset failed: %v", err)
}

expectedPartial := content[offset:]
if string(readPartial) != string(expectedPartial) {
t.Fatalf("Reader offset content mismatch: expected %s, got %s", expectedPartial, readPartial)
}
}

func TestDelete(t *testing.T) {
skipCheck(t)

driver, err := cosDriverConstructor()
if err != nil {
t.Fatalf("failed to create driver: %v", err)
}

ctx := context.Background()
path := "/main/to_delete.txt"
content := []byte("to be deleted")

// 先写入
if err := driver.PutContent(ctx, path, content); err != nil {
t.Fatalf("PutContent failed: %v", err)
}

// 确认存在
if _, err := driver.Stat(ctx, path); err != nil {
t.Fatalf("Stat before delete failed: %v", err)
}

// 调用 Delete
if err := driver.Delete(ctx, path); err != nil {
t.Fatalf("Delete failed: %v", err)
}

// 再次 Stat 应报 PathNotFoundError
if _, err := driver.Stat(ctx, path); err == nil {
t.Errorf("Stat after delete should have failed")
} else if _, ok := err.(storagedriver.PathNotFoundError); !ok {
t.Errorf("Stat after delete error must be PathNotFoundError, got %T", err)
}
}

func TestMove(t *testing.T) {
skipCheck(t)

driver, err := cosDriverConstructor()
if err != nil {
t.Fatalf("failed to create driver: %v", err)
}

ctx := context.Background()
src := "/main/move_src.txt"
dst := "/main/move_dst.txt"
content := []byte("move me")

// 写入源文件
if err := driver.PutContent(ctx, src, content); err != nil {
t.Fatalf("PutContent failed: %v", err)
}

// 调用 Move
if err := driver.Move(ctx, src, dst); err != nil {
t.Fatalf("Move failed: %v", err)
}

// 源文件应不存在
if _, err := driver.Stat(ctx, src); err == nil {
t.Errorf("Stat on src after move should have failed")
} else if _, ok := err.(storagedriver.PathNotFoundError); !ok {
t.Errorf("Stat on src after move error must be PathNotFoundError, got %T", err)
}

// 目标文件应存在且内容一致
data, err := driver.GetContent(ctx, dst)
if err != nil {
t.Fatalf("GetContent on dst failed: %v", err)
}
if string(data) != string(content) {
t.Errorf("Move content mismatch: expected %s, got %s", content, data)
}
}
Loading
Loading