mirror of
https://github.com/DullJZ/s3-balance.git
synced 2026-06-27 22:11:22 +08:00
Support VirtualHost
This commit is contained in:
@@ -91,6 +91,7 @@ func main() {
|
||||
metricsService,
|
||||
cfg.S3API.ProxyMode,
|
||||
cfg.S3API.AuthRequired,
|
||||
cfg.S3API.VirtualHost,
|
||||
)
|
||||
|
||||
// 注册配置热更新回调
|
||||
|
||||
@@ -109,14 +109,14 @@ func (h *S3Handler) handleListObjectsForVirtualBucket(w http.ResponseWriter, r *
|
||||
}
|
||||
|
||||
result := ListBucketResult{
|
||||
Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||
Name: bucketName,
|
||||
Prefix: prefix,
|
||||
Marker: marker,
|
||||
MaxKeys: maxKeys,
|
||||
Delimiter: delimiter,
|
||||
IsTruncated: false,
|
||||
Contents: make([]ObjectInfo, 0, len(objects)),
|
||||
Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||
Name: bucketName,
|
||||
Prefix: prefix,
|
||||
Marker: marker,
|
||||
MaxKeys: maxKeys,
|
||||
Delimiter: delimiter,
|
||||
IsTruncated: false,
|
||||
Contents: make([]ObjectInfo, 0, len(objects)),
|
||||
CommonPrefixes: make([]CommonPrefix, 0),
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ func (h *S3Handler) handleUploadPart(w http.ResponseWriter, r *http.Request) {
|
||||
if etag != "" {
|
||||
w.Header().Set("ETag", etag)
|
||||
}
|
||||
|
||||
|
||||
// 更新上传会话的分片数
|
||||
session, err := h.storage.GetUploadSession(uploadID)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
// 读取错误响应体以获取详细信息
|
||||
@@ -294,7 +294,7 @@ func (h *S3Handler) handleListMultipartUploads(w http.ResponseWriter, r *http.Re
|
||||
sessions = sessions[:maxUploads]
|
||||
isTruncated = true
|
||||
}
|
||||
|
||||
|
||||
// 转换会话为Upload格式
|
||||
for _, session := range sessions {
|
||||
allUploads = append(allUploads, Upload{
|
||||
@@ -321,13 +321,13 @@ func (h *S3Handler) handleListMultipartUploads(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// 构建响应
|
||||
result := ListMultipartUploadsResult{
|
||||
Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||
Bucket: bucketName,
|
||||
KeyMarker: keyMarker,
|
||||
Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||
Bucket: bucketName,
|
||||
KeyMarker: keyMarker,
|
||||
UploadIdMarker: uploadIdMarker,
|
||||
MaxUploads: maxUploads,
|
||||
IsTruncated: isTruncated,
|
||||
Uploads: allUploads,
|
||||
MaxUploads: maxUploads,
|
||||
IsTruncated: isTruncated,
|
||||
Uploads: allUploads,
|
||||
}
|
||||
|
||||
// 如果有更多结果,设置下一个标记
|
||||
@@ -510,7 +510,7 @@ func (h *S3Handler) handleCompleteMultipartUpload(w http.ResponseWriter, r *http
|
||||
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))
|
||||
for i, part := range completeReq.Parts {
|
||||
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{
|
||||
Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||
Location: "/" + bucketName + "/" + key, // 返回虚拟存储桶路径
|
||||
Bucket: bucketName, // 返回虚拟存储桶名称
|
||||
Bucket: bucketName, // 返回虚拟存储桶名称
|
||||
Key: key,
|
||||
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)
|
||||
|
||||
|
||||
// 更新存储桶使用量
|
||||
if objectSize > 0 {
|
||||
targetBucket.UpdateUsedSize(objectSize)
|
||||
|
||||
@@ -20,6 +20,7 @@ type S3Handler struct {
|
||||
metrics *metrics.Metrics
|
||||
proxyMode bool
|
||||
authRequired bool
|
||||
virtualHost bool
|
||||
}
|
||||
|
||||
// NewS3Handler 创建新的S3兼容API处理器
|
||||
@@ -33,6 +34,7 @@ func NewS3Handler(
|
||||
metrics *metrics.Metrics,
|
||||
proxyMode bool,
|
||||
authRequired bool,
|
||||
virtualHost bool,
|
||||
) *S3Handler {
|
||||
return &S3Handler{
|
||||
bucketManager: bucketManager,
|
||||
@@ -44,6 +46,7 @@ func NewS3Handler(
|
||||
metrics: metrics,
|
||||
proxyMode: proxyMode,
|
||||
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.Use(h.virtualHostMiddleware)
|
||||
router.Use(h.authMiddleware)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 辅助函数:解析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
internal/api/virtual_host_middleware.go
Normal file
72
internal/api/virtual_host_middleware.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user