// 对象存储:namespace / 桶 / 对象 / 预签名请求(PAR)。 // 上传下载数据面走 PAR 直连 OCI,面板只做控制面; // 例外:预览/编辑的小文件经面板中转(GetObject/PutObject),不签发 PAR。 package oci import ( "bytes" "context" "errors" "fmt" "io" "strings" "time" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/objectstorage" ) // ErrObjectTooLarge 表示对象超出面板中转大小上限。 var ErrObjectTooLarge = errors.New("object too large for inline transfer") // ObjectContent 是对象内容与写入所需元数据(面板中转小文件用)。 type ObjectContent struct { Data []byte ContentType string Etag string } // Bucket 是存储桶摘要(含用量估算,来自 GetBucket 的 approximate 字段)。 type Bucket struct { Name string `json:"name"` Namespace string `json:"namespace"` Visibility string `json:"visibility"` // NoPublicAccess / ObjectRead StorageTier string `json:"storageTier"` VersioningOn bool `json:"versioningOn"` ApproximateCount int64 `json:"approximateCount"` ApproximateSize int64 `json:"approximateSize"` TimeCreated *time.Time `json:"timeCreated"` } // ObjectSummary 是对象列表行;文件夹以 prefixes 单独返回。 type ObjectSummary struct { Name string `json:"name"` Size int64 `json:"size"` StorageTier string `json:"storageTier"` ArchivalState string `json:"archivalState"` TimeModified *time.Time `json:"timeModified"` } // ListObjectsResult 是分页对象列表:delimiter="/" 模式,子层级归入 Prefixes。 type ListObjectsResult struct { Objects []ObjectSummary `json:"objects"` Prefixes []string `json:"prefixes"` NextStartWith string `json:"nextStartWith"` } // ObjectDetail 是对象元数据(HEAD)。 type ObjectDetail struct { Name string `json:"name"` Size int64 `json:"size"` ContentType string `json:"contentType"` Etag string `json:"etag"` ContentMd5 string `json:"contentMd5"` StorageTier string `json:"storageTier"` ArchivalState string `json:"archivalState"` TimeModified *time.Time `json:"timeModified"` Metadata map[string]string `json:"metadata"` } // PAR 是预签名请求摘要;FullURL 仅创建响应携带(OCI 事后不可再取回)。 type PAR struct { ID string `json:"id"` Name string `json:"name"` ObjectName string `json:"objectName"` AccessType string `json:"accessType"` FullURL string `json:"fullUrl,omitempty"` TimeExpires *time.Time `json:"timeExpires"` TimeCreated *time.Time `json:"timeCreated"` } // CreateBucketInput 创建桶参数;CompartmentID 空为生效 compartment。 type CreateBucketInput struct { Name string CompartmentID string PublicRead bool StorageTier string // Standard / Archive VersioningOn bool } // UpdateBucketInput 更新桶参数;nil 字段不改。 type UpdateBucketInput struct { PublicRead *bool VersioningOn *bool } // CreatePARInput 签发预签名请求参数。 type CreatePARInput struct { Name string ObjectName string // 空 = 桶级(配合 AnyObjectReadWrite 前缀用法) AccessType string // ObjectRead / ObjectWrite / ObjectReadWrite / AnyObjectReadWrite ExpiresHours int } // bulkRetryPolicy 批量删除(清空桶)的限流退避:16 并发下 OCI 可能回 429。 // 不用 DefaultRetryPolicy:其 8 次尝试、退避上限 30s,且带最终一致性模式 // (9 次、上限 45s)——大批量删除里 worker 一旦触发就长睡,吞吐塌方; // 这里收紧为 5 次、上限 3s、关掉最终一致性,单条最坏 ~13s 后放行不拖全场。 var bulkRetryPolicy = common.NewRetryPolicyWithOptions( common.ReplaceWithValuesFromRetryPolicy(common.DefaultRetryPolicyWithoutEventualConsistency()), common.WithMaximumNumberAttempts(5), common.WithExponentialBackoff(3*time.Second, 2.0), ) func (c *RealClient) osClient(cred Credentials, region string) (objectstorage.ObjectStorageClient, error) { oc, err := objectstorage.NewObjectStorageClientWithConfigurationProvider(provider(cred)) if err != nil { return oc, fmt.Errorf("new object storage client: %w", err) } applyProxy(&oc.BaseClient, cred) if region != "" { oc.SetRegion(normalizeRegion(region)) } return oc, nil } // GetObjectStorageNamespace 实现 Client:租户 namespace(全租户唯一,常量语义), // 首次回源后按 tenancy 进程内缓存;此前每个对象存储操作都远程取一次, // 经代理链路时白付一整次冷连接往返。并发首次可能重复回源,结果相同无害。 func (c *RealClient) GetObjectStorageNamespace(ctx context.Context, cred Credentials, region string) (string, error) { if v, ok := c.namespaces.Load(cred.TenancyOCID); ok { return v.(string), nil } oc, err := c.osClient(cred, region) if err != nil { return "", err } resp, err := oc.GetNamespace(ctx, objectstorage.GetNamespaceRequest{}) if err != nil { return "", fmt.Errorf("get namespace: %w", err) } if cred.TenancyOCID != "" { c.namespaces.Store(cred.TenancyOCID, deref(resp.Value)) } return deref(resp.Value), nil } // ListBuckets 实现 Client:列出指定 compartment 下的桶并逐个补全用量(空为生效 compartment)。 func (c *RealClient) ListBuckets(ctx context.Context, cred Credentials, region, compartmentID string) ([]Bucket, error) { oc, err := c.osClient(cred, region) if err != nil { return nil, err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return nil, err } resp, err := oc.ListBuckets(ctx, objectstorage.ListBucketsRequest{ NamespaceName: &ns, CompartmentId: hostCompartment(cred, compartmentID), }) if err != nil { return nil, fmt.Errorf("list buckets: %w", err) } out := make([]Bucket, 0, len(resp.Items)) for _, it := range resp.Items { out = append(out, c.bucketDetail(ctx, oc, ns, deref(it.Name))) } return out, nil } // bucketDetail 取单桶详情;失败时退化为只有名字的摘要,不拖垮列表。 func (c *RealClient) bucketDetail(ctx context.Context, oc objectstorage.ObjectStorageClient, ns, name string) Bucket { resp, err := oc.GetBucket(ctx, objectstorage.GetBucketRequest{ NamespaceName: &ns, BucketName: &name, Fields: []objectstorage.GetBucketFieldsEnum{"approximateCount", "approximateSize"}, }) if err != nil { return Bucket{Name: name, Namespace: ns} } b := Bucket{ Name: name, Namespace: ns, Visibility: string(resp.PublicAccessType), StorageTier: string(resp.StorageTier), VersioningOn: resp.Versioning == objectstorage.BucketVersioningEnabled, ApproximateCount: derefI64(resp.ApproximateCount), ApproximateSize: derefI64(resp.ApproximateSize), } if resp.TimeCreated != nil { b.TimeCreated = &resp.TimeCreated.Time } return b } func derefI64(p *int64) int64 { if p != nil { return *p } return 0 } // CreateBucket 实现 Client。 func (c *RealClient) CreateBucket(ctx context.Context, cred Credentials, region string, in CreateBucketInput) (Bucket, error) { oc, err := c.osClient(cred, region) if err != nil { return Bucket{}, err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return Bucket{}, err } details := objectstorage.CreateBucketDetails{ Name: &in.Name, CompartmentId: hostCompartment(cred, in.CompartmentID), PublicAccessType: objectstorage.CreateBucketDetailsPublicAccessTypeNopublicaccess, StorageTier: objectstorage.CreateBucketDetailsStorageTierStandard, } if in.PublicRead { details.PublicAccessType = objectstorage.CreateBucketDetailsPublicAccessTypeObjectread } if strings.EqualFold(in.StorageTier, "Archive") { details.StorageTier = objectstorage.CreateBucketDetailsStorageTierArchive } if in.VersioningOn { details.Versioning = objectstorage.CreateBucketDetailsVersioningEnabled } if _, err := oc.CreateBucket(ctx, objectstorage.CreateBucketRequest{ NamespaceName: &ns, CreateBucketDetails: details, }); err != nil { return Bucket{}, fmt.Errorf("create bucket: %w", err) } return c.bucketDetail(ctx, oc, ns, in.Name), nil } // UpdateBucket 实现 Client:改可见性 / 版本控制。 func (c *RealClient) UpdateBucket(ctx context.Context, cred Credentials, region, name string, in UpdateBucketInput) (Bucket, error) { oc, err := c.osClient(cred, region) if err != nil { return Bucket{}, err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return Bucket{}, err } details := objectstorage.UpdateBucketDetails{} if in.PublicRead != nil { details.PublicAccessType = objectstorage.UpdateBucketDetailsPublicAccessTypeNopublicaccess if *in.PublicRead { details.PublicAccessType = objectstorage.UpdateBucketDetailsPublicAccessTypeObjectread } } if in.VersioningOn != nil { details.Versioning = objectstorage.UpdateBucketDetailsVersioningSuspended if *in.VersioningOn { details.Versioning = objectstorage.UpdateBucketDetailsVersioningEnabled } } if _, err := oc.UpdateBucket(ctx, objectstorage.UpdateBucketRequest{ NamespaceName: &ns, BucketName: &name, UpdateBucketDetails: details, }); err != nil { return Bucket{}, fmt.Errorf("update bucket: %w", err) } return c.bucketDetail(ctx, oc, ns, name), nil } // DeleteBucket 实现 Client:仅空桶可删,OCI 拒绝非空桶。 // ErrBucketNotEmpty 表示桶内仍有对象 / 历史版本 / 未完成分片上传,OCI 拒绝删除。 var ErrBucketNotEmpty = errors.New("bucket not empty") func (c *RealClient) DeleteBucket(ctx context.Context, cred Credentials, region, name string) error { oc, err := c.osClient(cred, region) if err != nil { return err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return err } if _, err := oc.DeleteBucket(ctx, objectstorage.DeleteBucketRequest{ NamespaceName: &ns, BucketName: &name, }); err != nil { if isBucketNotEmptyErr(err) { return fmt.Errorf("delete bucket %s: %w", name, ErrBucketNotEmpty) } return fmt.Errorf("delete bucket: %w", err) } return nil } // isBucketNotEmptyErr 识别「桶非空」类删除拒绝:对象/版本/分片报 BucketNotEmpty, // 仅剩活跃 PAR 时 OCI 报 409 且消息为 Active Preauthenticated Requests still exist, // 两者都可经后台清空(对象版本+PAR)后重删。 func isBucketNotEmptyErr(err error) bool { var se common.ServiceError if !errors.As(err, &se) { return false } if se.GetCode() == "BucketNotEmpty" { return true } return se.GetHTTPStatusCode() == 409 && strings.Contains(se.GetMessage(), "Preauthenticated Request") } // ObjectVersion 是对象版本摘要,清空桶时逐版本删除用。 type ObjectVersion struct { Name string `json:"name"` VersionID string `json:"versionId"` } // ListObjectVersions 实现 Client:分页列出全部对象版本(未开版本控制的桶返回当前版本)。 func (c *RealClient) ListObjectVersions(ctx context.Context, cred Credentials, region, bucket, page string) ([]ObjectVersion, string, error) { oc, err := c.osClient(cred, region) if err != nil { return nil, "", err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return nil, "", err } req := objectstorage.ListObjectVersionsRequest{ NamespaceName: &ns, BucketName: &bucket, Limit: common.Int(1000), } if page != "" { req.Page = &page } resp, err := oc.ListObjectVersions(ctx, req) if err != nil { return nil, "", fmt.Errorf("list object versions: %w", err) } out := make([]ObjectVersion, 0, len(resp.Items)) for _, it := range resp.Items { out = append(out, ObjectVersion{Name: deref(it.Name), VersionID: deref(it.VersionId)}) } return out, deref(resp.OpcNextPage), nil } // DeleteObjectVersion 实现 Client:删除指定版本(versionID 空为当前版本)。 func (c *RealClient) DeleteObjectVersion(ctx context.Context, cred Credentials, region, bucket, object, versionID string) error { oc, err := c.osClient(cred, region) if err != nil { return err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return err } req := objectstorage.DeleteObjectRequest{ NamespaceName: &ns, BucketName: &bucket, ObjectName: &object, RequestMetadata: common.RequestMetadata{RetryPolicy: &bulkRetryPolicy}, } if versionID != "" { req.VersionId = &versionID } if _, err := oc.DeleteObject(ctx, req); err != nil { return fmt.Errorf("delete object version: %w", err) } return nil } // ListObjects 实现 Client:delimiter="/" 前缀模式;startWith 为上页游标。 func (c *RealClient) ListObjects(ctx context.Context, cred Credentials, region, bucket, prefix, startWith string, limit int) (ListObjectsResult, error) { oc, err := c.osClient(cred, region) if err != nil { return ListObjectsResult{}, err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return ListObjectsResult{}, err } if limit <= 0 || limit > 500 { limit = 100 } req := objectstorage.ListObjectsRequest{ NamespaceName: &ns, BucketName: &bucket, Delimiter: common.String("/"), Limit: &limit, Fields: common.String("name,size,timeModified,storageTier,archivalState"), } if prefix != "" { req.Prefix = &prefix } if startWith != "" { req.Start = &startWith } resp, err := oc.ListObjects(ctx, req) if err != nil { return ListObjectsResult{}, fmt.Errorf("list objects: %w", err) } return toListObjectsResult(resp), nil } func toListObjectsResult(resp objectstorage.ListObjectsResponse) ListObjectsResult { out := ListObjectsResult{ Objects: make([]ObjectSummary, 0, len(resp.Objects)), Prefixes: orEmpty(resp.Prefixes), NextStartWith: deref(resp.NextStartWith), } for _, o := range resp.Objects { item := ObjectSummary{ Name: deref(o.Name), Size: derefI64(o.Size), StorageTier: string(o.StorageTier), ArchivalState: string(o.ArchivalState), } if o.TimeModified != nil { item.TimeModified = &o.TimeModified.Time } out.Objects = append(out.Objects, item) } return out } // DeleteObject 实现 Client。 func (c *RealClient) DeleteObject(ctx context.Context, cred Credentials, region, bucket, object string) error { oc, err := c.osClient(cred, region) if err != nil { return err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return err } if _, err := oc.DeleteObject(ctx, objectstorage.DeleteObjectRequest{ NamespaceName: &ns, BucketName: &bucket, ObjectName: &object, }); err != nil { return fmt.Errorf("delete object: %w", err) } return nil } // RenameObject 实现 Client:OCI 原生重命名(同桶内)。 func (c *RealClient) RenameObject(ctx context.Context, cred Credentials, region, bucket, src, dst string) error { oc, err := c.osClient(cred, region) if err != nil { return err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return err } if _, err := oc.RenameObject(ctx, objectstorage.RenameObjectRequest{ NamespaceName: &ns, BucketName: &bucket, RenameObjectDetails: objectstorage.RenameObjectDetails{ SourceName: &src, NewName: &dst, }, }); err != nil { return fmt.Errorf("rename object: %w", err) } return nil } // RestoreObject 实现 Client:Archive 对象取回,hours 为可下载时长(默认 24)。 func (c *RealClient) RestoreObject(ctx context.Context, cred Credentials, region, bucket, object string, hours int) error { oc, err := c.osClient(cred, region) if err != nil { return err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return err } req := objectstorage.RestoreObjectsRequest{ NamespaceName: &ns, BucketName: &bucket, RestoreObjectsDetails: objectstorage.RestoreObjectsDetails{ ObjectName: &object, }, } if hours > 0 { req.RestoreObjectsDetails.Hours = &hours } if _, err := oc.RestoreObjects(ctx, req); err != nil { return fmt.Errorf("restore object: %w", err) } return nil } // HeadObject 实现 Client:对象元数据。 func (c *RealClient) HeadObject(ctx context.Context, cred Credentials, region, bucket, object string) (ObjectDetail, error) { oc, err := c.osClient(cred, region) if err != nil { return ObjectDetail{}, err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return ObjectDetail{}, err } resp, err := oc.HeadObject(ctx, objectstorage.HeadObjectRequest{ NamespaceName: &ns, BucketName: &bucket, ObjectName: &object, }) if err != nil { return ObjectDetail{}, fmt.Errorf("head object: %w", err) } d := ObjectDetail{ Name: object, Size: derefI64(resp.ContentLength), ContentType: deref(resp.ContentType), Etag: deref(resp.ETag), ContentMd5: deref(resp.ContentMd5), StorageTier: string(resp.StorageTier), ArchivalState: string(resp.ArchivalState), Metadata: resp.OpcMeta, } if resp.LastModified != nil { d.TimeModified = &resp.LastModified.Time } return d, nil } // GetObject 实现 Client:整体读入对象内容;超过 maxBytes 返回 ErrObjectTooLarge。 func (c *RealClient) GetObject(ctx context.Context, cred Credentials, region, bucket, object string, maxBytes int64) (ObjectContent, error) { oc, err := c.osClient(cred, region) if err != nil { return ObjectContent{}, err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return ObjectContent{}, err } resp, err := oc.GetObject(ctx, objectstorage.GetObjectRequest{ NamespaceName: &ns, BucketName: &bucket, ObjectName: &object, }) if err != nil { return ObjectContent{}, fmt.Errorf("get object: %w", err) } defer resp.Content.Close() if derefI64(resp.ContentLength) > maxBytes { return ObjectContent{}, fmt.Errorf("get object %s (%d bytes): %w", object, derefI64(resp.ContentLength), ErrObjectTooLarge) } data, err := io.ReadAll(io.LimitReader(resp.Content, maxBytes+1)) if err != nil { return ObjectContent{}, fmt.Errorf("read object body: %w", err) } if int64(len(data)) > maxBytes { return ObjectContent{}, fmt.Errorf("get object %s: %w", object, ErrObjectTooLarge) } return ObjectContent{Data: data, ContentType: deref(resp.ContentType), Etag: deref(resp.ETag)}, nil } // PutObject 实现 Client:整体写入对象;ifMatch 非空时作为 If-Match 条件, // 不匹配由 OCI 返回 412(经 ServiceError 透出)。返回新 ETag。 func (c *RealClient) PutObject(ctx context.Context, cred Credentials, region, bucket, object string, data []byte, contentType, ifMatch string) (string, error) { oc, err := c.osClient(cred, region) if err != nil { return "", err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return "", err } length := int64(len(data)) req := objectstorage.PutObjectRequest{ NamespaceName: &ns, BucketName: &bucket, ObjectName: &object, ContentLength: &length, PutObjectBody: io.NopCloser(bytes.NewReader(data)), } if contentType != "" { req.ContentType = &contentType } if ifMatch != "" { req.IfMatch = &ifMatch } resp, err := oc.PutObject(ctx, req) if err != nil { return "", fmt.Errorf("put object: %w", err) } return deref(resp.ETag), nil } // CreatePAR 实现 Client:签发预签名请求并拼出完整 URL(仅此时可得)。 func (c *RealClient) CreatePAR(ctx context.Context, cred Credentials, region, bucket string, in CreatePARInput) (PAR, error) { oc, err := c.osClient(cred, region) if err != nil { return PAR{}, err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return PAR{}, err } if in.ExpiresHours <= 0 { in.ExpiresHours = 24 } details := objectstorage.CreatePreauthenticatedRequestDetails{ Name: &in.Name, AccessType: objectstorage.CreatePreauthenticatedRequestDetailsAccessTypeEnum(in.AccessType), TimeExpires: &common.SDKTime{Time: time.Now().Add(time.Duration(in.ExpiresHours) * time.Hour)}, } if in.ObjectName != "" { details.ObjectName = &in.ObjectName } // 桶级 PAR(AnyObject*)必须显式桶列举动作,缺省会被 OCI 拒绝;一律放开列举便于收件人浏览 if strings.HasPrefix(in.AccessType, "AnyObject") { details.BucketListingAction = objectstorage.PreauthenticatedRequestBucketListingActionListobjects } resp, err := oc.CreatePreauthenticatedRequest(ctx, objectstorage.CreatePreauthenticatedRequestRequest{ NamespaceName: &ns, BucketName: &bucket, CreatePreauthenticatedRequestDetails: details, }) if err != nil { return PAR{}, fmt.Errorf("create par: %w", err) } return toPAR(resp.PreauthenticatedRequest, oc.Endpoint()), nil } func toPAR(p objectstorage.PreauthenticatedRequest, endpoint string) PAR { out := PAR{ ID: deref(p.Id), Name: deref(p.Name), ObjectName: deref(p.ObjectName), AccessType: string(p.AccessType), } if p.AccessUri != nil { out.FullURL = strings.TrimSuffix(endpoint, "/") + *p.AccessUri } if p.TimeExpires != nil { out.TimeExpires = &p.TimeExpires.Time } if p.TimeCreated != nil { out.TimeCreated = &p.TimeCreated.Time } return out } // ListPARsPage 实现 Client:单页列出桶内预签名请求(摘要不含 URL), // 返回下一页游标,空串表示已到末页;limit ≤0 时用 OCI 默认页大小。 func (c *RealClient) ListPARsPage(ctx context.Context, cred Credentials, region, bucket, page string, limit int) ([]PAR, string, error) { oc, err := c.osClient(cred, region) if err != nil { return nil, "", err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return nil, "", err } req := objectstorage.ListPreauthenticatedRequestsRequest{NamespaceName: &ns, BucketName: &bucket} if limit > 0 { req.Limit = common.Int(limit) } if page != "" { req.Page = &page } resp, err := oc.ListPreauthenticatedRequests(ctx, req) if err != nil { return nil, "", fmt.Errorf("list pars: %w", err) } out := make([]PAR, 0, len(resp.Items)) for _, it := range resp.Items { out = append(out, summaryPAR(it)) } return out, deref(resp.OpcNextPage), nil } // ListPARs 实现 Client:翻页列出桶内全部有效预签名请求(清空/全部删除用)。 func (c *RealClient) ListPARs(ctx context.Context, cred Credentials, region, bucket string) ([]PAR, error) { out := make([]PAR, 0, 16) page := "" for { items, next, err := c.ListPARsPage(ctx, cred, region, bucket, page, 1000) if err != nil { return nil, err } out = append(out, items...) if next == "" { return out, nil } page = next } } func summaryPAR(p objectstorage.PreauthenticatedRequestSummary) PAR { out := PAR{ ID: deref(p.Id), Name: deref(p.Name), ObjectName: deref(p.ObjectName), AccessType: string(p.AccessType), } if p.TimeExpires != nil { out.TimeExpires = &p.TimeExpires.Time } if p.TimeCreated != nil { out.TimeCreated = &p.TimeCreated.Time } return out } // DeletePAR 实现 Client:撤销预签名请求,链接立即失效。 func (c *RealClient) DeletePAR(ctx context.Context, cred Credentials, region, bucket, parID string) error { oc, err := c.osClient(cred, region) if err != nil { return err } ns, err := c.GetObjectStorageNamespace(ctx, cred, region) if err != nil { return err } if _, err := oc.DeletePreauthenticatedRequest(ctx, objectstorage.DeletePreauthenticatedRequestRequest{ NamespaceName: &ns, BucketName: &bucket, ParId: &parID, RequestMetadata: common.RequestMetadata{RetryPolicy: &bulkRetryPolicy}, }); err != nil { return fmt.Errorf("delete par: %w", err) } return nil }