Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,360 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"github.com/qsfera/server/services/search/pkg/search"
mock "github.com/stretchr/testify/mock"
)
// NewBatchOperator creates a new instance of BatchOperator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewBatchOperator(t interface {
mock.TestingT
Cleanup(func())
}) *BatchOperator {
mock := &BatchOperator{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// BatchOperator is an autogenerated mock type for the BatchOperator type
type BatchOperator struct {
mock.Mock
}
type BatchOperator_Expecter struct {
mock *mock.Mock
}
func (_m *BatchOperator) EXPECT() *BatchOperator_Expecter {
return &BatchOperator_Expecter{mock: &_m.Mock}
}
// Delete provides a mock function for the type BatchOperator
func (_mock *BatchOperator) Delete(id string) error {
ret := _mock.Called(id)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
r0 = returnFunc(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// BatchOperator_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type BatchOperator_Delete_Call struct {
*mock.Call
}
// Delete is a helper method to define mock.On call
// - id string
func (_e *BatchOperator_Expecter) Delete(id interface{}) *BatchOperator_Delete_Call {
return &BatchOperator_Delete_Call{Call: _e.mock.On("Delete", id)}
}
func (_c *BatchOperator_Delete_Call) Run(run func(id string)) *BatchOperator_Delete_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
})
return _c
}
func (_c *BatchOperator_Delete_Call) Return(err error) *BatchOperator_Delete_Call {
_c.Call.Return(err)
return _c
}
func (_c *BatchOperator_Delete_Call) RunAndReturn(run func(id string) error) *BatchOperator_Delete_Call {
_c.Call.Return(run)
return _c
}
// Move provides a mock function for the type BatchOperator
func (_mock *BatchOperator) Move(rootID string, parentID string, location string) error {
ret := _mock.Called(rootID, parentID, location)
if len(ret) == 0 {
panic("no return value specified for Move")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = returnFunc(rootID, parentID, location)
} else {
r0 = ret.Error(0)
}
return r0
}
// BatchOperator_Move_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Move'
type BatchOperator_Move_Call struct {
*mock.Call
}
// Move is a helper method to define mock.On call
// - rootID string
// - parentID string
// - location string
func (_e *BatchOperator_Expecter) Move(rootID interface{}, parentID interface{}, location interface{}) *BatchOperator_Move_Call {
return &BatchOperator_Move_Call{Call: _e.mock.On("Move", rootID, parentID, location)}
}
func (_c *BatchOperator_Move_Call) Run(run func(rootID string, parentID string, location string)) *BatchOperator_Move_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *BatchOperator_Move_Call) Return(err error) *BatchOperator_Move_Call {
_c.Call.Return(err)
return _c
}
func (_c *BatchOperator_Move_Call) RunAndReturn(run func(rootID string, parentID string, location string) error) *BatchOperator_Move_Call {
_c.Call.Return(run)
return _c
}
// Purge provides a mock function for the type BatchOperator
func (_mock *BatchOperator) Purge(id string, onlyDeleted bool) error {
ret := _mock.Called(id, onlyDeleted)
if len(ret) == 0 {
panic("no return value specified for Purge")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = returnFunc(id, onlyDeleted)
} else {
r0 = ret.Error(0)
}
return r0
}
// BatchOperator_Purge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Purge'
type BatchOperator_Purge_Call struct {
*mock.Call
}
// Purge is a helper method to define mock.On call
// - id string
// - onlyDeleted bool
func (_e *BatchOperator_Expecter) Purge(id interface{}, onlyDeleted interface{}) *BatchOperator_Purge_Call {
return &BatchOperator_Purge_Call{Call: _e.mock.On("Purge", id, onlyDeleted)}
}
func (_c *BatchOperator_Purge_Call) Run(run func(id string, onlyDeleted bool)) *BatchOperator_Purge_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 bool
if args[1] != nil {
arg1 = args[1].(bool)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *BatchOperator_Purge_Call) Return(err error) *BatchOperator_Purge_Call {
_c.Call.Return(err)
return _c
}
func (_c *BatchOperator_Purge_Call) RunAndReturn(run func(id string, onlyDeleted bool) error) *BatchOperator_Purge_Call {
_c.Call.Return(run)
return _c
}
// Push provides a mock function for the type BatchOperator
func (_mock *BatchOperator) Push() error {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Push")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func() error); ok {
r0 = returnFunc()
} else {
r0 = ret.Error(0)
}
return r0
}
// BatchOperator_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push'
type BatchOperator_Push_Call struct {
*mock.Call
}
// Push is a helper method to define mock.On call
func (_e *BatchOperator_Expecter) Push() *BatchOperator_Push_Call {
return &BatchOperator_Push_Call{Call: _e.mock.On("Push")}
}
func (_c *BatchOperator_Push_Call) Run(run func()) *BatchOperator_Push_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *BatchOperator_Push_Call) Return(err error) *BatchOperator_Push_Call {
_c.Call.Return(err)
return _c
}
func (_c *BatchOperator_Push_Call) RunAndReturn(run func() error) *BatchOperator_Push_Call {
_c.Call.Return(run)
return _c
}
// Restore provides a mock function for the type BatchOperator
func (_mock *BatchOperator) Restore(id string) error {
ret := _mock.Called(id)
if len(ret) == 0 {
panic("no return value specified for Restore")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
r0 = returnFunc(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// BatchOperator_Restore_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Restore'
type BatchOperator_Restore_Call struct {
*mock.Call
}
// Restore is a helper method to define mock.On call
// - id string
func (_e *BatchOperator_Expecter) Restore(id interface{}) *BatchOperator_Restore_Call {
return &BatchOperator_Restore_Call{Call: _e.mock.On("Restore", id)}
}
func (_c *BatchOperator_Restore_Call) Run(run func(id string)) *BatchOperator_Restore_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
})
return _c
}
func (_c *BatchOperator_Restore_Call) Return(err error) *BatchOperator_Restore_Call {
_c.Call.Return(err)
return _c
}
func (_c *BatchOperator_Restore_Call) RunAndReturn(run func(id string) error) *BatchOperator_Restore_Call {
_c.Call.Return(run)
return _c
}
// Upsert provides a mock function for the type BatchOperator
func (_mock *BatchOperator) Upsert(id string, r search.Resource) error {
ret := _mock.Called(id, r)
if len(ret) == 0 {
panic("no return value specified for Upsert")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string, search.Resource) error); ok {
r0 = returnFunc(id, r)
} else {
r0 = ret.Error(0)
}
return r0
}
// BatchOperator_Upsert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upsert'
type BatchOperator_Upsert_Call struct {
*mock.Call
}
// Upsert is a helper method to define mock.On call
// - id string
// - r search.Resource
func (_e *BatchOperator_Expecter) Upsert(id interface{}, r interface{}) *BatchOperator_Upsert_Call {
return &BatchOperator_Upsert_Call{Call: _e.mock.On("Upsert", id, r)}
}
func (_c *BatchOperator_Upsert_Call) Run(run func(id string, r search.Resource)) *BatchOperator_Upsert_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 search.Resource
if args[1] != nil {
arg1 = args[1].(search.Resource)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *BatchOperator_Upsert_Call) Return(err error) *BatchOperator_Upsert_Call {
_c.Call.Return(err)
return _c
}
func (_c *BatchOperator_Upsert_Call) RunAndReturn(run func(id string, r search.Resource) error) *BatchOperator_Upsert_Call {
_c.Call.Return(run)
return _c
}
@@ -0,0 +1,502 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/qsfera/server/protogen/gen/qsfera/services/search/v0"
"github.com/qsfera/server/services/search/pkg/search"
mock "github.com/stretchr/testify/mock"
)
// NewEngine creates a new instance of Engine. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewEngine(t interface {
mock.TestingT
Cleanup(func())
}) *Engine {
mock := &Engine{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Engine is an autogenerated mock type for the Engine type
type Engine struct {
mock.Mock
}
type Engine_Expecter struct {
mock *mock.Mock
}
func (_m *Engine) EXPECT() *Engine_Expecter {
return &Engine_Expecter{mock: &_m.Mock}
}
// Delete provides a mock function for the type Engine
func (_mock *Engine) Delete(id string) error {
ret := _mock.Called(id)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
r0 = returnFunc(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Engine_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type Engine_Delete_Call struct {
*mock.Call
}
// Delete is a helper method to define mock.On call
// - id string
func (_e *Engine_Expecter) Delete(id interface{}) *Engine_Delete_Call {
return &Engine_Delete_Call{Call: _e.mock.On("Delete", id)}
}
func (_c *Engine_Delete_Call) Run(run func(id string)) *Engine_Delete_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
})
return _c
}
func (_c *Engine_Delete_Call) Return(err error) *Engine_Delete_Call {
_c.Call.Return(err)
return _c
}
func (_c *Engine_Delete_Call) RunAndReturn(run func(id string) error) *Engine_Delete_Call {
_c.Call.Return(run)
return _c
}
// DocCount provides a mock function for the type Engine
func (_mock *Engine) DocCount() (uint64, error) {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for DocCount")
}
var r0 uint64
var r1 error
if returnFunc, ok := ret.Get(0).(func() (uint64, error)); ok {
return returnFunc()
}
if returnFunc, ok := ret.Get(0).(func() uint64); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(uint64)
}
if returnFunc, ok := ret.Get(1).(func() error); ok {
r1 = returnFunc()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Engine_DocCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DocCount'
type Engine_DocCount_Call struct {
*mock.Call
}
// DocCount is a helper method to define mock.On call
func (_e *Engine_Expecter) DocCount() *Engine_DocCount_Call {
return &Engine_DocCount_Call{Call: _e.mock.On("DocCount")}
}
func (_c *Engine_DocCount_Call) Run(run func()) *Engine_DocCount_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Engine_DocCount_Call) Return(v uint64, err error) *Engine_DocCount_Call {
_c.Call.Return(v, err)
return _c
}
func (_c *Engine_DocCount_Call) RunAndReturn(run func() (uint64, error)) *Engine_DocCount_Call {
_c.Call.Return(run)
return _c
}
// Move provides a mock function for the type Engine
func (_mock *Engine) Move(id string, parentid string, target string) error {
ret := _mock.Called(id, parentid, target)
if len(ret) == 0 {
panic("no return value specified for Move")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = returnFunc(id, parentid, target)
} else {
r0 = ret.Error(0)
}
return r0
}
// Engine_Move_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Move'
type Engine_Move_Call struct {
*mock.Call
}
// Move is a helper method to define mock.On call
// - id string
// - parentid string
// - target string
func (_e *Engine_Expecter) Move(id interface{}, parentid interface{}, target interface{}) *Engine_Move_Call {
return &Engine_Move_Call{Call: _e.mock.On("Move", id, parentid, target)}
}
func (_c *Engine_Move_Call) Run(run func(id string, parentid string, target string)) *Engine_Move_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *Engine_Move_Call) Return(err error) *Engine_Move_Call {
_c.Call.Return(err)
return _c
}
func (_c *Engine_Move_Call) RunAndReturn(run func(id string, parentid string, target string) error) *Engine_Move_Call {
_c.Call.Return(run)
return _c
}
// NewBatch provides a mock function for the type Engine
func (_mock *Engine) NewBatch(batchSize int) (search.BatchOperator, error) {
ret := _mock.Called(batchSize)
if len(ret) == 0 {
panic("no return value specified for NewBatch")
}
var r0 search.BatchOperator
var r1 error
if returnFunc, ok := ret.Get(0).(func(int) (search.BatchOperator, error)); ok {
return returnFunc(batchSize)
}
if returnFunc, ok := ret.Get(0).(func(int) search.BatchOperator); ok {
r0 = returnFunc(batchSize)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(search.BatchOperator)
}
}
if returnFunc, ok := ret.Get(1).(func(int) error); ok {
r1 = returnFunc(batchSize)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Engine_NewBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewBatch'
type Engine_NewBatch_Call struct {
*mock.Call
}
// NewBatch is a helper method to define mock.On call
// - batchSize int
func (_e *Engine_Expecter) NewBatch(batchSize interface{}) *Engine_NewBatch_Call {
return &Engine_NewBatch_Call{Call: _e.mock.On("NewBatch", batchSize)}
}
func (_c *Engine_NewBatch_Call) Run(run func(batchSize int)) *Engine_NewBatch_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 int
if args[0] != nil {
arg0 = args[0].(int)
}
run(
arg0,
)
})
return _c
}
func (_c *Engine_NewBatch_Call) Return(batchOperator search.BatchOperator, err error) *Engine_NewBatch_Call {
_c.Call.Return(batchOperator, err)
return _c
}
func (_c *Engine_NewBatch_Call) RunAndReturn(run func(batchSize int) (search.BatchOperator, error)) *Engine_NewBatch_Call {
_c.Call.Return(run)
return _c
}
// Purge provides a mock function for the type Engine
func (_mock *Engine) Purge(id string, onlyDeleted bool) error {
ret := _mock.Called(id, onlyDeleted)
if len(ret) == 0 {
panic("no return value specified for Purge")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string, bool) error); ok {
r0 = returnFunc(id, onlyDeleted)
} else {
r0 = ret.Error(0)
}
return r0
}
// Engine_Purge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Purge'
type Engine_Purge_Call struct {
*mock.Call
}
// Purge is a helper method to define mock.On call
// - id string
// - onlyDeleted bool
func (_e *Engine_Expecter) Purge(id interface{}, onlyDeleted interface{}) *Engine_Purge_Call {
return &Engine_Purge_Call{Call: _e.mock.On("Purge", id, onlyDeleted)}
}
func (_c *Engine_Purge_Call) Run(run func(id string, onlyDeleted bool)) *Engine_Purge_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 bool
if args[1] != nil {
arg1 = args[1].(bool)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Engine_Purge_Call) Return(err error) *Engine_Purge_Call {
_c.Call.Return(err)
return _c
}
func (_c *Engine_Purge_Call) RunAndReturn(run func(id string, onlyDeleted bool) error) *Engine_Purge_Call {
_c.Call.Return(run)
return _c
}
// Restore provides a mock function for the type Engine
func (_mock *Engine) Restore(id string) error {
ret := _mock.Called(id)
if len(ret) == 0 {
panic("no return value specified for Restore")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
r0 = returnFunc(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Engine_Restore_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Restore'
type Engine_Restore_Call struct {
*mock.Call
}
// Restore is a helper method to define mock.On call
// - id string
func (_e *Engine_Expecter) Restore(id interface{}) *Engine_Restore_Call {
return &Engine_Restore_Call{Call: _e.mock.On("Restore", id)}
}
func (_c *Engine_Restore_Call) Run(run func(id string)) *Engine_Restore_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
run(
arg0,
)
})
return _c
}
func (_c *Engine_Restore_Call) Return(err error) *Engine_Restore_Call {
_c.Call.Return(err)
return _c
}
func (_c *Engine_Restore_Call) RunAndReturn(run func(id string) error) *Engine_Restore_Call {
_c.Call.Return(run)
return _c
}
// Search provides a mock function for the type Engine
func (_mock *Engine) Search(ctx context.Context, req *v0.SearchIndexRequest) (*v0.SearchIndexResponse, error) {
ret := _mock.Called(ctx, req)
if len(ret) == 0 {
panic("no return value specified for Search")
}
var r0 *v0.SearchIndexResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.SearchIndexRequest) (*v0.SearchIndexResponse, error)); ok {
return returnFunc(ctx, req)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.SearchIndexRequest) *v0.SearchIndexResponse); ok {
r0 = returnFunc(ctx, req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.SearchIndexResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v0.SearchIndexRequest) error); ok {
r1 = returnFunc(ctx, req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Engine_Search_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Search'
type Engine_Search_Call struct {
*mock.Call
}
// Search is a helper method to define mock.On call
// - ctx context.Context
// - req *v0.SearchIndexRequest
func (_e *Engine_Expecter) Search(ctx interface{}, req interface{}) *Engine_Search_Call {
return &Engine_Search_Call{Call: _e.mock.On("Search", ctx, req)}
}
func (_c *Engine_Search_Call) Run(run func(ctx context.Context, req *v0.SearchIndexRequest)) *Engine_Search_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v0.SearchIndexRequest
if args[1] != nil {
arg1 = args[1].(*v0.SearchIndexRequest)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Engine_Search_Call) Return(searchIndexResponse *v0.SearchIndexResponse, err error) *Engine_Search_Call {
_c.Call.Return(searchIndexResponse, err)
return _c
}
func (_c *Engine_Search_Call) RunAndReturn(run func(ctx context.Context, req *v0.SearchIndexRequest) (*v0.SearchIndexResponse, error)) *Engine_Search_Call {
_c.Call.Return(run)
return _c
}
// Upsert provides a mock function for the type Engine
func (_mock *Engine) Upsert(id string, r search.Resource) error {
ret := _mock.Called(id, r)
if len(ret) == 0 {
panic("no return value specified for Upsert")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string, search.Resource) error); ok {
r0 = returnFunc(id, r)
} else {
r0 = ret.Error(0)
}
return r0
}
// Engine_Upsert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upsert'
type Engine_Upsert_Call struct {
*mock.Call
}
// Upsert is a helper method to define mock.On call
// - id string
// - r search.Resource
func (_e *Engine_Expecter) Upsert(id interface{}, r interface{}) *Engine_Upsert_Call {
return &Engine_Upsert_Call{Call: _e.mock.On("Upsert", id, r)}
}
func (_c *Engine_Upsert_Call) Run(run func(id string, r search.Resource)) *Engine_Upsert_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 search.Resource
if args[1] != nil {
arg1 = args[1].(search.Resource)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Engine_Upsert_Call) Return(err error) *Engine_Upsert_Call {
_c.Call.Return(err)
return _c
}
func (_c *Engine_Upsert_Call) RunAndReturn(run func(id string, r search.Resource) error) *Engine_Upsert_Call {
_c.Call.Return(run)
return _c
}
@@ -0,0 +1,416 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/qsfera/server/protogen/gen/qsfera/services/search/v0"
mock "github.com/stretchr/testify/mock"
)
// NewSearcher creates a new instance of Searcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewSearcher(t interface {
mock.TestingT
Cleanup(func())
}) *Searcher {
mock := &Searcher{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Searcher is an autogenerated mock type for the Searcher type
type Searcher struct {
mock.Mock
}
type Searcher_Expecter struct {
mock *mock.Mock
}
func (_m *Searcher) EXPECT() *Searcher_Expecter {
return &Searcher_Expecter{mock: &_m.Mock}
}
// IndexSpace provides a mock function for the type Searcher
func (_mock *Searcher) IndexSpace(rID *providerv1beta1.StorageSpaceId, forceRescan bool) error {
ret := _mock.Called(rID, forceRescan)
if len(ret) == 0 {
panic("no return value specified for IndexSpace")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(*providerv1beta1.StorageSpaceId, bool) error); ok {
r0 = returnFunc(rID, forceRescan)
} else {
r0 = ret.Error(0)
}
return r0
}
// Searcher_IndexSpace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IndexSpace'
type Searcher_IndexSpace_Call struct {
*mock.Call
}
// IndexSpace is a helper method to define mock.On call
// - rID *providerv1beta1.StorageSpaceId
// - forceRescan bool
func (_e *Searcher_Expecter) IndexSpace(rID interface{}, forceRescan interface{}) *Searcher_IndexSpace_Call {
return &Searcher_IndexSpace_Call{Call: _e.mock.On("IndexSpace", rID, forceRescan)}
}
func (_c *Searcher_IndexSpace_Call) Run(run func(rID *providerv1beta1.StorageSpaceId, forceRescan bool)) *Searcher_IndexSpace_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *providerv1beta1.StorageSpaceId
if args[0] != nil {
arg0 = args[0].(*providerv1beta1.StorageSpaceId)
}
var arg1 bool
if args[1] != nil {
arg1 = args[1].(bool)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Searcher_IndexSpace_Call) Return(err error) *Searcher_IndexSpace_Call {
_c.Call.Return(err)
return _c
}
func (_c *Searcher_IndexSpace_Call) RunAndReturn(run func(rID *providerv1beta1.StorageSpaceId, forceRescan bool) error) *Searcher_IndexSpace_Call {
_c.Call.Return(run)
return _c
}
// MoveItem provides a mock function for the type Searcher
func (_mock *Searcher) MoveItem(ref *providerv1beta1.Reference) {
_mock.Called(ref)
return
}
// Searcher_MoveItem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MoveItem'
type Searcher_MoveItem_Call struct {
*mock.Call
}
// MoveItem is a helper method to define mock.On call
// - ref *providerv1beta1.Reference
func (_e *Searcher_Expecter) MoveItem(ref interface{}) *Searcher_MoveItem_Call {
return &Searcher_MoveItem_Call{Call: _e.mock.On("MoveItem", ref)}
}
func (_c *Searcher_MoveItem_Call) Run(run func(ref *providerv1beta1.Reference)) *Searcher_MoveItem_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *providerv1beta1.Reference
if args[0] != nil {
arg0 = args[0].(*providerv1beta1.Reference)
}
run(
arg0,
)
})
return _c
}
func (_c *Searcher_MoveItem_Call) Return() *Searcher_MoveItem_Call {
_c.Call.Return()
return _c
}
func (_c *Searcher_MoveItem_Call) RunAndReturn(run func(ref *providerv1beta1.Reference)) *Searcher_MoveItem_Call {
_c.Run(run)
return _c
}
// PurgeDeleted provides a mock function for the type Searcher
func (_mock *Searcher) PurgeDeleted(spaceID *providerv1beta1.StorageSpaceId) error {
ret := _mock.Called(spaceID)
if len(ret) == 0 {
panic("no return value specified for PurgeDeleted")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(*providerv1beta1.StorageSpaceId) error); ok {
r0 = returnFunc(spaceID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Searcher_PurgeDeleted_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PurgeDeleted'
type Searcher_PurgeDeleted_Call struct {
*mock.Call
}
// PurgeDeleted is a helper method to define mock.On call
// - spaceID *providerv1beta1.StorageSpaceId
func (_e *Searcher_Expecter) PurgeDeleted(spaceID interface{}) *Searcher_PurgeDeleted_Call {
return &Searcher_PurgeDeleted_Call{Call: _e.mock.On("PurgeDeleted", spaceID)}
}
func (_c *Searcher_PurgeDeleted_Call) Run(run func(spaceID *providerv1beta1.StorageSpaceId)) *Searcher_PurgeDeleted_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *providerv1beta1.StorageSpaceId
if args[0] != nil {
arg0 = args[0].(*providerv1beta1.StorageSpaceId)
}
run(
arg0,
)
})
return _c
}
func (_c *Searcher_PurgeDeleted_Call) Return(err error) *Searcher_PurgeDeleted_Call {
_c.Call.Return(err)
return _c
}
func (_c *Searcher_PurgeDeleted_Call) RunAndReturn(run func(spaceID *providerv1beta1.StorageSpaceId) error) *Searcher_PurgeDeleted_Call {
_c.Call.Return(run)
return _c
}
// PurgeItem provides a mock function for the type Searcher
func (_mock *Searcher) PurgeItem(rID *providerv1beta1.Reference) {
_mock.Called(rID)
return
}
// Searcher_PurgeItem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PurgeItem'
type Searcher_PurgeItem_Call struct {
*mock.Call
}
// PurgeItem is a helper method to define mock.On call
// - rID *providerv1beta1.Reference
func (_e *Searcher_Expecter) PurgeItem(rID interface{}) *Searcher_PurgeItem_Call {
return &Searcher_PurgeItem_Call{Call: _e.mock.On("PurgeItem", rID)}
}
func (_c *Searcher_PurgeItem_Call) Run(run func(rID *providerv1beta1.Reference)) *Searcher_PurgeItem_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *providerv1beta1.Reference
if args[0] != nil {
arg0 = args[0].(*providerv1beta1.Reference)
}
run(
arg0,
)
})
return _c
}
func (_c *Searcher_PurgeItem_Call) Return() *Searcher_PurgeItem_Call {
_c.Call.Return()
return _c
}
func (_c *Searcher_PurgeItem_Call) RunAndReturn(run func(rID *providerv1beta1.Reference)) *Searcher_PurgeItem_Call {
_c.Run(run)
return _c
}
// RestoreItem provides a mock function for the type Searcher
func (_mock *Searcher) RestoreItem(ref *providerv1beta1.Reference) {
_mock.Called(ref)
return
}
// Searcher_RestoreItem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RestoreItem'
type Searcher_RestoreItem_Call struct {
*mock.Call
}
// RestoreItem is a helper method to define mock.On call
// - ref *providerv1beta1.Reference
func (_e *Searcher_Expecter) RestoreItem(ref interface{}) *Searcher_RestoreItem_Call {
return &Searcher_RestoreItem_Call{Call: _e.mock.On("RestoreItem", ref)}
}
func (_c *Searcher_RestoreItem_Call) Run(run func(ref *providerv1beta1.Reference)) *Searcher_RestoreItem_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *providerv1beta1.Reference
if args[0] != nil {
arg0 = args[0].(*providerv1beta1.Reference)
}
run(
arg0,
)
})
return _c
}
func (_c *Searcher_RestoreItem_Call) Return() *Searcher_RestoreItem_Call {
_c.Call.Return()
return _c
}
func (_c *Searcher_RestoreItem_Call) RunAndReturn(run func(ref *providerv1beta1.Reference)) *Searcher_RestoreItem_Call {
_c.Run(run)
return _c
}
// Search provides a mock function for the type Searcher
func (_mock *Searcher) Search(ctx context.Context, req *v0.SearchRequest) (*v0.SearchResponse, error) {
ret := _mock.Called(ctx, req)
if len(ret) == 0 {
panic("no return value specified for Search")
}
var r0 *v0.SearchResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.SearchRequest) (*v0.SearchResponse, error)); ok {
return returnFunc(ctx, req)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.SearchRequest) *v0.SearchResponse); ok {
r0 = returnFunc(ctx, req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.SearchResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v0.SearchRequest) error); ok {
r1 = returnFunc(ctx, req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Searcher_Search_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Search'
type Searcher_Search_Call struct {
*mock.Call
}
// Search is a helper method to define mock.On call
// - ctx context.Context
// - req *v0.SearchRequest
func (_e *Searcher_Expecter) Search(ctx interface{}, req interface{}) *Searcher_Search_Call {
return &Searcher_Search_Call{Call: _e.mock.On("Search", ctx, req)}
}
func (_c *Searcher_Search_Call) Run(run func(ctx context.Context, req *v0.SearchRequest)) *Searcher_Search_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v0.SearchRequest
if args[1] != nil {
arg1 = args[1].(*v0.SearchRequest)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Searcher_Search_Call) Return(searchResponse *v0.SearchResponse, err error) *Searcher_Search_Call {
_c.Call.Return(searchResponse, err)
return _c
}
func (_c *Searcher_Search_Call) RunAndReturn(run func(ctx context.Context, req *v0.SearchRequest) (*v0.SearchResponse, error)) *Searcher_Search_Call {
_c.Call.Return(run)
return _c
}
// TrashItem provides a mock function for the type Searcher
func (_mock *Searcher) TrashItem(rID *providerv1beta1.ResourceId) {
_mock.Called(rID)
return
}
// Searcher_TrashItem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TrashItem'
type Searcher_TrashItem_Call struct {
*mock.Call
}
// TrashItem is a helper method to define mock.On call
// - rID *providerv1beta1.ResourceId
func (_e *Searcher_Expecter) TrashItem(rID interface{}) *Searcher_TrashItem_Call {
return &Searcher_TrashItem_Call{Call: _e.mock.On("TrashItem", rID)}
}
func (_c *Searcher_TrashItem_Call) Run(run func(rID *providerv1beta1.ResourceId)) *Searcher_TrashItem_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *providerv1beta1.ResourceId
if args[0] != nil {
arg0 = args[0].(*providerv1beta1.ResourceId)
}
run(
arg0,
)
})
return _c
}
func (_c *Searcher_TrashItem_Call) Return() *Searcher_TrashItem_Call {
_c.Call.Return()
return _c
}
func (_c *Searcher_TrashItem_Call) RunAndReturn(run func(rID *providerv1beta1.ResourceId)) *Searcher_TrashItem_Call {
_c.Run(run)
return _c
}
// UpsertItem provides a mock function for the type Searcher
func (_mock *Searcher) UpsertItem(ref *providerv1beta1.Reference) {
_mock.Called(ref)
return
}
// Searcher_UpsertItem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpsertItem'
type Searcher_UpsertItem_Call struct {
*mock.Call
}
// UpsertItem is a helper method to define mock.On call
// - ref *providerv1beta1.Reference
func (_e *Searcher_Expecter) UpsertItem(ref interface{}) *Searcher_UpsertItem_Call {
return &Searcher_UpsertItem_Call{Call: _e.mock.On("UpsertItem", ref)}
}
func (_c *Searcher_UpsertItem_Call) Run(run func(ref *providerv1beta1.Reference)) *Searcher_UpsertItem_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *providerv1beta1.Reference
if args[0] != nil {
arg0 = args[0].(*providerv1beta1.Reference)
}
run(
arg0,
)
})
return _c
}
func (_c *Searcher_UpsertItem_Call) Return() *Searcher_UpsertItem_Call {
_c.Call.Return()
return _c
}
func (_c *Searcher_UpsertItem_Call) RunAndReturn(run func(ref *providerv1beta1.Reference)) *Searcher_UpsertItem_Call {
_c.Run(run)
return _c
}
+212
View File
@@ -0,0 +1,212 @@
package search
import (
"context"
"errors"
"fmt"
"regexp"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/qsfera/server/pkg/log"
searchmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/search/v0"
searchService "github.com/qsfera/server/protogen/gen/qsfera/services/search/v0"
"github.com/qsfera/server/services/search/pkg/content"
)
var scopeRegex = regexp.MustCompile(`scope:\s*([^" "\n\r]*)`)
// Engine is the interface to the search engine
type Engine interface {
Search(ctx context.Context, req *searchService.SearchIndexRequest) (*searchService.SearchIndexResponse, error)
DocCount() (uint64, error)
Upsert(id string, r Resource) error
Move(id string, parentid string, target string) error
Delete(id string) error
Restore(id string) error
Purge(id string, onlyDeleted bool) error
NewBatch(batchSize int) (BatchOperator, error)
}
type BatchOperator interface {
Upsert(id string, r Resource) error
Move(rootID, parentID, location string) error
Delete(id string) error
Restore(id string) error
Purge(id string, onlyDeleted bool) error
Push() error
}
// Resource is the entity that is stored in the index.
type Resource struct {
content.Document
ID string
RootID string
Path string
ParentID string
Type uint64
Deleted bool
Hidden bool
}
// ResolveReference makes sure the path is relative to the space root
func ResolveReference(ctx context.Context, ref *provider.Reference, ri *provider.ResourceInfo, gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) (*provider.Reference, error) {
if ref.GetResourceId().GetOpaqueId() == ref.GetResourceId().GetSpaceId() {
return ref, nil
}
gatewayClient, err := gatewaySelector.Next()
if err != nil {
return nil, err
}
gpRes, err := gatewayClient.GetPath(ctx, &provider.GetPathRequest{
ResourceId: ri.GetId(),
})
if err != nil || gpRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
return nil, err
}
return &provider.Reference{
ResourceId: &provider.ResourceId{
StorageId: ref.GetResourceId().GetStorageId(),
SpaceId: ref.GetResourceId().GetSpaceId(),
OpaqueId: ref.GetResourceId().GetSpaceId(),
},
Path: utils.MakeRelativePath(gpRes.GetPath()),
}, nil
}
type matchArray []*searchmsg.Match
func (ma matchArray) Len() int {
return len(ma)
}
func (ma matchArray) Swap(i, j int) {
ma[i], ma[j] = ma[j], ma[i]
}
func (ma matchArray) Less(i, j int) bool {
return ma[i].GetScore() > ma[j].GetScore()
}
func logDocCount(engine Engine, logger log.Logger) {
c, err := engine.DocCount()
if err != nil {
logger.Error().Err(err).Msg("error getting document count from the index")
}
logger.Debug().Interface("count", c).Msg("new document count")
}
func getAuthContext(serviceAccountID string, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], secret string, logger log.Logger) (context.Context, error) {
gatewayClient, err := gatewaySelector.Next()
if err != nil {
logger.Error().Err(err).Msg("could not get reva gatewayClient")
return nil, err
}
return utils.GetServiceUserContext(serviceAccountID, gatewayClient, secret)
}
func statResource(ctx context.Context, ref *provider.Reference, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], logger log.Logger) (*provider.StatResponse, error) {
gatewayClient, err := gatewaySelector.Next()
if err != nil {
return nil, err
}
res, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil {
logger.Error().Err(err).Msg("failed to stat the moved resource")
return nil, err
}
switch res.GetStatus().GetCode() {
case rpc.Code_CODE_OK:
return res, nil
case rpc.Code_CODE_NOT_FOUND:
// Resource was moved or deleted in the meantime. ignore.
return nil, err
default:
err := errors.New("failed to stat the moved resource")
logger.Error().Interface("res", res).Msg(err.Error())
return nil, err
}
}
// NOTE: this converts CS3 to WebDAV permissions
// since conversions pkg is reva internal we have no other choice than to duplicate the logic
func convertToWebDAVPermissions(isShared, isMountpoint, isDir bool, p *provider.ResourcePermissions) string {
if p == nil {
return ""
}
var b strings.Builder
if isShared {
fmt.Fprintf(&b, "S")
}
if p.GetListContainer() &&
p.GetListFileVersions() &&
p.GetListRecycle() &&
p.GetStat() &&
p.GetGetPath() &&
p.GetGetQuota() &&
p.GetInitiateFileDownload() {
fmt.Fprintf(&b, "R")
}
if isMountpoint {
fmt.Fprintf(&b, "M")
}
if p.GetDelete() {
fmt.Fprintf(&b, "D")
}
if p.GetInitiateFileUpload() &&
p.GetRestoreFileVersion() &&
p.GetRestoreRecycleItem() {
fmt.Fprintf(&b, "NV")
if !isDir {
fmt.Fprintf(&b, "W")
}
}
if isDir &&
p.GetListContainer() &&
p.GetStat() &&
p.GetCreateContainer() &&
p.GetInitiateFileUpload() {
fmt.Fprintf(&b, "CK")
}
if grants.PermissionsEqual(p, conversions.NewSecureViewerRole().CS3ResourcePermissions()) {
fmt.Fprintf(&b, "X")
}
return b.String()
}
// ParseScope extract a scope value from the query string and returns search, scope strings
func ParseScope(query string) (string, string) {
match := scopeRegex.FindStringSubmatch(query)
if len(match) >= 2 {
cut := match[0]
return strings.TrimSpace(strings.ReplaceAll(query, cut, "")), strings.TrimSpace(match[1])
}
return query, ""
}
// ParseFlags extracts supported flags from the query string and returns the cleaned query and a map of flags
func ParseFlags(query string) (string, []string) {
supportedFlags := []string{"is:favorite"}
flags := []string{}
for _, flag := range supportedFlags {
if strings.Contains(query, flag) {
flags = append(flags, flag)
query = strings.ReplaceAll(query, flag, "")
}
}
return strings.TrimSpace(query), flags
}
@@ -0,0 +1,25 @@
package search_test
import (
"testing"
"github.com/qsfera/server/pkg/registry"
mRegistry "go-micro.dev/v4/registry"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func init() {
r := registry.GetRegistry(registry.Inmemory())
service := registry.BuildGRPCService("qsfera.api.gateway", "", "", "")
service.Nodes = []*mRegistry.Node{{
Address: "any",
}}
_ = r.Register(service)
}
func TestSearch(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Search Suite")
}
@@ -0,0 +1,765 @@
package search
import (
"context"
"fmt"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
sdk "github.com/opencloud-eu/reva/v2/pkg/sdk/common"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/walker"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/qsfera/server/pkg/log"
searchmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/search/v0"
searchsvc "github.com/qsfera/server/protogen/gen/qsfera/services/search/v0"
"github.com/qsfera/server/services/search/pkg/config"
"github.com/qsfera/server/services/search/pkg/content"
"github.com/qsfera/server/services/search/pkg/metrics"
)
const (
_spaceStateTrashed = "trashed"
_spaceTypeMountpoint = "mountpoint"
_spaceTypePersonal = "personal"
_spaceTypeProject = "project"
_spaceTypeGrant = "grant"
_slowQueryDuration = 500 * time.Millisecond
)
// Searcher is the interface to the SearchService
type Searcher interface {
Search(ctx context.Context, req *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error)
IndexSpace(rID *provider.StorageSpaceId, forceRescan bool) error
PurgeDeleted(spaceID *provider.StorageSpaceId) error
TrashItem(rID *provider.ResourceId)
PurgeItem(rID *provider.Reference)
UpsertItem(ref *provider.Reference)
RestoreItem(ref *provider.Reference)
MoveItem(ref *provider.Reference)
}
// Service is responsible for indexing spaces and pass on a search
// to it's underlying engine.
type Service struct {
logger log.Logger
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
engine Engine
extractor content.Extractor
metrics *metrics.Metrics
serviceAccountID string
serviceAccountSecret string
batchSize int
}
var errSkipSpace error
// NewService creates a new Provider instance.
func NewService(gatewaySelector pool.Selectable[gateway.GatewayAPIClient], eng Engine, extractor content.Extractor, metrics *metrics.Metrics, logger log.Logger, cfg *config.Config) *Service {
var s = &Service{
gatewaySelector: gatewaySelector,
engine: eng,
logger: logger,
extractor: extractor,
metrics: metrics,
serviceAccountID: cfg.ServiceAccount.ServiceAccountID,
serviceAccountSecret: cfg.ServiceAccount.ServiceAccountSecret,
batchSize: cfg.BatchSize,
}
return s
}
// Search processes a search request and passes it down to the engine.
func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) {
s.logger.Debug().Str("query", req.Query).Msg("performing a search")
// collect metrics
startTime := time.Now()
success := false
defer func() {
if s.metrics == nil {
return
}
status := "success"
if !success {
status = "error"
}
s.metrics.SearchDuration.WithLabelValues(status).Observe(time.Since(startTime).Seconds())
}()
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
currentUser := revactx.ContextMustGetUser(ctx)
// Handle flags
query, flags := ParseFlags(req.Query)
for _, flag := range flags {
switch flag {
case "is:favorite":
query += " Favorites:\"" + currentUser.GetId().GetOpaqueId() + "\""
}
}
// Extract scope from query if set
query, scope := ParseScope(query)
if query == "" {
return nil, errtypes.BadRequest("empty query provided")
}
req.Query = query
if len(scope) > 0 {
scopedID, err := storagespace.ParseID(scope)
if err != nil {
s.logger.Error().Err(err).Msg("failed to parse scope")
}
// Stat the scope to get the resource id
statRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: &scopedID,
},
FieldMask: &fieldmaskpb.FieldMask{Paths: []string{"space"}},
})
if err != nil {
return nil, err
}
// GetPath the scope to get the full path in the space
gpRes, err := gatewayClient.GetPath(ctx, &provider.GetPathRequest{
ResourceId: statRes.GetInfo().GetId(),
})
if err != nil {
return nil, err
}
req.Ref = &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: statRes.GetInfo().GetSpace().GetRoot().GetStorageId(),
SpaceId: statRes.GetInfo().GetSpace().GetRoot().GetSpaceId(),
OpaqueId: statRes.GetInfo().GetSpace().GetRoot().GetOpaqueId(),
},
Path: gpRes.Path,
}
}
filters := []*provider.ListStorageSpacesRequest_Filter{
{
Type: provider.ListStorageSpacesRequest_Filter_TYPE_USER,
Term: &provider.ListStorageSpacesRequest_Filter_User{User: currentUser.GetId()},
},
{
Type: provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
Term: &provider.ListStorageSpacesRequest_Filter_SpaceType{SpaceType: "+grant"},
},
}
// Get the spaces to search
spaces := []*provider.StorageSpace{}
listSpacesRes, err := gatewayClient.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{Filters: filters})
if err != nil {
s.logger.Error().Err(err).Msg("failed to list the user's storage spaces")
return nil, err
}
for _, space := range listSpacesRes.StorageSpaces {
if utils.ReadPlainFromOpaque(space.Opaque, "trashed") == _spaceStateTrashed {
// Do not consider disabled spaces
continue
}
if space.SpaceType != "mountpoint" && req.Ref != nil && (req.Ref.GetResourceId().GetSpaceId() != space.Root.GetSpaceId()) {
// Do not search (non-mountpoint) spaces that do not match the given scope (if a scope is set)
// We still need the mountpoint in order to map the result paths to the according share
continue
}
spaces = append(spaces, space)
}
mountpointMap := map[string]string{}
for _, space := range spaces {
if space.SpaceType != _spaceTypeMountpoint {
continue
}
opaqueMap := sdk.DecodeOpaqueMap(space.Opaque)
grantSpaceID := storagespace.FormatResourceID(&provider.ResourceId{
StorageId: opaqueMap["grantStorageID"],
SpaceId: opaqueMap["grantSpaceID"],
OpaqueId: opaqueMap["grantOpaqueID"],
})
mountpointMap[grantSpaceID] = space.Id.OpaqueId
}
matches := matchArray{}
total := int32(0)
errg, ctx := errgroup.WithContext(ctx)
work := make(chan *provider.StorageSpace, len(spaces))
results := make(chan *searchsvc.SearchIndexResponse, len(spaces))
// Distribute work
errg.Go(func() error {
defer close(work)
for _, space := range spaces {
select {
case work <- space:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
// Spawn workers that'll concurrently work the queue
numWorkers := 20
if len(spaces) < numWorkers {
numWorkers = len(spaces)
}
for i := 0; i < numWorkers; i++ {
errg.Go(func() error {
for space := range work {
res, err := s.searchIndex(ctx, req, space, mountpointMap[space.Id.OpaqueId])
if err != nil && err != errSkipSpace {
return err
}
select {
case results <- res:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
}
// Wait for things to settle down, then close results chan
go func() {
_ = errg.Wait() // error is checked later
close(results)
}()
responses := make([]*searchsvc.SearchIndexResponse, len(spaces))
i := 0
for r := range results {
responses[i] = r
i++
}
if err := errg.Wait(); err != nil {
return nil, err
}
for _, res := range responses {
if res == nil {
continue
}
total += res.TotalMatches
for _, match := range res.Matches {
matches = append(matches, match)
}
}
// compile one sorted list of matches from all spaces and apply the limit if needed
sort.Sort(matches)
limit := req.PageSize
if limit == 0 {
limit = 200
}
if int32(len(matches)) > limit && limit != -1 {
matches = matches[0:limit]
}
success = true
return &searchsvc.SearchResponse{
Matches: matches,
TotalMatches: total,
}, nil
}
func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest, space *provider.StorageSpace, mountpointID string) (*searchsvc.SearchIndexResponse, error) {
if req.Ref != nil &&
(req.Ref.ResourceId.StorageId != space.Root.StorageId ||
req.Ref.ResourceId.SpaceId != space.Root.SpaceId) {
return nil, errSkipSpace
}
searchRootID := &searchmsg.ResourceID{
StorageId: space.Root.StorageId,
SpaceId: space.Root.SpaceId,
OpaqueId: space.Root.OpaqueId,
}
var (
mountpointRootID *searchmsg.ResourceID
rootName string
permissions *provider.ResourcePermissions
remoteItemId *searchmsg.ResourceID
)
mountpointPrefix := ""
searchPathPrefix := req.Ref.GetPath()
switch space.SpaceType {
case _spaceTypeMountpoint:
return nil, errSkipSpace // mountpoint spaces are only "links" to the shared spaces. we have to search the shared "grant" space instead
case _spaceTypeGrant:
// In case of grant spaces we search the root of the outer space and translate the paths to the according mountpoint
searchRootID.OpaqueId = space.Root.SpaceId
if mountpointID == "" {
s.logger.Warn().Interface("space", space).Msg("could not find mountpoint space for grant space")
return nil, errSkipSpace
}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
serviceCtx, err := getAuthContext(s.serviceAccountID, s.gatewaySelector, s.serviceAccountSecret, s.logger)
if err != nil {
return nil, err
}
gpRes, err := gatewayClient.GetPath(serviceCtx, &provider.GetPathRequest{
ResourceId: space.Root,
})
if err != nil {
s.logger.Error().Err(err).Str("space", space.Id.OpaqueId).Msg("failed to get path for grant space root")
return nil, errSkipSpace
}
if gpRes.Status.Code != rpcv1beta1.Code_CODE_OK {
s.logger.Error().Interface("status", gpRes.Status).Str("space", space.Id.OpaqueId).Msg("failed to get path for grant space root")
return nil, errSkipSpace
}
mountpointPrefix = utils.MakeRelativePath(gpRes.Path)
if searchPathPrefix == "" {
searchPathPrefix = mountpointPrefix
}
sid, spid, oid, err := storagespace.SplitID(mountpointID)
if err != nil {
s.logger.Error().Err(err).Str("space", space.Id.OpaqueId).Str("mountpointId", mountpointID).Msg("invalid mountpoint space id")
return nil, errSkipSpace
}
// exclude the hidden shares
rs, err := gatewayClient.GetReceivedShare(ctx, &collaborationv1beta1.GetReceivedShareRequest{
Ref: &collaborationv1beta1.ShareReference{
Spec: &collaborationv1beta1.ShareReference_Id{
Id: &collaborationv1beta1.ShareId{
OpaqueId: oid,
},
},
},
})
if err != nil {
s.logger.Error().Err(err).Str("space", space.Id.OpaqueId).Str("shareId", oid).Msg("invalid receive share")
}
if rs.GetStatus().GetCode() == rpcv1beta1.Code_CODE_OK && rs.GetShare().GetHidden() {
return nil, errSkipSpace
}
mountpointRootID = &searchmsg.ResourceID{
StorageId: sid,
SpaceId: spid,
OpaqueId: oid,
}
rootName = space.GetRootInfo().GetPath()
permissions = space.GetRootInfo().GetPermissionSet()
remoteItemId = &searchmsg.ResourceID{
StorageId: space.GetRootInfo().GetId().GetStorageId(),
SpaceId: space.GetRootInfo().GetId().GetSpaceId(),
OpaqueId: space.GetRootInfo().GetId().GetOpaqueId(),
}
s.logger.Debug().Interface("grantSpace", space).Interface("mountpointRootId", mountpointRootID).Msg("searching a grant")
case _spaceTypePersonal, _spaceTypeProject:
permissions = space.GetRootInfo().GetPermissionSet()
}
searchRequest := &searchsvc.SearchIndexRequest{
Query: req.Query,
Ref: &searchmsg.Reference{
ResourceId: searchRootID,
Path: searchPathPrefix,
},
PageSize: req.PageSize,
}
start := time.Now()
res, err := s.engine.Search(ctx, searchRequest)
duration := time.Since(start)
if err != nil {
s.logger.Error().Err(err).Str("duration", fmt.Sprint(duration)).Str("space", space.Id.OpaqueId).Msg("failed to search the index")
return nil, err
}
if duration > _slowQueryDuration {
s.logger.Info().Interface("searchRequest", searchRequest).Str("duration", fmt.Sprint(duration)).Str("space", space.Id.OpaqueId).Int("hits", len(res.Matches)).Msg("slow space search")
} else {
s.logger.Debug().Interface("searchRequest", searchRequest).Str("duration", fmt.Sprint(duration)).Str("space", space.Id.OpaqueId).Int("hits", len(res.Matches)).Msg("space search done")
}
matches := make([]*searchmsg.Match, 0, len(res.Matches))
for _, match := range res.Matches {
if mountpointPrefix != "" {
match.Entity.Ref.Path = utils.MakeRelativePath(strings.TrimPrefix(match.Entity.Ref.Path, mountpointPrefix))
}
if mountpointRootID != nil {
match.Entity.Ref.ResourceId = mountpointRootID
}
match.Entity.ShareRootName = rootName
match.Entity.RemoteItemId = remoteItemId
isShared := match.GetEntity().GetRef().GetResourceId().GetSpaceId() == utils.ShareStorageSpaceID
isMountpoint := isShared && match.GetEntity().GetRef().GetPath() == "."
isDir := match.GetEntity().GetMimeType() == "httpd/unix-directory"
match.Entity.Permissions = convertToWebDAVPermissions(isShared, isMountpoint, isDir, permissions)
if req.Ref != nil && searchPathPrefix == "/"+match.Entity.Name {
continue
}
matches = append(matches, match)
}
res.Matches = matches
return res, nil
}
// IndexSpace (re)indexes all resources of a given space.
func (s *Service) IndexSpace(spaceID *provider.StorageSpaceId, forceRescan bool) error {
ownerCtx, err := getAuthContext(s.serviceAccountID, s.gatewaySelector, s.serviceAccountSecret, s.logger)
if err != nil {
return err
}
rootID, err := storagespace.ParseID(spaceID.OpaqueId)
if err != nil {
s.logger.Error().Err(err).Msg("invalid space id")
return err
}
if rootID.StorageId == "" || rootID.SpaceId == "" {
s.logger.Error().Err(err).Msg("invalid space id")
return fmt.Errorf("invalid space id")
}
rootID.OpaqueId = rootID.SpaceId
// Collect metrics
startTime := time.Now()
success := false
defer func() {
if s.metrics == nil {
return
}
status := "success"
if !success {
status = "error"
}
s.metrics.IndexDuration.WithLabelValues(status).Observe(time.Since(startTime).Seconds())
}()
w := walker.NewWalker(s.gatewaySelector)
batch, err := s.engine.NewBatch(s.batchSize)
if err != nil {
return err
}
defer func() {
if err := batch.Push(); err != nil {
s.logger.Error().Err(err).Msg("failed to end batch")
}
logDocCount(s.engine, s.logger)
}()
err = w.Walk(ownerCtx, &rootID, func(wd string, info *provider.ResourceInfo, err error) error {
if err != nil {
s.logger.Error().Err(err).Msg("error walking the tree")
return err
}
if info == nil {
return nil
}
ref := &provider.Reference{
Path: utils.MakeRelativePath(filepath.Join(wd, info.Path)),
ResourceId: &rootID,
}
s.logger.Debug().Str("path", ref.Path).Msg("Walking tree")
if forceRescan {
s.doUpsertItem(ref, batch)
return nil
}
searchRes, err := s.engine.Search(ownerCtx, &searchsvc.SearchIndexRequest{
Query: "id:" + storagespace.FormatResourceID(info.Id) + ` mtime>=` + utils.TSToTime(info.Mtime).Format(time.RFC3339Nano),
})
if err == nil && len(searchRes.Matches) >= 1 {
if info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
s.logger.Debug().Str("path", ref.Path).Msg("subtree hasn't changed. Skipping.")
return filepath.SkipDir
}
s.logger.Debug().Str("path", ref.Path).Msg("element hasn't changed. Skipping.")
return nil
}
s.doUpsertItem(ref, batch)
return nil
})
if err != nil {
return err
}
success = true
return nil
}
// TrashItem marks the item as deleted.
func (s *Service) TrashItem(rID *provider.ResourceId) {
if err := s.engine.Delete(storagespace.FormatResourceID(rID)); err != nil {
s.logger.Error().Err(err).Interface("Id", rID).Msg("failed to remove item from index")
}
}
func (s *Service) PurgeItem(ref *provider.Reference) {
if ref.Path != "" && ref.Path != "." {
s.logger.Warn().Str("path", ref.Path).Msg("purging an item with a path is not supported")
return
}
err := s.engine.Purge(storagespace.FormatResourceID(ref.ResourceId), false)
if err != nil {
s.logger.Error().Err(err).Interface("Id", ref.ResourceId).Msg("failed to purge item from index")
return
}
s.logger.Info().Interface("Id", ref.ResourceId).Msg("purged item from index")
logDocCount(s.engine, s.logger)
}
func (s *Service) PurgeDeleted(spaceID *provider.StorageSpaceId) error {
if spaceID == nil {
return fmt.Errorf("spaceID must not be nil")
}
rootID, err := storagespace.ParseID(spaceID.OpaqueId)
if err != nil {
s.logger.Error().Err(err).Msg("invalid space id")
return err
}
if rootID.StorageId == "" || rootID.SpaceId == "" {
s.logger.Error().Err(err).Msg("invalid space id")
return fmt.Errorf("invalid space id")
}
rootID.OpaqueId = rootID.SpaceId
if err := s.engine.Purge(storagespace.FormatResourceID(&rootID), true); err != nil {
s.logger.Error().Err(err).Interface("Id", &rootID).Msg("failed to purge deleted items from index")
return err
}
logDocCount(s.engine, s.logger)
return nil
}
// UpsertItem indexes or stores Resource data fields.
func (s *Service) UpsertItem(ref *provider.Reference) {
s.doUpsertItem(ref, nil)
}
// doUpsertItem indexes or stores Resource data fields.
func (s *Service) doUpsertItem(ref *provider.Reference, batch BatchOperator) {
ctx, stat, path := s.resInfo(ref)
if ctx == nil || stat == nil || path == "" {
return
}
doc, err := s.extractor.Extract(ctx, stat.Info)
if err != nil {
s.logger.Error().Err(err).Msg("failed to extract resource content")
return
}
r := Resource{
ID: storagespace.FormatResourceID(stat.Info.Id),
RootID: storagespace.FormatResourceID(&provider.ResourceId{
StorageId: stat.Info.Id.StorageId,
OpaqueId: stat.Info.Id.SpaceId,
SpaceId: stat.Info.Id.SpaceId,
}),
Path: utils.MakeRelativePath(path),
Type: uint64(stat.Info.Type),
Document: doc,
}
r.Hidden = strings.HasPrefix(r.Path, ".")
if parentID := stat.GetInfo().GetParentId(); parentID != nil {
r.ParentID = storagespace.FormatResourceID(parentID)
}
if batch != nil {
err = batch.Upsert(r.ID, r)
} else {
err = s.engine.Upsert(r.ID, r)
}
if err != nil {
s.logger.Error().Err(err).Msg("error adding updating the resource in the index")
} else {
logDocCount(s.engine, s.logger)
}
// determine if metadata needs to be stored in storage as well
metadata := map[string]string{}
addAudioMetadata(metadata, doc.Audio)
addImageMetadata(metadata, doc.Image)
addLocationMetadata(metadata, doc.Location)
addPhotoMetadata(metadata, doc.Photo)
if len(metadata) == 0 {
return
}
s.logger.Trace().Str("name", doc.Name).Interface("metadata", metadata).Msg("Storing metadata")
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
s.logger.Error().Err(err).Msg("could not retrieve client to store metadata")
return
}
resp, err := gatewayClient.SetArbitraryMetadata(ctx, &provider.SetArbitraryMetadataRequest{
Ref: ref,
ArbitraryMetadata: &provider.ArbitraryMetadata{
Metadata: metadata,
},
})
if err != nil || resp.GetStatus().GetCode() != rpc.Code_CODE_OK {
s.logger.Error().Err(err).Int32("status", int32(resp.GetStatus().GetCode())).Msg("error storing metadata")
return
}
}
func addAudioMetadata(metadata map[string]string, audio *libregraph.Audio) {
if audio == nil {
return
}
marshalToStringMap(audio, metadata, "libre.graph.audio.")
}
func addImageMetadata(metadata map[string]string, image *libregraph.Image) {
if image == nil {
return
}
marshalToStringMap(image, metadata, "libre.graph.image.")
}
func addLocationMetadata(metadata map[string]string, location *libregraph.GeoCoordinates) {
if location == nil {
return
}
marshalToStringMap(location, metadata, "libre.graph.location.")
}
func addPhotoMetadata(metadata map[string]string, photo *libregraph.Photo) {
if photo == nil {
return
}
marshalToStringMap(photo, metadata, "libre.graph.photo.")
}
func marshalToStringMap[T libregraph.MappedNullable](source T, target map[string]string, prefix string) {
// ToMap never returns a non-nil error ...
m, _ := source.ToMap()
for k, v := range m {
if v == nil {
continue
}
target[prefix+k] = valueToString(v)
}
}
func valueToString(value any) string {
if value == nil {
return ""
}
switch v := value.(type) {
case *string:
return *v
case *int32:
return strconv.FormatInt(int64(*v), 10)
case *int64:
return strconv.FormatInt(*v, 10)
case *float32:
return strconv.FormatFloat(float64(*v), 'f', -1, 32)
case *float64:
return strconv.FormatFloat(*v, 'f', -1, 64)
case *bool:
return strconv.FormatBool(*v)
case *time.Time:
return v.Format(time.RFC3339)
default:
return fmt.Sprintf("%v", v)
}
}
// RestoreItem makes the item available again.
func (s *Service) RestoreItem(ref *provider.Reference) {
ctx, stat, path := s.resInfo(ref)
if ctx == nil || stat == nil || path == "" {
return
}
if err := s.engine.Restore(storagespace.FormatResourceID(stat.Info.Id)); err != nil {
s.logger.Error().Err(err).Msg("failed to restore the changed resource in the index")
}
}
// MoveItem updates the resource location and all of its necessary fields.
func (s *Service) MoveItem(ref *provider.Reference) {
ctx, stat, path := s.resInfo(ref)
if ctx == nil || stat == nil || path == "" {
return
}
if err := s.engine.Move(storagespace.FormatResourceID(stat.GetInfo().GetId()), storagespace.FormatResourceID(stat.GetInfo().GetParentId()), path); err != nil {
s.logger.Error().Err(err).Msg("failed to move the changed resource in the index")
}
}
func (s *Service) resInfo(ref *provider.Reference) (context.Context, *provider.StatResponse, string) {
ownerCtx, err := getAuthContext(s.serviceAccountID, s.gatewaySelector, s.serviceAccountSecret, s.logger)
if err != nil {
return nil, nil, ""
}
statRes, err := statResource(ownerCtx, ref, s.gatewaySelector, s.logger)
if err != nil {
return nil, nil, ""
}
r, err := ResolveReference(ownerCtx, ref, statRes.GetInfo(), s.gatewaySelector)
if err != nil {
return nil, nil, ""
}
return ownerCtx, statRes, r.GetPath()
}
@@ -0,0 +1,539 @@
package search_test
import (
"context"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
"github.com/qsfera/server/pkg/log"
searchmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/search/v0"
searchsvc "github.com/qsfera/server/protogen/gen/qsfera/services/search/v0"
"github.com/qsfera/server/services/search/pkg/config"
"github.com/qsfera/server/services/search/pkg/content"
contentMocks "github.com/qsfera/server/services/search/pkg/content/mocks"
"github.com/qsfera/server/services/search/pkg/search"
engineMocks "github.com/qsfera/server/services/search/pkg/search/mocks"
)
var _ = Describe("Searchprovider", func() {
var (
s search.Searcher
extractor *contentMocks.Extractor
gatewayClient *cs3mocks.GatewayAPIClient
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
indexClient *engineMocks.Engine
ctx context.Context
logger = log.NewLogger()
user = &userv1beta1.User{
Id: &userv1beta1.UserId{
OpaqueId: "user",
},
}
otherUser = &userv1beta1.User{
Id: &userv1beta1.UserId{
OpaqueId: "otheruser",
},
}
personalSpace = &sprovider.StorageSpace{
Opaque: &typesv1beta1.Opaque{
Map: map[string]*typesv1beta1.OpaqueEntry{
"path": {
Decoder: "plain",
Value: []byte("/foo"),
},
},
},
Id: &sprovider.StorageSpaceId{OpaqueId: "storageid$personalspace!personalspace"},
Root: &sprovider.ResourceId{StorageId: "storageid", SpaceId: "personalspace", OpaqueId: "personalspace"},
Name: "personalspace",
SpaceType: "personal",
}
ri = &sprovider.ResourceInfo{
Id: &sprovider.ResourceId{
StorageId: "storageid",
OpaqueId: "opaqueid",
},
ParentId: &sprovider.ResourceId{
StorageId: "storageid",
OpaqueId: "parentopaqueid",
},
Path: "foo.pdf",
Size: 12345,
Mtime: &typesv1beta1.Timestamp{Seconds: 4000},
}
)
BeforeEach(func() {
pool.RemoveSelector("GatewaySelector" + "qsfera.api.gateway")
gatewayClient = &cs3mocks.GatewayAPIClient{}
gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient](
"GatewaySelector",
"qsfera.api.gateway",
func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient {
return gatewayClient
},
)
ctx = revactx.ContextSetUser(context.Background(), user)
indexClient = &engineMocks.Engine{}
extractor = &contentMocks.Extractor{}
s = search.NewService(gatewaySelector, indexClient, extractor, nil, logger, &config.Config{})
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{
Status: status.NewOK(ctx),
Token: "authtoken",
}, nil)
gatewayClient.On("GetPath", mock.Anything, mock.MatchedBy(func(req *sprovider.GetPathRequest) bool {
return req.ResourceId.OpaqueId == ri.Id.OpaqueId
})).Return(&sprovider.GetPathResponse{
Status: status.NewOK(context.Background()),
Path: ri.Path,
}, nil)
gatewayClient.On("GetReceivedShare", mock.Anything, mock.Anything).Return(&collaborationv1beta1.GetReceivedShareResponse{
Status: status.NewOK(ctx),
}, nil)
indexClient.On("DocCount").Return(uint64(1), nil)
})
Describe("New", func() {
It("returns a new instance", func() {
s := search.NewService(gatewaySelector, indexClient, extractor, nil, logger, &config.Config{})
Expect(s).ToNot(BeNil())
})
})
Describe("IndexSpace", func() {
It("walks the space and indexes all files", func() {
batch := &engineMocks.BatchOperator{}
batch.EXPECT().Push().Return(nil)
gatewayClient.On("GetUserByClaim", mock.Anything, mock.Anything).Return(&userv1beta1.GetUserByClaimResponse{
Status: status.NewOK(context.Background()),
User: user,
}, nil)
extractor.On("Extract", mock.Anything, mock.Anything, mock.Anything).Return(content.Document{}, nil)
indexClient.On("NewBatch", mock.Anything).Return(batch, nil)
batch.On("Upsert", mock.Anything, mock.Anything).Return(nil)
indexClient.On("Search", mock.Anything, mock.Anything).Return(&searchsvc.SearchIndexResponse{}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
Status: status.NewOK(context.Background()),
Info: ri,
}, nil)
err := s.IndexSpace(&sprovider.StorageSpaceId{OpaqueId: "storageid$spaceid!spaceid"}, false)
Expect(err).ShouldNot(HaveOccurred())
})
})
Describe("Search", func() {
It("fails when an empty query is given", func() {
res, err := s.Search(ctx, &searchsvc.SearchRequest{
Query: "",
})
Expect(err).To(HaveOccurred())
Expect(res).To(BeNil())
})
Context("with a personal space", func() {
BeforeEach(func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&sprovider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*sprovider.StorageSpace{personalSpace},
}, nil)
indexClient.On("Search", mock.Anything, mock.Anything).Return(&searchsvc.SearchIndexResponse{
TotalMatches: 1,
Matches: []*searchmsg.Match{
{
Score: 1,
Entity: &searchmsg.Entity{
Ref: &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: personalSpace.Root.StorageId,
SpaceId: personalSpace.Root.SpaceId,
OpaqueId: personalSpace.Root.OpaqueId,
},
Path: "./path/to/Foo.pdf",
},
Id: &searchmsg.ResourceID{
StorageId: personalSpace.Root.StorageId,
OpaqueId: "foo-id",
},
Name: "Foo.pdf",
},
},
},
}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
Status: status.NewOK(context.Background()),
Info: ri,
}, nil)
})
It("does not mess with field-based searches", func() {
_, err := s.Search(ctx, &searchsvc.SearchRequest{
Query: "Size:<10",
})
Expect(err).ToNot(HaveOccurred())
indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
return req.Query == "Size:<10"
}))
})
It("searches the personal user space", func() {
res, err := s.Search(ctx, &searchsvc.SearchRequest{
Query: "foo",
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(res.TotalMatches).To(Equal(int32(1)))
Expect(len(res.Matches)).To(Equal(1))
match := res.Matches[0]
Expect(match.Entity.Id.OpaqueId).To(Equal("foo-id"))
Expect(match.Entity.Name).To(Equal("Foo.pdf"))
Expect(match.Entity.Ref.ResourceId.OpaqueId).To(Equal(personalSpace.Root.OpaqueId))
Expect(match.Entity.Ref.Path).To(Equal("./path/to/Foo.pdf"))
})
})
Context("with a personal space with a filter", func() {
BeforeEach(func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&sprovider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*sprovider.StorageSpace{personalSpace},
}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
Status: status.NewOK(context.Background()),
Info: &sprovider.ResourceInfo{
Space: &sprovider.StorageSpace{Root: &sprovider.ResourceId{
StorageId: "storageid",
SpaceId: "personalspace",
OpaqueId: "personalspace",
}},
},
}, nil)
gatewayClient.On("GetPath", mock.Anything, mock.Anything).Return(&sprovider.GetPathResponse{
Status: status.NewOK(ctx),
Path: "/path",
}, nil)
indexClient.On("Search", mock.Anything, mock.Anything).Return(&searchsvc.SearchIndexResponse{
TotalMatches: 1,
Matches: []*searchmsg.Match{
{
Score: 1,
Entity: &searchmsg.Entity{
Ref: &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: personalSpace.Root.StorageId,
SpaceId: personalSpace.Root.SpaceId,
OpaqueId: personalSpace.Root.OpaqueId,
},
Path: "./path/to/Foo.pdf",
},
Id: &searchmsg.ResourceID{
StorageId: personalSpace.Root.StorageId,
OpaqueId: "foo-id",
},
Name: "Foo.pdf",
},
},
},
}, nil)
})
It("searches the personal user space", func() {
res, err := s.Search(ctx, &searchsvc.SearchRequest{
Query: "foo scope:storageid$personalspace!personalspace/path",
Ref: &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: "storageid",
SpaceId: "personalspace",
OpaqueId: "personalspace",
},
},
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(res.TotalMatches).To(Equal(int32(1)))
Expect(len(res.Matches)).To(Equal(1))
match := res.Matches[0]
Expect(match.Entity.Id.OpaqueId).To(Equal("foo-id"))
Expect(match.Entity.Name).To(Equal("Foo.pdf"))
Expect(match.Entity.Ref.ResourceId.OpaqueId).To(Equal(personalSpace.Root.OpaqueId))
Expect(match.Entity.Ref.Path).To(Equal("./path/to/Foo.pdf"))
})
})
Context("with received shares", func() {
var (
grantSpace *sprovider.StorageSpace
mountpointSpace *sprovider.StorageSpace
)
BeforeEach(func() {
grantSpace = &sprovider.StorageSpace{
SpaceType: "grant",
Owner: otherUser,
Id: &sprovider.StorageSpaceId{OpaqueId: "storageproviderid$spaceid!otherspacegrant"},
Root: &sprovider.ResourceId{StorageId: "storageproviderid", SpaceId: "spaceid", OpaqueId: "otherspacegrant"},
Name: "grantspace",
RootInfo: &sprovider.ResourceInfo{
Id: &sprovider.ResourceId{
StorageId: "storageid",
SpaceId: "spaceid",
OpaqueId: "opaqueid",
},
},
}
mountpointSpace = &sprovider.StorageSpace{
SpaceType: "mountpoint",
Owner: otherUser,
Id: &sprovider.StorageSpaceId{OpaqueId: "storageproviderid$spaceid!otherspacemountpoint"},
Root: &sprovider.ResourceId{StorageId: "storageproviderid", SpaceId: "spaceid", OpaqueId: "otherspacemountpoint"},
Name: "mountpointspace",
Opaque: &typesv1beta1.Opaque{
Map: map[string]*typesv1beta1.OpaqueEntry{
"grantStorageID": {Decoder: "plain", Value: []byte("storageproviderid")},
"grantSpaceID": {Decoder: "plain", Value: []byte("spaceid")},
"grantOpaqueID": {Decoder: "plain", Value: []byte("otherspacegrant")},
},
},
}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
Status: status.NewOK(context.Background()),
Info: ri,
}, nil)
gatewayClient.On("GetPath", mock.Anything, mock.Anything).Return(&sprovider.GetPathResponse{
Status: status.NewOK(ctx),
Path: "/grant/path",
}, nil)
})
It("searches the received spaces", func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&sprovider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*sprovider.StorageSpace{grantSpace, mountpointSpace},
}, nil)
indexClient.On("Search", mock.Anything, mock.Anything).Return(&searchsvc.SearchIndexResponse{
TotalMatches: 1,
Matches: []*searchmsg.Match{
{
Entity: &searchmsg.Entity{
Ref: &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: grantSpace.Root.StorageId,
SpaceId: grantSpace.Root.SpaceId,
OpaqueId: grantSpace.Root.OpaqueId,
},
Path: "./grant/path/to/Shared.pdf",
},
Id: &searchmsg.ResourceID{
StorageId: grantSpace.Root.StorageId,
OpaqueId: "grant-shared-id",
},
Name: "Shared.pdf",
},
},
},
}, nil)
res, err := s.Search(ctx, &searchsvc.SearchRequest{
Query: "Foo",
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Matches)).To(Equal(1))
match := res.Matches[0]
Expect(match.Entity.Id.OpaqueId).To(Equal("grant-shared-id"))
Expect(match.Entity.Name).To(Equal("Shared.pdf"))
Expect(match.Entity.Ref.ResourceId.OpaqueId).To(Equal(mountpointSpace.Root.OpaqueId))
Expect(match.Entity.Ref.Path).To(Equal("./to/Shared.pdf"))
Expect(match.Entity.RemoteItemId).To(Equal(&searchmsg.ResourceID{
StorageId: grantSpace.RootInfo.Id.StorageId,
SpaceId: grantSpace.RootInfo.Id.SpaceId,
OpaqueId: grantSpace.RootInfo.Id.OpaqueId,
}))
})
Context("when searching both spaces", func() {
BeforeEach(func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&sprovider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*sprovider.StorageSpace{personalSpace, grantSpace, mountpointSpace},
}, nil)
indexClient.On("Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
return req.Ref.ResourceId.OpaqueId == grantSpace.Root.SpaceId &&
req.Ref.ResourceId.SpaceId == grantSpace.Root.SpaceId
})).Return(&searchsvc.SearchIndexResponse{
TotalMatches: 2,
Matches: []*searchmsg.Match{
{
Score: 2,
Entity: &searchmsg.Entity{
Ref: &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: grantSpace.Root.StorageId,
SpaceId: grantSpace.Root.SpaceId,
OpaqueId: grantSpace.Root.OpaqueId,
},
Path: "./grant/path/to/Shared.pdf",
},
Id: &searchmsg.ResourceID{
StorageId: grantSpace.Root.StorageId,
OpaqueId: "grant-shared-id",
},
Name: "Shared.pdf",
},
},
{
Score: 0.01,
Entity: &searchmsg.Entity{
Ref: &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: grantSpace.Root.StorageId,
SpaceId: grantSpace.Root.SpaceId,
OpaqueId: grantSpace.Root.OpaqueId,
},
Path: "./grant/path/to/Irrelevant.pdf",
},
Id: &searchmsg.ResourceID{
StorageId: grantSpace.Root.StorageId,
OpaqueId: "grant-irrelevant-id",
},
Name: "Irrelevant.pdf",
},
},
},
}, nil)
indexClient.On("Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
return req.Ref.ResourceId.OpaqueId == personalSpace.Root.OpaqueId &&
req.Ref.ResourceId.SpaceId == personalSpace.Root.SpaceId
})).Return(&searchsvc.SearchIndexResponse{
TotalMatches: 1,
Matches: []*searchmsg.Match{
{
Score: 1,
Entity: &searchmsg.Entity{
Ref: &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: personalSpace.Root.StorageId,
SpaceId: personalSpace.Root.SpaceId,
OpaqueId: personalSpace.Root.OpaqueId,
},
Path: "./path/to/Foo.pdf",
},
Id: &searchmsg.ResourceID{
StorageId: personalSpace.Root.StorageId,
OpaqueId: "foo-id",
},
Name: "Foo.pdf",
},
},
},
}, nil)
})
It("considers the search Ref parameter", func() {
res, err := s.Search(ctx, &searchsvc.SearchRequest{
Query: "foo",
Ref: &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: "storageid",
SpaceId: "personalspace",
OpaqueId: "personalspace",
},
},
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Matches)).To(Equal(1))
Expect(res.Matches[0].Entity.Id.OpaqueId).To(Equal("foo-id"))
})
It("finds matches in both the personal space AND the grant", func() {
res, err := s.Search(ctx, &searchsvc.SearchRequest{
Query: "foo",
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Matches)).To(Equal(3))
ids := []string{res.Matches[0].Entity.Id.OpaqueId, res.Matches[1].Entity.Id.OpaqueId, res.Matches[2].Entity.Id.OpaqueId}
Expect(ids).To(ConsistOf("foo-id", "grant-shared-id", "grant-irrelevant-id"))
})
It("sorts and limits the combined results from all spaces", func() {
res, err := s.Search(ctx, &searchsvc.SearchRequest{
Query: "foo",
PageSize: 2,
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Matches)).To(Equal(2))
ids := []string{res.Matches[0].Entity.Id.OpaqueId, res.Matches[1].Entity.Id.OpaqueId}
Expect(ids).To(Equal([]string{"grant-shared-id", "foo-id"}))
})
})
})
})
})
var _ = DescribeTable("Parse Scope",
func(pattern, wantSearch, wantScope string) {
gotSearch, gotScope := search.ParseScope(pattern)
Expect(gotSearch).To(Equal(wantSearch))
Expect(gotScope).To(Equal(wantScope))
},
Entry("When scope is at the end of the line",
`+Name:*file* +Tags:&quot;foo&quot; scope:<uuid>/folder/subfolder`,
`+Name:*file* +Tags:&quot;foo&quot;`,
`<uuid>/folder/subfolder`,
),
Entry("When scope is at the end of the line 2",
`+Name:*file* +Tags:&quot;foo&quot; scope:<uuid>/folder`,
`+Name:*file* +Tags:&quot;foo&quot;`,
`<uuid>/folder`,
),
Entry("When scope is at the end of the line 3",
`file scope:<uuid>/folder/subfolder`,
`file`,
`<uuid>/folder/subfolder`,
),
Entry("When scope is at the end of the line with a space",
`+Name:*file* +Tags:&quot;foo&quot; scope: <uuid>/folder/subfolder`,
`+Name:*file* +Tags:&quot;foo&quot;`,
`<uuid>/folder/subfolder`,
),
Entry("When scope is in the middle of the line",
`+Name:*file* scope:<uuid>/folder/subfolder +Tags:&quot;foo&quot;`,
`+Name:*file* +Tags:&quot;foo&quot;`,
`<uuid>/folder/subfolder`,
),
Entry("When scope is at the end of the line",
`scope:<uuid>/folder/subfolder +Name:*file*`,
`+Name:*file*`,
`<uuid>/folder/subfolder`,
),
Entry("When scope is at the begging of the line",
`scope:<uuid>/folder/subfolder file`,
`file`,
`<uuid>/folder/subfolder`,
),
Entry("When no scope",
`+Name:*file* +Tags:&quot;foo&quot;`,
`+Name:*file* +Tags:&quot;foo&quot;`,
``,
),
)