Support VirtualHost

This commit is contained in:
DullJZ
2025-09-29 23:42:52 +08:00
parent 6af9165452
commit 99fec072af
6 changed files with 97 additions and 48 deletions
+1
View File
@@ -91,6 +91,7 @@ func main() {
metricsService, metricsService,
cfg.S3API.ProxyMode, cfg.S3API.ProxyMode,
cfg.S3API.AuthRequired, cfg.S3API.AuthRequired,
cfg.S3API.VirtualHost,
) )
// 注册配置热更新回调 // 注册配置热更新回调
+8 -8
View File
@@ -109,14 +109,14 @@ func (h *S3Handler) handleListObjectsForVirtualBucket(w http.ResponseWriter, r *
} }
result := ListBucketResult{ result := ListBucketResult{
Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/", Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
Name: bucketName, Name: bucketName,
Prefix: prefix, Prefix: prefix,
Marker: marker, Marker: marker,
MaxKeys: maxKeys, MaxKeys: maxKeys,
Delimiter: delimiter, Delimiter: delimiter,
IsTruncated: false, IsTruncated: false,
Contents: make([]ObjectInfo, 0, len(objects)), Contents: make([]ObjectInfo, 0, len(objects)),
CommonPrefixes: make([]CommonPrefix, 0), CommonPrefixes: make([]CommonPrefix, 0),
} }
+12 -12
View File
@@ -127,7 +127,7 @@ func (h *S3Handler) handleUploadPart(w http.ResponseWriter, r *http.Request) {
if etag != "" { if etag != "" {
w.Header().Set("ETag", etag) w.Header().Set("ETag", etag)
} }
// 更新上传会话的分片数 // 更新上传会话的分片数
session, err := h.storage.GetUploadSession(uploadID) session, err := h.storage.GetUploadSession(uploadID)
if err != nil { if err != nil {
@@ -138,7 +138,7 @@ func (h *S3Handler) handleUploadPart(w http.ResponseWriter, r *http.Request) {
log.Printf("Failed to update upload session for uploadID %s: %v", uploadID, err) log.Printf("Failed to update upload session for uploadID %s: %v", uploadID, err)
} }
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} else { } else {
// 读取错误响应体以获取详细信息 // 读取错误响应体以获取详细信息
@@ -294,7 +294,7 @@ func (h *S3Handler) handleListMultipartUploads(w http.ResponseWriter, r *http.Re
sessions = sessions[:maxUploads] sessions = sessions[:maxUploads]
isTruncated = true isTruncated = true
} }
// 转换会话为Upload格式 // 转换会话为Upload格式
for _, session := range sessions { for _, session := range sessions {
allUploads = append(allUploads, Upload{ allUploads = append(allUploads, Upload{
@@ -321,13 +321,13 @@ func (h *S3Handler) handleListMultipartUploads(w http.ResponseWriter, r *http.Re
// 构建响应 // 构建响应
result := ListMultipartUploadsResult{ result := ListMultipartUploadsResult{
Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/", Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
Bucket: bucketName, Bucket: bucketName,
KeyMarker: keyMarker, KeyMarker: keyMarker,
UploadIdMarker: uploadIdMarker, UploadIdMarker: uploadIdMarker,
MaxUploads: maxUploads, MaxUploads: maxUploads,
IsTruncated: isTruncated, IsTruncated: isTruncated,
Uploads: allUploads, Uploads: allUploads,
} }
// 如果有更多结果,设置下一个标记 // 如果有更多结果,设置下一个标记
@@ -510,7 +510,7 @@ func (h *S3Handler) handleCompleteMultipartUpload(w http.ResponseWriter, r *http
return return
} }
log.Printf("CompleteMultipartUpload request - Bucket: %s, Key: %s, UploadID: %s, Parts: %d", log.Printf("CompleteMultipartUpload request - Bucket: %s, Key: %s, UploadID: %s, Parts: %d",
bucketName, key, uploadID, len(completeReq.Parts)) bucketName, key, uploadID, len(completeReq.Parts))
for i, part := range completeReq.Parts { for i, part := range completeReq.Parts {
log.Printf(" Part %d: PartNumber=%d, ETag=%s", i+1, part.PartNumber, part.ETag) log.Printf(" Part %d: PartNumber=%d, ETag=%s", i+1, part.PartNumber, part.ETag)
@@ -544,7 +544,7 @@ func (h *S3Handler) handleCompleteMultipartUpload(w http.ResponseWriter, r *http
result := CompleteMultipartUploadResult{ result := CompleteMultipartUploadResult{
Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/", Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
Location: "/" + bucketName + "/" + key, // 返回虚拟存储桶路径 Location: "/" + bucketName + "/" + key, // 返回虚拟存储桶路径
Bucket: bucketName, // 返回虚拟存储桶名称 Bucket: bucketName, // 返回虚拟存储桶名称
Key: key, Key: key,
ETag: *completeResp.ETag, ETag: *completeResp.ETag,
} }
@@ -565,7 +565,7 @@ func (h *S3Handler) handleCompleteMultipartUpload(w http.ResponseWriter, r *http
// 记录对象元数据(使用实际大小) // 记录对象元数据(使用实际大小)
h.storage.RecordObject(key, targetBucket.Config.Name, objectSize, nil) h.storage.RecordObject(key, targetBucket.Config.Name, objectSize, nil)
// 更新存储桶使用量 // 更新存储桶使用量
if objectSize > 0 { if objectSize > 0 {
targetBucket.UpdateUsedSize(objectSize) targetBucket.UpdateUsedSize(objectSize)
+4
View File
@@ -20,6 +20,7 @@ type S3Handler struct {
metrics *metrics.Metrics metrics *metrics.Metrics
proxyMode bool proxyMode bool
authRequired bool authRequired bool
virtualHost bool
} }
// NewS3Handler 创建新的S3兼容API处理器 // NewS3Handler 创建新的S3兼容API处理器
@@ -33,6 +34,7 @@ func NewS3Handler(
metrics *metrics.Metrics, metrics *metrics.Metrics,
proxyMode bool, proxyMode bool,
authRequired bool, authRequired bool,
virtualHost bool,
) *S3Handler { ) *S3Handler {
return &S3Handler{ return &S3Handler{
bucketManager: bucketManager, bucketManager: bucketManager,
@@ -44,6 +46,7 @@ func NewS3Handler(
metrics: metrics, metrics: metrics,
proxyMode: proxyMode, proxyMode: proxyMode,
authRequired: authRequired, authRequired: authRequired,
virtualHost: virtualHost,
} }
} }
@@ -73,5 +76,6 @@ func (h *S3Handler) RegisterS3Routes(router *mux.Router) {
router.HandleFunc("/{bucket}/{key:.*}", h.handleObjectOperations).Methods("GET", "HEAD", "PUT", "DELETE") router.HandleFunc("/{bucket}/{key:.*}", h.handleObjectOperations).Methods("GET", "HEAD", "PUT", "DELETE")
// 添加认证中间件 // 添加认证中间件
router.Use(h.virtualHostMiddleware)
router.Use(h.authMiddleware) router.Use(h.authMiddleware)
} }
-28
View File
@@ -4,9 +4,7 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/DullJZ/s3-balance/internal/storage" "github.com/DullJZ/s3-balance/internal/storage"
@@ -66,29 +64,3 @@ func (h *S3Handler) setObjectHeaders(w http.ResponseWriter, obj *storage.Object)
w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Type", "application/octet-stream")
} }
} }
// 辅助函数:解析S3路径
func parseS3Path(requestPath string) (bucket string, key string) {
requestPath = strings.TrimPrefix(requestPath, "/")
parts := strings.SplitN(requestPath, "/", 2)
if len(parts) > 0 {
bucket = parts[0]
}
if len(parts) > 1 {
key = parts[1]
}
return bucket, key
}
// 辅助函数:URL编码/解码
func urlEncodePath(p string) string {
return strings.ReplaceAll(url.QueryEscape(p), "+", "%20")
}
func urlDecodePath(p string) string {
decoded, _ := url.QueryUnescape(p)
return decoded
}
+72
View File
@@ -0,0 +1,72 @@
package api
import (
"net"
"net/http"
"strings"
)
// virtualHostMiddleware 支持根据 Host 头推断存储桶名称
func (h *S3Handler) virtualHostMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !h.virtualHost {
next.ServeHTTP(w, r)
return
}
bucketName := h.bucketFromHost(r.Host)
if bucketName == "" {
next.ServeHTTP(w, r)
return
}
// 若路由中已包含桶名称则无需改写
if strings.HasPrefix(r.URL.Path, "/"+bucketName) {
next.ServeHTTP(w, r)
return
}
// 确保桶存在
if _, ok := h.bucketManager.GetBucket(bucketName); !ok {
next.ServeHTTP(w, r)
return
}
newPath := "/" + bucketName
if r.URL.Path != "/" {
newPath += r.URL.Path
}
clone := r.Clone(r.Context())
clone.URL.Path = newPath
clone.RequestURI = newPath
next.ServeHTTP(w, clone)
})
}
func (h *S3Handler) bucketFromHost(host string) string {
if host == "" {
return ""
}
cleanHost := host
if strings.Contains(host, ":") {
hostname, _, err := net.SplitHostPort(host)
if err == nil {
cleanHost = hostname
}
}
parts := strings.Split(cleanHost, ".")
if len(parts) == 0 {
return ""
}
candidate := parts[0]
if candidate == "" {
return ""
}
return candidate
}