mirror of
https://github.com/versity/versitygw.git
synced 2026-01-04 19:13:57 +00:00
feat: adds Location in CompleteMultipartUpload response
Closes #1714 There is a `Location` field in the `CompleteMultipartUpload` result that represents the newly created object URL. This PR adds this property to the `CompleteMultipartUpload` response, generating it dynamically in either host-style or path-style format, depending on the gateway configuration.
This commit is contained in:
@@ -31,12 +31,13 @@ import (
|
||||
)
|
||||
|
||||
type S3ApiController struct {
|
||||
be backend.Backend
|
||||
iam auth.IAMService
|
||||
logger s3log.AuditLogger
|
||||
evSender s3event.S3EventSender
|
||||
mm metrics.Manager
|
||||
readonly bool
|
||||
be backend.Backend
|
||||
iam auth.IAMService
|
||||
logger s3log.AuditLogger
|
||||
evSender s3event.S3EventSender
|
||||
mm metrics.Manager
|
||||
readonly bool
|
||||
virtualDomain string
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -56,14 +57,15 @@ var (
|
||||
xmlhdr = []byte(`<?xml version="1.0" encoding="UTF-8"?>` + "\n")
|
||||
)
|
||||
|
||||
func New(be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, evs s3event.S3EventSender, mm metrics.Manager, readonly bool) S3ApiController {
|
||||
func New(be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, evs s3event.S3EventSender, mm metrics.Manager, readonly bool, virtualDomain string) S3ApiController {
|
||||
return S3ApiController{
|
||||
be: be,
|
||||
iam: iam,
|
||||
logger: logger,
|
||||
evSender: evs,
|
||||
readonly: readonly,
|
||||
mm: mm,
|
||||
be: be,
|
||||
iam: iam,
|
||||
logger: logger,
|
||||
evSender: evs,
|
||||
readonly: readonly,
|
||||
mm: mm,
|
||||
virtualDomain: virtualDomain,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -352,6 +352,10 @@ func (c S3ApiController) CompleteMultipartUpload(ctx *fiber.Ctx) (*Response, err
|
||||
IfMatch: ifMatch,
|
||||
IfNoneMatch: ifNoneMatch,
|
||||
})
|
||||
if err == nil {
|
||||
objUrl := utils.GenerateObjectLocation(ctx, c.virtualDomain, bucket, key)
|
||||
res.Location = &objUrl
|
||||
}
|
||||
return &Response{
|
||||
Data: res,
|
||||
Headers: map[string]*string{
|
||||
|
||||
@@ -536,7 +536,8 @@ func TestS3ApiController_CompleteMultipartUpload(t *testing.T) {
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
Data: s3response.CompleteMultipartUploadResult{
|
||||
ETag: &ETag,
|
||||
ETag: &ETag,
|
||||
Location: utils.GetStringPtr("http://example.com/bucket/object"),
|
||||
},
|
||||
Headers: map[string]*string{
|
||||
"x-amz-version-id": &versionId,
|
||||
|
||||
@@ -30,8 +30,8 @@ type S3ApiRouter struct {
|
||||
WithAdmSrv bool
|
||||
}
|
||||
|
||||
func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, aLogger s3log.AuditLogger, evs s3event.S3EventSender, mm metrics.Manager, readonly bool, region string, root middlewares.RootUserConfig) {
|
||||
ctrl := controllers.New(be, iam, logger, evs, mm, readonly)
|
||||
func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, aLogger s3log.AuditLogger, evs s3event.S3EventSender, mm metrics.Manager, readonly bool, region, virtualDomain string, root middlewares.RootUserConfig) {
|
||||
ctrl := controllers.New(be, iam, logger, evs, mm, readonly, virtualDomain)
|
||||
adminServices := &controllers.Services{
|
||||
Logger: aLogger,
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestS3ApiRouter_Init(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam, nil, nil, nil, nil, false, "us-east-1", middlewares.RootUserConfig{})
|
||||
tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam, nil, nil, nil, nil, false, "us-east-1", "", middlewares.RootUserConfig{})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ func New(
|
||||
app.Use(middlewares.DebugLogger())
|
||||
}
|
||||
|
||||
server.router.Init(app, be, iam, l, adminLogger, evs, mm, server.readonly, region, root)
|
||||
server.router.Init(app, be, iam, l, adminLogger, evs, mm, server.readonly, region, server.virtualDomain, root)
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
@@ -887,3 +887,26 @@ func ValidateVersionId(versionId string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateObjectLocation generates the object location path-styled or host-styled
|
||||
// depending on the gateway configuration
|
||||
func GenerateObjectLocation(ctx *fiber.Ctx, virtualDomain, bucket, object string) string {
|
||||
scheme := ctx.Protocol()
|
||||
host := ctx.Hostname()
|
||||
|
||||
// escape the object name
|
||||
obj := url.PathEscape(object)
|
||||
|
||||
if virtualDomain != "" && strings.Contains(host, virtualDomain) {
|
||||
// the host already contains the bucket name
|
||||
return fmt.Sprintf("%s://%s/%s", scheme, host, obj)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s://%s/%s/%s",
|
||||
scheme,
|
||||
host,
|
||||
bucket,
|
||||
obj,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1631,6 +1631,13 @@ func CompleteMultipartUpload_success(s *S3Conf) error {
|
||||
if getString(res.Key) != obj {
|
||||
return fmt.Errorf("expected object key to be %v, instead got %v", obj, *res.Key)
|
||||
}
|
||||
location := constructObjectLocation(s.endpoint, bucket, obj, s.hostStyle)
|
||||
if res.Location == nil {
|
||||
return fmt.Errorf("expected non nil Location")
|
||||
}
|
||||
if *res.Location != location {
|
||||
return fmt.Errorf("expected Location to be %s, instead got %s", location, *res.Location)
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
resp, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
|
||||
@@ -480,6 +480,28 @@ func listObjects(client *s3.Client, bucket, prefix, delimiter string, maxKeys in
|
||||
return contents, commonPrefixes, nil
|
||||
}
|
||||
|
||||
func constructObjectLocation(endpoint, bucket, object string, hostStyle bool) string {
|
||||
// Normalize endpoint (no trailing slash)
|
||||
endpoint = strings.TrimRight(endpoint, "/")
|
||||
|
||||
if !hostStyle {
|
||||
// Path-style: http://endpoint/bucket/object
|
||||
return fmt.Sprintf("%s/%s/%s", endpoint, bucket, object)
|
||||
}
|
||||
|
||||
// Host-style: http://bucket.endpoint/object
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil || u.Host == "" {
|
||||
// Fallback for raw host:port endpoints (e.g. "127.0.0.1:7070")
|
||||
return fmt.Sprintf("http://%s.%s/%s", bucket, endpoint, object)
|
||||
}
|
||||
|
||||
host := u.Host
|
||||
u.Host = fmt.Sprintf("%s.%s", bucket, host)
|
||||
|
||||
return fmt.Sprintf("%s/%s", u.String(), object)
|
||||
}
|
||||
|
||||
func hasObjNames(objs []types.Object, names []string) bool {
|
||||
if len(objs) != len(names) {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user