diff --git a/backend/backend.go b/backend/backend.go index 0fc431b..faf1029 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -43,6 +43,7 @@ type Backend interface { ListParts(context.Context, *s3.ListPartsInput) (s3response.ListPartsResult, error) UploadPart(context.Context, *s3.UploadPartInput) (etag string, err error) UploadPartCopy(context.Context, *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) + SelectObjectContent(context.Context, *s3.SelectObjectContentInput) (s3response.SelectObjectContentResult, error) PutObject(context.Context, *s3.PutObjectInput) (string, error) HeadObject(context.Context, *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) @@ -100,6 +101,9 @@ func (BackendUnsupported) CreateBucket(context.Context, *s3.CreateBucketInput) e func (BackendUnsupported) DeleteBucket(context.Context, *s3.DeleteBucketInput) error { return s3err.GetAPIError(s3err.ErrNotImplemented) } +func (BackendUnsupported) SelectObjectContent(context.Context, *s3.SelectObjectContentInput) (s3response.SelectObjectContentResult, error) { + return s3response.SelectObjectContentResult{}, s3err.GetAPIError(s3err.ErrNotImplemented) +} func (BackendUnsupported) CreateMultipartUpload(context.Context, *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) diff --git a/s3api/controllers/backend_moq_test.go b/s3api/controllers/backend_moq_test.go index 19c277e..4ace61a 100644 --- a/s3api/controllers/backend_moq_test.go +++ b/s3api/controllers/backend_moq_test.go @@ -97,6 +97,9 @@ var _ backend.Backend = &BackendMock{} // RestoreObjectFunc: func(contextMoqParam context.Context, restoreObjectInput *s3.RestoreObjectInput) error { // panic("mock out the RestoreObject method") // }, +// SelectObjectContentFunc: func(contextMoqParam context.Context, selectObjectContentInput *s3.SelectObjectContentInput) (s3response.SelectObjectContentResult, error) { +// panic("mock out the SelectObjectContent method") +// }, // SetTagsFunc: func(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error { // panic("mock out the SetTags method") // }, @@ -194,6 +197,9 @@ type BackendMock struct { // RestoreObjectFunc mocks the RestoreObject method. RestoreObjectFunc func(contextMoqParam context.Context, restoreObjectInput *s3.RestoreObjectInput) error + // SelectObjectContentFunc mocks the SelectObjectContent method. + SelectObjectContentFunc func(contextMoqParam context.Context, selectObjectContentInput *s3.SelectObjectContentInput) (s3response.SelectObjectContentResult, error) + // SetTagsFunc mocks the SetTags method. SetTagsFunc func(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error @@ -396,6 +402,13 @@ type BackendMock struct { // RestoreObjectInput is the restoreObjectInput argument value. RestoreObjectInput *s3.RestoreObjectInput } + // SelectObjectContent holds details about calls to the SelectObjectContent method. + SelectObjectContent []struct { + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context + // SelectObjectContentInput is the selectObjectContentInput argument value. + SelectObjectContentInput *s3.SelectObjectContentInput + } // SetTags holds details about calls to the SetTags method. SetTags []struct { // ContextMoqParam is the contextMoqParam argument value. @@ -453,6 +466,7 @@ type BackendMock struct { lockPutObjectAcl sync.RWMutex lockRemoveTags sync.RWMutex lockRestoreObject sync.RWMutex + lockSelectObjectContent sync.RWMutex lockSetTags sync.RWMutex lockShutdown sync.RWMutex lockString sync.RWMutex @@ -1380,6 +1394,42 @@ func (mock *BackendMock) RestoreObjectCalls() []struct { return calls } +// SelectObjectContent calls SelectObjectContentFunc. +func (mock *BackendMock) SelectObjectContent(contextMoqParam context.Context, selectObjectContentInput *s3.SelectObjectContentInput) (s3response.SelectObjectContentResult, error) { + if mock.SelectObjectContentFunc == nil { + panic("BackendMock.SelectObjectContentFunc: method is nil but Backend.SelectObjectContent was just called") + } + callInfo := struct { + ContextMoqParam context.Context + SelectObjectContentInput *s3.SelectObjectContentInput + }{ + ContextMoqParam: contextMoqParam, + SelectObjectContentInput: selectObjectContentInput, + } + mock.lockSelectObjectContent.Lock() + mock.calls.SelectObjectContent = append(mock.calls.SelectObjectContent, callInfo) + mock.lockSelectObjectContent.Unlock() + return mock.SelectObjectContentFunc(contextMoqParam, selectObjectContentInput) +} + +// SelectObjectContentCalls gets all the calls that were made to SelectObjectContent. +// Check the length with: +// +// len(mockedBackend.SelectObjectContentCalls()) +func (mock *BackendMock) SelectObjectContentCalls() []struct { + ContextMoqParam context.Context + SelectObjectContentInput *s3.SelectObjectContentInput +} { + var calls []struct { + ContextMoqParam context.Context + SelectObjectContentInput *s3.SelectObjectContentInput + } + mock.lockSelectObjectContent.RLock() + calls = mock.calls.SelectObjectContent + mock.lockSelectObjectContent.RUnlock() + return calls +} + // SetTags calls SetTagsFunc. func (mock *BackendMock) SetTags(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error { if mock.SetTagsFunc == nil { diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index f0348c9..5867e09 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -830,6 +830,34 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { }) } + if ctx.Request().URI().QueryArgs().Has("select") && ctx.Query("select-type") == "2" { + var payload s3response.SelectObjectContentPayload + + if err := xml.Unmarshal(ctx.Body(), &payload); err != nil { + return SendXMLResponse(ctx, nil, s3err.GetAPIError(s3err.ErrMalformedXML), &MetaOpts{ + Logger: c.logger, + Action: "SelectObjectContent", + BucketOwner: parsedAcl.Owner, + }) + } + + if err := auth.VerifyACL(parsedAcl, bucket, access, "WRITE", isRoot); err != nil { + return SendXMLResponse(ctx, nil, err, &MetaOpts{Logger: c.logger, Action: "SelectObjectContent", BucketOwner: parsedAcl.Owner}) + } + + res, err := c.be.SelectObjectContent(ctx.Context(), &s3.SelectObjectContentInput{ + Bucket: &bucket, + Key: &key, + Expression: payload.Expression, + ExpressionType: payload.ExpressionType, + InputSerialization: payload.InputSerialization, + OutputSerialization: payload.OutputSerialization, + RequestProgress: payload.RequestProgress, + ScanRange: payload.ScanRange, + }) + return SendXMLResponse(ctx, res, err, &MetaOpts{Logger: c.logger, Action: "SelectObjectContent", BucketOwner: parsedAcl.Owner}) + } + if uploadId != "" { data := struct { Parts []types.CompletedPart `xml:"Part"` diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index aa536fb..9bbd48e 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -1299,9 +1299,19 @@ func TestS3ApiController_CreateActions(t *testing.T) { CreateMultipartUploadFunc: func(context.Context, *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) { return &s3.CreateMultipartUploadOutput{}, nil }, + SelectObjectContentFunc: func(contextMoqParam context.Context, selectObjectContentInput *s3.SelectObjectContentInput) (s3response.SelectObjectContentResult, error) { + return s3response.SelectObjectContentResult{}, nil + }, }, } + bdy := ` + + string + string + + ` + app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("access", "valid access") ctx.Locals("isRoot", true) @@ -1336,6 +1346,24 @@ func TestS3ApiController_CreateActions(t *testing.T) { wantErr: false, statusCode: 500, }, + { + name: "Select-object-content-invalid-body", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", nil), + }, + wantErr: false, + statusCode: 400, + }, + { + name: "Select-object-content-invalid-body", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", strings.NewReader(bdy)), + }, + wantErr: false, + statusCode: 200, + }, { name: "Complete-multipart-upload-error", app: app, diff --git a/s3response/s3response.go b/s3response/s3response.go index 39a0bd7..f7cc739 100644 --- a/s3response/s3response.go +++ b/s3response/s3response.go @@ -118,3 +118,19 @@ type DeleteObjectsResult struct { Deleted []types.DeletedObject Errors []types.Error } +type SelectObjectContentPayload struct { + Expression *string + ExpressionType types.ExpressionType + RequestProgress *types.RequestProgress + InputSerialization *types.InputSerialization + OutputSerialization *types.OutputSerialization + ScanRange *types.ScanRange +} + +type SelectObjectContentResult struct { + Records *types.RecordsEvent + Stats *types.StatsEvent + Progress *types.ProgressEvent + Cont *string + End *string +}