mirror of
https://github.com/DullJZ/s3-balance.git
synced 2026-09-07 00:46:38 +08:00
Support VirtualHost
This commit is contained in:
@@ -91,6 +91,7 @@ func main() {
|
|||||||
metricsService,
|
metricsService,
|
||||||
cfg.S3API.ProxyMode,
|
cfg.S3API.ProxyMode,
|
||||||
cfg.S3API.AuthRequired,
|
cfg.S3API.AuthRequired,
|
||||||
|
cfg.S3API.VirtualHost,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 注册配置热更新回调
|
// 注册配置热更新回调
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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