@@ -66,8 +66,9 @@ func (s *OciConfigService) UpdateBucket(ctx context.Context, id uint, region, na
|
||||
return s.client.UpdateBucket(ctx, cred, region, name, in)
|
||||
}
|
||||
|
||||
// DeleteBucket 删除桶:空桶同步秒删;非空桶转后台清空(对象全部版本 + PAR)后删除,
|
||||
// 返回 queued=true 表示已排队。
|
||||
// DeleteBucket 删除桶:空桶同步秒删;非空桶转后台清空(对象全部版本 + PAR +
|
||||
// 未完成分片)后删除,返回 queued=true 表示已排队。同一桶的清空任务全局单飞,
|
||||
// 重复删除请求不再并发起新任务;执行权在触发方同步抢占,避免双触发窗口。
|
||||
func (s *OciConfigService) DeleteBucket(ctx context.Context, id uint, region, name string) (bool, error) {
|
||||
cred, err := s.credentialsByID(ctx, id)
|
||||
if err != nil {
|
||||
@@ -80,16 +81,43 @@ func (s *OciConfigService) DeleteBucket(ctx context.Context, id uint, region, na
|
||||
if !errors.Is(err, oci.ErrBucketNotEmpty) {
|
||||
return false, err
|
||||
}
|
||||
go s.purgeAndDeleteBucket(cred, region, name)
|
||||
key := fmt.Sprintf("%d/%s/%s", id, region, name)
|
||||
if !s.beginPurge(key) {
|
||||
return true, nil // 已有清空任务在途,视为已排队
|
||||
}
|
||||
s.bindWG.Add(1)
|
||||
go func() {
|
||||
defer s.bindWG.Done()
|
||||
defer s.endPurge(key)
|
||||
s.purgeAndDeleteBucket(cred, region, name)
|
||||
}()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// beginPurge / endPurge 维护桶清空在途注册表(键 cfgID/region/bucket)。
|
||||
func (s *OciConfigService) beginPurge(key string) bool {
|
||||
s.purgeMu.Lock()
|
||||
defer s.purgeMu.Unlock()
|
||||
if s.purgeInFlight[key] {
|
||||
return false
|
||||
}
|
||||
s.purgeInFlight[key] = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *OciConfigService) endPurge(key string) {
|
||||
s.purgeMu.Lock()
|
||||
defer s.purgeMu.Unlock()
|
||||
delete(s.purgeInFlight, key)
|
||||
}
|
||||
|
||||
// purgeBucketTimeout 是后台清空删除桶的总时限;超大桶超时后可重试删除续跑。
|
||||
const purgeBucketTimeout = 30 * time.Minute
|
||||
|
||||
// purgeAndDeleteBucket 后台清空并删除桶;失败只记日志(删除请求已受理)。
|
||||
// 顺序:对象版本 → PAR → 未完成分片 → 删桶;分片不清会让空桶仍以非空拒绝删除。
|
||||
func (s *OciConfigService) purgeAndDeleteBucket(cred oci.Credentials, region, name string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), purgeBucketTimeout)
|
||||
ctx, cancel := s.purgeContext()
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
if err := s.purgeBucketVersions(ctx, cred, region, name); err != nil {
|
||||
@@ -99,6 +127,9 @@ func (s *OciConfigService) purgeAndDeleteBucket(cred oci.Credentials, region, na
|
||||
if err := s.purgeBucketPARs(ctx, cred, region, name); err != nil {
|
||||
log.Printf("[bucket-purge] %s purge pars failed: %v", name, err)
|
||||
}
|
||||
if err := s.client.AbortAllMultipartUploads(ctx, cred, region, name); err != nil {
|
||||
log.Printf("[bucket-purge] %s abort multipart uploads failed: %v", name, err)
|
||||
}
|
||||
if err := s.client.DeleteBucket(ctx, cred, region, name); err != nil {
|
||||
log.Printf("[bucket-purge] %s delete failed: %v", name, err)
|
||||
return
|
||||
@@ -106,6 +137,21 @@ func (s *OciConfigService) purgeAndDeleteBucket(cred oci.Credentials, region, na
|
||||
log.Printf("[bucket-purge] %s purged and deleted in %s", name, time.Since(start).Round(time.Second))
|
||||
}
|
||||
|
||||
// purgeContext 组装清空任务的 ctx:总超时之外叠加关服信号,
|
||||
// 避免 Stop 等待在途清空长达整个超时窗口。
|
||||
func (s *OciConfigService) purgeContext() (context.Context, context.CancelFunc) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), purgeBucketTimeout)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-s.bindStop:
|
||||
cancel()
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
return ctx, func() { close(done); cancel() }
|
||||
}
|
||||
|
||||
// purgeBucketVersions 逐页并发删除全部对象版本(未开版本控制的桶即当前对象);
|
||||
// 结束时记总量与耗时,便于判断慢在对象清空还是 PAR 清空。
|
||||
func (s *OciConfigService) purgeBucketVersions(ctx context.Context, cred oci.Credentials, region, name string) error {
|
||||
@@ -238,19 +284,34 @@ func (s *OciConfigService) PutObjectContent(ctx context.Context, id uint, region
|
||||
|
||||
// parAccessTypes 是允许签发的 PAR 访问类型。
|
||||
var parAccessTypes = map[string]bool{
|
||||
"ObjectRead": true, "ObjectWrite": true, "ObjectReadWrite": true, "AnyObjectReadWrite": true,
|
||||
"ObjectRead": true, "ObjectWrite": true, "ObjectReadWrite": true,
|
||||
"AnyObjectRead": true, "AnyObjectWrite": true, "AnyObjectReadWrite": true,
|
||||
}
|
||||
|
||||
// CreatePAR 签发预签名请求;过期时长限制在 1 小时到 30 天。
|
||||
// PAR 输入校验错误供 API 层稳定映射 400。
|
||||
var (
|
||||
ErrPARInvalidAccessType = errors.New("invalid PAR access type")
|
||||
ErrPARInvalidExpiration = errors.New("invalid PAR expiration")
|
||||
)
|
||||
|
||||
// maxParHours 是 PAR 有效期安全上限(100 年):time.Duration 按小时乘会在
|
||||
// ≈292 年(2562047h)溢出回绕为负,收在远低于溢出点又远超实际需求的位置。
|
||||
const maxParHours = 100 * 365 * 24
|
||||
|
||||
// CreatePAR 签发预签名请求;过期时长 1 小时到 100 年(放开旧 30 天上限)。
|
||||
func (s *OciConfigService) CreatePAR(ctx context.Context, id uint, region, bucket string, in oci.CreatePARInput) (oci.PAR, error) {
|
||||
if !parAccessTypes[in.AccessType] {
|
||||
return oci.PAR{}, fmt.Errorf("create par: unsupported access type %q", in.AccessType)
|
||||
return oci.PAR{}, fmt.Errorf("create par: %w: unsupported access type %q", ErrPARInvalidAccessType, in.AccessType)
|
||||
}
|
||||
if in.ExpiresHours < 1 || in.ExpiresHours > 30*24 {
|
||||
return oci.PAR{}, fmt.Errorf("create par: expiresHours must be within 1-720")
|
||||
if in.ExpiresHours < 1 || in.ExpiresHours > maxParHours {
|
||||
return oci.PAR{}, fmt.Errorf("create par: %w: expiresHours must be within 1-%d", ErrPARInvalidExpiration, maxParHours)
|
||||
}
|
||||
if strings.TrimSpace(in.Name) == "" {
|
||||
in.Name = "par-" + strings.ReplaceAll(in.ObjectName, "/", "-")
|
||||
base := in.ObjectName
|
||||
if base == "" {
|
||||
base = "bucket-" + bucket
|
||||
}
|
||||
in.Name = "par-" + strings.ReplaceAll(base, "/", "-")
|
||||
}
|
||||
cred, err := s.credentialsByID(ctx, id)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user