package storage import ( "context" "fmt" "io" "kra/internal/biz/system" "net/http" "net/url" "path" "strings" "time" "kra/internal/config" cos "github.com/tencentyun/cos-go-sdk-v5" ) type tencentStorage struct { client *cos.Client baseURL, prefix string } func newTencentStorage(config *config.ObjectStore) (system.FileStorage, error) { if config == nil || config.Bucket == "" || config.Region == "" || config.AccessKey == "" || config.SecretKey == "" { return nil, fmt.Errorf("tencent-cos storage configuration is incomplete") } endpoint := config.Endpoint if endpoint == "" { endpoint = fmt.Sprintf("https://%s.cos.%s.myqcloud.com", config.Bucket, config.Region) } else if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { if config.UseSSL { endpoint = "https://" + endpoint } else { endpoint = "http://" + endpoint } } bucketURL, err := url.Parse(endpoint) if err != nil { return nil, err } client := cos.NewClient(&cos.BaseURL{BucketURL: bucketURL}, &http.Client{Transport: &cos.AuthorizationTransport{SecretID: config.AccessKey, SecretKey: config.SecretKey}}) return &tencentStorage{client: client, baseURL: strings.TrimSuffix(config.BaseURL, "/"), prefix: strings.Trim(config.PathPrefix, "/")}, nil } func (s *tencentStorage) key(name string) string { if s.prefix == "" { return strings.TrimPrefix(name, "/") } return path.Join(s.prefix, strings.TrimPrefix(name, "/")) } func (s *tencentStorage) unkey(key string) string { return strings.TrimPrefix(strings.TrimPrefix(key, s.prefix), "/") } func (s *tencentStorage) file(key string, size int64) *system.StoredFile { name := s.unkey(key) fileURL := s.baseURL + "/" + key return &system.StoredFile{Name: path.Base(name), Path: name, URL: fileURL, Size: size} } func (s *tencentStorage) Put(ctx context.Context, name string, reader io.Reader) (*system.StoredFile, error) { key := s.key(name) if _, err := s.client.Object.Put(ctx, key, reader, nil); err != nil { return nil, err } response, err := s.client.Object.Head(ctx, key, nil) if err != nil { return nil, err } return s.file(key, response.ContentLength), nil } func (s *tencentStorage) Open(ctx context.Context, name string) (io.ReadCloser, error) { response, err := s.client.Object.Get(ctx, s.key(name), nil) if err != nil { return nil, err } return response.Body, nil } func (s *tencentStorage) Delete(ctx context.Context, name string) error { _, err := s.client.Object.Delete(ctx, s.key(name)) return err } func (s *tencentStorage) Compose(ctx context.Context, names []string, destination string) (*system.StoredFile, string, error) { return composeFiles(ctx, s, names, destination) } func (s *tencentStorage) DeletePrefix(ctx context.Context, prefix string) error { return deletePrefixViaList(ctx, prefix, s.List, s.Delete) } func (s *tencentStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) { if limit <= 0 { limit = 100 } result, _, err := s.client.Bucket.Get(ctx, &cos.BucketGetOptions{Prefix: boundedPrefix(s.key(prefix), prefix), Marker: cursor, MaxKeys: limit}) if err != nil { return nil, "", false, err } items := make([]*system.StoredFile, 0, len(result.Contents)) for _, object := range result.Contents { item := s.file(object.Key, object.Size) for _, layout := range []string{time.RFC3339, "2006-01-02T15:04:05.000Z"} { if parsed, parseErr := time.Parse(layout, object.LastModified); parseErr == nil { item.LastModified = parsed break } } items = append(items, item) } next := result.NextMarker if result.IsTruncated && next == "" && len(result.Contents) > 0 { next = result.Contents[len(result.Contents)-1].Key } return items, next, result.IsTruncated, nil }