package storage import ( "context" "fmt" "io" "net/url" "path" "strings" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "kra/app/system/internal/biz" "kra/app/system/internal/conf" ) type s3Storage struct { client *minio.Client bucket, baseURL, prefix string } func newS3Storage(provider string, config *conf.AdminBackend_ObjectStore) (biz.FileStorage, error) { if config == nil || config.Bucket == "" || config.AccessKey == "" || config.SecretKey == "" { return nil, fmt.Errorf("%s storage configuration is incomplete", provider) } endpoint := config.Endpoint if endpoint == "" { switch provider { case "aws-s3": endpoint = "s3." + config.Region + ".amazonaws.com" case "cloudflare-r2": endpoint = config.AccountId + ".r2.cloudflarestorage.com" case "aliyun-oss": endpoint = "oss-" + config.Region + ".aliyuncs.com" case "huawei-obs": endpoint = "obs." + config.Region + ".myhuaweicloud.com" case "tencent-cos": endpoint = "cos." + config.Region + ".myqcloud.com" } } secure := config.UseSsl if parsed, err := url.Parse(endpoint); err == nil && parsed.Host != "" { secure = parsed.Scheme == "https" endpoint = parsed.Host } if endpoint == "" { return nil, fmt.Errorf("%s endpoint is required", provider) } client, err := minio.New(strings.TrimSuffix(endpoint, "/"), &minio.Options{Creds: credentials.NewStaticV4(config.AccessKey, config.SecretKey, ""), Secure: secure, Region: config.Region, BucketLookup: func() minio.BucketLookupType { if config.ForcePathStyle { return minio.BucketLookupPath } return minio.BucketLookupAuto }()}) if err != nil { return nil, err } return &s3Storage{client: client, bucket: config.Bucket, baseURL: strings.TrimSuffix(config.BaseUrl, "/"), prefix: strings.Trim(config.PathPrefix, "/")}, nil } func (s *s3Storage) key(name string) string { if s.prefix == "" { return strings.TrimPrefix(name, "/") } return path.Join(s.prefix, strings.TrimPrefix(name, "/")) } func (s *s3Storage) unkey(key string) string { return strings.TrimPrefix(strings.TrimPrefix(key, s.prefix), "/") } func (s *s3Storage) file(key string, size int64) *biz.StoredFile { name := s.unkey(key) rawURL := s.baseURL + "/" + key return &biz.StoredFile{Name: path.Base(name), Path: name, URL: rawURL, Size: size} } func (s *s3Storage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) { key := s.key(name) info, err := s.client.PutObject(ctx, s.bucket, key, reader, -1, minio.PutObjectOptions{}) if err != nil { return nil, err } return s.file(key, info.Size), nil } func (s *s3Storage) Open(ctx context.Context, name string) (io.ReadCloser, error) { obj, err := s.client.GetObject(ctx, s.bucket, s.key(name), minio.GetObjectOptions{}) if err != nil { return nil, err } if _, err = obj.Stat(); err != nil { _ = obj.Close() return nil, err } return obj, nil } func (s *s3Storage) Delete(ctx context.Context, name string) error { return s.client.RemoveObject(ctx, s.bucket, s.key(name), minio.RemoveObjectOptions{}) } func (s *s3Storage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) { return composeFiles(ctx, s, names, destination) } func (s *s3Storage) DeletePrefix(ctx context.Context, prefix string) error { items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true}) for item := range items { if item.Err != nil { return item.Err } if err := s.client.RemoveObject(ctx, s.bucket, item.Key, minio.RemoveObjectOptions{}); err != nil { return err } } return nil } func (s *s3Storage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) { if limit <= 0 { limit = 100 } items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true, StartAfter: s.key(cursor)}) out := make([]*biz.StoredFile, 0, limit+1) for item := range items { if item.Err != nil { return nil, "", false, item.Err } file := s.file(item.Key, item.Size) file.LastModified = item.LastModified file.ContentType = item.ContentType out = append(out, file) if len(out) > limit { break } } more := len(out) > limit if more { out = out[:limit] } next := "" if len(out) > 0 { next = out[len(out)-1].Path } return out, next, more, nil }