diff --git a/cmd/s3-balance/main.go b/cmd/s3-balance/main.go index da0f169..0050862 100644 --- a/cmd/s3-balance/main.go +++ b/cmd/s3-balance/main.go @@ -91,6 +91,7 @@ func main() { metricsService, cfg.S3API.ProxyMode, cfg.S3API.AuthRequired, + cfg.S3API.VirtualHost, ) // 注册配置热更新回调 diff --git a/internal/api/bucket_handler.go b/internal/api/bucket_handler.go index 0fe00ab..246d91a 100644 --- a/internal/api/bucket_handler.go +++ b/internal/api/bucket_handler.go @@ -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), } diff --git a/internal/api/multipart_handler.go b/internal/api/multipart_handler.go index 578be0e..9755c62 100644 --- a/internal/api/multipart_handler.go +++ b/internal/api/multipart_handler.go @@ -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) diff --git a/internal/api/s3_handler.go b/internal/api/s3_handler.go index 000777e..bceb40d 100644 --- a/internal/api/s3_handler.go +++ b/internal/api/s3_handler.go @@ -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) } diff --git a/internal/api/utils.go b/internal/api/utils.go index 5d219e9..0753023 100644 --- a/internal/api/utils.go +++ b/internal/api/utils.go @@ -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 -} diff --git a/internal/api/virtual_host_middleware.go b/internal/api/virtual_host_middleware.go new file mode 100644 index 0000000..0109c27 --- /dev/null +++ b/internal/api/virtual_host_middleware.go @@ -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 +}