可靠性: 完善 R2 备份恢复与对象生命周期

This commit is contained in:
tamakyi
2026-11-05 08:41:55 +08:00
parent 3e7719f200
commit 937e688cfa
32 changed files with 4151 additions and 231 deletions
+128
View File
@@ -0,0 +1,128 @@
package storage
import (
"fmt"
"net/url"
"strings"
"unicode"
)
// ObjectKeyFromCDNURL 将属于 cdnBase 的 HTTPS URL 转成原始 S3 对象 key。
// cdnBase 可省略 scheme,也可带 base path。外站或不属于 base path 的 URL
// 返回 owned=false 且不报错;格式非法或试图伪装路径的 URL 返回错误。
func ObjectKeyFromCDNURL(rawURL, cdnBase string) (key string, owned bool, err error) {
base, err := parseCDNBase(cdnBase)
if err != nil {
return "", false, err
}
parsed, err := parseCDNURL(rawURL, false)
if err != nil {
return "", false, err
}
if !strings.EqualFold(parsed.Host, base.Host) {
return "", false, nil
}
basePath := base.Path
if basePath != "" {
if parsed.Path == basePath {
return "", false, fmt.Errorf("CDN URL does not contain an object key")
}
prefix := basePath + "/"
if !strings.HasPrefix(parsed.Path, prefix) {
return "", false, nil
}
key = strings.TrimPrefix(parsed.Path, prefix)
} else {
key = strings.TrimPrefix(parsed.Path, "/")
}
if key == "" {
return "", false, fmt.Errorf("CDN URL does not contain an object key")
}
// 只去掉 cdnBase 的路径和前导分隔符;不添加或移除 bucket,也不 Clean,
// 因而返回的解码路径仍是实际 S3 key。
return key, true, nil
}
func parseCDNBase(cdnBase string) (*url.URL, error) {
if cdnBase == "" {
return nil, fmt.Errorf("CDN base URL is empty")
}
rawBase := cdnBase
if strings.HasPrefix(rawBase, "//") {
rawBase = "https:" + rawBase
} else if !strings.Contains(rawBase, "://") {
rawBase = "https://" + rawBase
}
return parseCDNURL(rawBase, true)
}
func parseCDNURL(rawURL string, isBase bool) (*url.URL, error) {
if rawURL == "" {
return nil, fmt.Errorf("URL is empty")
}
if strings.Contains(rawURL, `\`) || containsControl(rawURL) {
return nil, fmt.Errorf("URL contains a backslash or control character")
}
parsed, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("parse URL: %w", err)
}
if !parsed.IsAbs() || !strings.EqualFold(parsed.Scheme, "https") || parsed.Host == "" ||
parsed.Hostname() == "" || parsed.Opaque != "" {
return nil, fmt.Errorf("URL must be an absolute HTTPS URL")
}
if parsed.User != nil {
return nil, fmt.Errorf("URL must not contain userinfo")
}
if parsed.RawQuery != "" || parsed.ForceQuery {
return nil, fmt.Errorf("URL must not contain a query")
}
if parsed.Fragment != "" || strings.Contains(rawURL, "#") {
return nil, fmt.Errorf("URL must not contain a fragment")
}
// 必须在 url.Parse 的 Path 解码之外检查 EscapedPath:编码斜杠在 Path 中
// 与普通分隔符无法区分,若放行会导致 CDN 与 S3 对 key 的解释不一致。
escapedPath := strings.ToLower(parsed.EscapedPath())
if strings.Contains(escapedPath, "%2f") || strings.Contains(escapedPath, "%5c") {
return nil, fmt.Errorf("URL path contains an encoded slash or backslash")
}
if strings.Contains(parsed.Path, `\`) || containsControl(parsed.Path) {
return nil, fmt.Errorf("URL path contains a backslash or control character")
}
if isBase && strings.HasSuffix(parsed.Path, "/") {
// 配置中的 CDN base 允许一个末尾斜杠;其余空路径段仍会被拒绝。
parsed.Path = strings.TrimSuffix(parsed.Path, "/")
parsed.RawPath = ""
}
if parsed.Path == "" {
if isBase {
return parsed, nil
}
return nil, fmt.Errorf("URL path is empty")
}
if !strings.HasPrefix(parsed.Path, "/") {
return nil, fmt.Errorf("URL path must be absolute")
}
for _, segment := range strings.Split(strings.TrimPrefix(parsed.Path, "/"), "/") {
if segment == "" {
return nil, fmt.Errorf("URL path contains an empty segment")
}
if segment == "." || segment == ".." {
return nil, fmt.Errorf("URL path contains a dot segment")
}
}
return parsed, nil
}
func containsControl(value string) bool {
for _, r := range value {
if unicode.IsControl(r) {
return true
}
}
return false
}