【真实根因】COS 桶明明存在却报"桶不存在/创建失败"
用户配置:endpoint=loki-log-1394668510.cos.ap-hongkong.myqcloud.com(虚拟主机式,已含桶名)
旧代码:始终 GET / 并按 ListBuckets 的 <Buckets><Bucket><Name> 解析。
但在虚拟主机式 endpoint 下,GET / 返回的是 ListObjects(<ListBucketResult>)——
桶内对象列表,不是桶列表。解析必然找不到桶名 → 误判"桶不存在"
→ 触发建桶 PUT → 用户的 AK 无 cos:PutBucket 权限 → 403
→ 最终报"创建后校验失败"。
【实测确认(用该 AK 直连 COS)】
GET / (vhost) → 200 <ListBucketResult> 桶存在,能列对象
GET / (区域端点) → 200 <ListAllMyBucketsResult> AK 有 cos:GetService
HEAD / → 403 AK 无 cos:HeadBucket
PUT / (CreateBucket) → 403 AK 无 cos:PutBucket
即 QcloudCOSDataFullControl = 数据面全控(读写/列桶),不含管控面(建桶/HeadBucket)。
【修复】
1. 探测统一用 GET /,再按 endpoint 形态解释响应:
- 虚拟主机式 → 按 ListObjects(<ListBucketResult>)判断,200 即桶存在
- 区域端点 → 按 ListBuckets(<Buckets><Bucket><Name>)匹配桶名
桶存在就直接返回"桶已存在",不再走建桶流程。
2. 建桶二次校验同样改用 GET /(不再用 HEAD,避免 HeadBucket 403 误判)。
3. 建桶返回 403 时给出明确友好提示:
"当前 Access Key 无建桶权限。腾讯云 QcloudCOSDataFullControl 只授予数据读写/列出,
不含 CreateBucket。请到 COS 控制台手动创建桶,或在 CAM 追加 cos:PutBucket"
4. models/storage.go Sanitized() 用 crypto.DecryptField 解密后再返回 AK/SK
(原来直接返回 ENC:xxx 密文,前端编辑框显示密文)。
5. 前端访问地址输入框补充说明:推荐填区域端点(不带桶名)。
镜像:lghost/lokistack:latest + v0.1 = sha256:350478003870
50 lines
1.5 KiB
Go
50 lines
1.5 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"opsxloki/pkg/crypto"
|
|
)
|
|
|
|
type StorageConfig struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
StorageType string `json:"storageType"`
|
|
Endpoint string `json:"endpoint"`
|
|
Bucket string `json:"bucket"`
|
|
Region string `json:"region"`
|
|
AccessKey string `json:"accessKey"`
|
|
SecretKey string `json:"secretKey"`
|
|
PathPrefix string `json:"pathPrefix"`
|
|
Insecure bool `json:"insecure"`
|
|
IsDefault bool `json:"isDefault"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
func (s *StorageConfig) Sanitized() map[string]interface{} {
|
|
// AccessKey / SecretKey 在库里是 ENC:<ciphertext> 加密存储,
|
|
// 直接返回会让前端编辑框显示 ENC:xxx 密文。这里统一解密后返回明文,
|
|
// 前端 Secret Key 仍用密码框(默认掩码,点「显示」才展开),
|
|
// 便于管理员核对凭据并「测试连接」。
|
|
plainAK := crypto.DecryptField(s.AccessKey)
|
|
plainSK := crypto.DecryptField(s.SecretKey)
|
|
|
|
return map[string]interface{}{
|
|
"id": s.ID,
|
|
"name": s.Name,
|
|
"storageType": s.StorageType,
|
|
"endpoint": s.Endpoint,
|
|
"bucket": s.Bucket,
|
|
"region": s.Region,
|
|
"accessKey": plainAK,
|
|
"secretKey": plainSK,
|
|
"pathPrefix": s.PathPrefix,
|
|
"insecure": s.Insecure,
|
|
"isDefault": s.IsDefault,
|
|
"hasSecretKey": plainSK != "",
|
|
"createdAt": s.CreatedAt,
|
|
"updatedAt": s.UpdatedAt,
|
|
}
|
|
}
|