feat(infra): add file listing support (#1836)

This commit is contained in:
Ryo
2025-08-27 12:06:55 +08:00
committed by GitHub
parent 901d0252e8
commit 263a75b1c0
8 changed files with 512 additions and 141 deletions

View File

@@ -22,20 +22,16 @@ import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/volcengine/ve-tos-golang-sdk/v2/tos"
"github.com/volcengine/ve-tos-golang-sdk/v2/tos/enum"
"github.com/coze-dev/coze-studio/backend/infra/contract/imagex"
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
"github.com/coze-dev/coze-studio/backend/infra/impl/storage/proxy"
"github.com/coze-dev/coze-studio/backend/pkg/ctxcache"
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/types/consts"
)
type tosClient struct {
@@ -43,11 +39,12 @@ type tosClient struct {
bucketName string
}
func NewStorageImagex(ctx context.Context, ak, sk, bucketName, endpoint, region string) (imagex.ImageX, error) {
func New(ctx context.Context, ak, sk, bucketName, endpoint, region string) (storage.Storage, error) {
t, err := getTosClient(ctx, ak, sk, bucketName, endpoint, region)
if err != nil {
return nil, err
}
// t.test()
return t, nil
}
@@ -69,19 +66,15 @@ func getTosClient(ctx context.Context, ak, sk, bucketName, endpoint, region stri
if err != nil {
return nil, err
}
return t, nil
}
func New(ctx context.Context, ak, sk, bucketName, endpoint, region string) (storage.Storage, error) {
t, err := getTosClient(ctx, ak, sk, bucketName, endpoint, region)
if err != nil {
return nil, err
}
// t.test()
return t, nil
}
func (t *tosClient) test() {
// test list objects
ctx := context.Background()
t.ListObjects(ctx, "")
// test upload
objectKey := fmt.Sprintf("test-%s.txt", time.Now().Format("20060102150405"))
err := t.PutObject(context.Background(), objectKey, []byte("hello world"))
@@ -129,7 +122,7 @@ func (t *tosClient) CheckAndCreateBucket(ctx context.Context) error {
if serverErr.StatusCode == http.StatusNotFound {
// Bucket does not exist
logs.CtxInfof(ctx, "Bucket not found.")
resp, err := client.CreateBucketV2(context.Background(), &tos.CreateBucketV2Input{
resp, err := client.CreateBucketV2(ctx, &tos.CreateBucketV2Input{
Bucket: bucketName,
ACL: enum.ACLPrivate,
})
@@ -183,6 +176,7 @@ func (t *tosClient) PutObjectWithReader(ctx context.Context, objectKey string, c
}
_, err := client.PutObjectV2(ctx, input)
return err
}
@@ -246,47 +240,84 @@ func (t *tosClient) GetObjectUrl(ctx context.Context, objectKey string, opts ...
return output.SignedUrl, nil
}
func (i *tosClient) GetUploadHost(ctx context.Context) string {
currentHost, ok := ctxcache.Get[string](ctx, consts.HostKeyInCtx)
if !ok {
return ""
func (t *tosClient) ListObjectsPaginated(ctx context.Context, input *storage.ListObjectsPaginatedInput) (*storage.ListObjectsPaginatedOutput, error) {
if input == nil {
return nil, fmt.Errorf("input cannot be nil")
}
return currentHost + consts.ApplyUploadActionURI
}
func (t *tosClient) GetServerID() string {
return ""
}
func (t *tosClient) GetUploadAuth(ctx context.Context, opt ...imagex.UploadAuthOpt) (*imagex.SecurityToken, error) {
scheme := strings.ToLower(os.Getenv(consts.StorageUploadHTTPScheme))
if scheme == "" {
scheme = "http"
if input.PageSize <= 0 {
return nil, fmt.Errorf("page size must be positive")
}
return &imagex.SecurityToken{
AccessKeyID: "",
SecretAccessKey: "",
SessionToken: "",
ExpiredTime: time.Now().Add(time.Hour).Format("2006-01-02 15:04:05"),
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
HostScheme: scheme,
}, nil
}
func (t *tosClient) GetResourceURL(ctx context.Context, uri string, opts ...imagex.GetResourceOpt) (*imagex.ResourceURL, error) {
url, err := t.GetObjectUrl(ctx, uri)
output, err := t.client.ListObjectsV2(ctx, &tos.ListObjectsV2Input{
Bucket: t.bucketName,
ListObjectsInput: tos.ListObjectsInput{
MaxKeys: int(input.PageSize),
Marker: input.Cursor,
Prefix: input.Prefix,
},
})
if err != nil {
return nil, err
return nil, fmt.Errorf("list objects failed, err: %w", err)
}
return &imagex.ResourceURL{
URL: url,
files := make([]*storage.FileInfo, 0, len(output.Contents))
for _, obj := range output.Contents {
if obj.Size == 0 && strings.HasSuffix(obj.Key, "/") {
logs.CtxDebugf(ctx, "[ListObjectsPaginated] skip dir: %s", obj.Key)
continue
}
files = append(files, &storage.FileInfo{
Key: obj.Key,
LastModified: obj.LastModified,
ETag: obj.ETag,
Size: obj.Size,
})
}
return &storage.ListObjectsPaginatedOutput{
Files: files,
Cursor: output.NextMarker,
IsTruncated: output.IsTruncated,
}, nil
}
func (t *tosClient) Upload(ctx context.Context, data []byte, opts ...imagex.UploadAuthOpt) (*imagex.UploadResult, error) {
return nil, nil
}
func (t *tosClient) ListObjects(ctx context.Context, prefix string) ([]*storage.FileInfo, error) {
const (
DefaultPageSize = 100
MaxListObjects = 10000
)
func (t *tosClient) GetUploadAuthWithExpire(ctx context.Context, expire time.Duration, opt ...imagex.UploadAuthOpt) (*imagex.SecurityToken, error) {
return nil, nil
files := make([]*storage.FileInfo, 0, DefaultPageSize)
cursor := ""
for {
output, err := t.ListObjectsPaginated(ctx, &storage.ListObjectsPaginatedInput{
Prefix: prefix,
PageSize: DefaultPageSize,
Cursor: cursor,
})
if err != nil {
return nil, fmt.Errorf("list objects failed, prefix = %v, err: %v", prefix, err)
}
for _, object := range output.Files {
logs.CtxDebugf(ctx, "key = %s, lastModified = %s, eTag = %s, size = %d", object.Key, object.LastModified, object.ETag, object.Size)
files = append(files, object)
}
cursor = output.Cursor
logs.CtxDebugf(ctx, "IsTruncated = %v, Cursor = %s", output.IsTruncated, output.Cursor)
if len(files) >= MaxListObjects {
logs.CtxErrorf(ctx, "[ListObjects] max list objects reached, total: %d", len(files))
break
}
if !output.IsTruncated || output.Cursor == "" {
break
}
}
return files, nil
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tos
import (
"context"
"os"
"strings"
"time"
"github.com/coze-dev/coze-studio/backend/infra/contract/imagex"
"github.com/coze-dev/coze-studio/backend/pkg/ctxcache"
"github.com/coze-dev/coze-studio/backend/types/consts"
)
func NewStorageImagex(ctx context.Context, ak, sk, bucketName, endpoint, region string) (imagex.ImageX, error) {
t, err := getTosClient(ctx, ak, sk, bucketName, endpoint, region)
if err != nil {
return nil, err
}
return t, nil
}
func (i *tosClient) GetUploadHost(ctx context.Context) string {
currentHost, ok := ctxcache.Get[string](ctx, consts.HostKeyInCtx)
if !ok {
return ""
}
return currentHost + consts.ApplyUploadActionURI
}
func (t *tosClient) GetServerID() string {
return ""
}
func (t *tosClient) GetUploadAuth(ctx context.Context, opt ...imagex.UploadAuthOpt) (*imagex.SecurityToken, error) {
scheme := strings.ToLower(os.Getenv(consts.StorageUploadHTTPScheme))
if scheme == "" {
scheme = "http"
}
return &imagex.SecurityToken{
AccessKeyID: "",
SecretAccessKey: "",
SessionToken: "",
ExpiredTime: time.Now().Add(time.Hour).Format("2006-01-02 15:04:05"),
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
HostScheme: scheme,
}, nil
}
func (t *tosClient) GetResourceURL(ctx context.Context, uri string, opts ...imagex.GetResourceOpt) (*imagex.ResourceURL, error) {
url, err := t.GetObjectUrl(ctx, uri)
if err != nil {
return nil, err
}
return &imagex.ResourceURL{
URL: url,
}, nil
}
func (t *tosClient) Upload(ctx context.Context, data []byte, opts ...imagex.UploadAuthOpt) (*imagex.UploadResult, error) {
return nil, nil
}
func (t *tosClient) GetUploadAuthWithExpire(ctx context.Context, expire time.Duration, opt ...imagex.UploadAuthOpt) (*imagex.SecurityToken, error) {
return nil, nil
}