update github.com/mennanov/fieldmask-utils@v0.3.3
slim down cofig
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
idxerrs "github.com/owncloud/ocis/ocis-pkg/indexer/errors"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/index"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/option"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/registry"
|
||||
)
|
||||
|
||||
// Autoincrement are fields for an index of type autoincrement.
|
||||
type Autoincrement struct {
|
||||
indexBy string
|
||||
typeName string
|
||||
filesDir string
|
||||
indexBaseDir string
|
||||
indexRootDir string
|
||||
|
||||
bound *option.Bound
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.IndexConstructorRegistry["disk"]["autoincrement"] = NewAutoincrementIndex
|
||||
}
|
||||
|
||||
// NewAutoincrementIndex instantiates a new AutoincrementIndex instance. Init() MUST be called upon instantiation.
|
||||
func NewAutoincrementIndex(o ...option.Option) index.Index {
|
||||
opts := &option.Options{}
|
||||
for _, opt := range o {
|
||||
opt(opts)
|
||||
}
|
||||
|
||||
if opts.Entity == nil {
|
||||
panic("invalid autoincrement index: configured without entity")
|
||||
}
|
||||
|
||||
k, err := getKind(opts.Entity, opts.IndexBy)
|
||||
if !isValidKind(k) || err != nil {
|
||||
panic("invalid autoincrement index: configured on non-numeric field")
|
||||
}
|
||||
|
||||
return &Autoincrement{
|
||||
indexBy: opts.IndexBy,
|
||||
typeName: opts.TypeName,
|
||||
filesDir: opts.FilesDir,
|
||||
bound: opts.Bound,
|
||||
indexBaseDir: path.Join(opts.DataDir, "index.disk"),
|
||||
indexRootDir: path.Join(path.Join(opts.DataDir, "index.disk"), strings.Join([]string{"autoincrement", opts.TypeName, opts.IndexBy}, ".")),
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes an autoincrement index.
|
||||
func (idx *Autoincrement) Init() error {
|
||||
if _, err := os.Stat(idx.filesDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(idx.indexRootDir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup exact lookup by value.
|
||||
func (idx *Autoincrement) Lookup(v string) ([]string, error) {
|
||||
searchPath := path.Join(idx.indexRootDir, v)
|
||||
if err := isValidSymlink(searchPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = &idxerrs.NotFoundErr{TypeName: idx.typeName, Key: idx.indexBy, Value: v}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, err := os.Readlink(searchPath)
|
||||
if err != nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return []string{p}, err
|
||||
}
|
||||
|
||||
// Add a new value to the index.
|
||||
func (idx *Autoincrement) Add(id, v string) (string, error) {
|
||||
nextID, err := idx.next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
oldName := filepath.Join(idx.filesDir, id)
|
||||
var newName string
|
||||
if v == "" {
|
||||
newName = filepath.Join(idx.indexRootDir, strconv.Itoa(nextID))
|
||||
} else {
|
||||
newName = filepath.Join(idx.indexRootDir, v)
|
||||
}
|
||||
err = os.Symlink(oldName, newName)
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return "", &idxerrs.AlreadyExistsErr{TypeName: idx.typeName, Key: idx.indexBy, Value: v}
|
||||
}
|
||||
|
||||
return newName, err
|
||||
}
|
||||
|
||||
// Remove a value v from an index.
|
||||
func (idx *Autoincrement) Remove(id string, v string) error {
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
searchPath := path.Join(idx.indexRootDir, v)
|
||||
return os.Remove(searchPath)
|
||||
}
|
||||
|
||||
// Update index from <oldV> to <newV>.
|
||||
func (idx *Autoincrement) Update(id, oldV, newV string) error {
|
||||
oldPath := path.Join(idx.indexRootDir, oldV)
|
||||
if err := isValidSymlink(oldPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &idxerrs.NotFoundErr{TypeName: idx.TypeName(), Key: idx.IndexBy(), Value: oldV}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
newPath := path.Join(idx.indexRootDir, newV)
|
||||
err := isValidSymlink(newPath)
|
||||
if err == nil {
|
||||
return &idxerrs.AlreadyExistsErr{TypeName: idx.typeName, Key: idx.indexBy, Value: newV}
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
err = os.Rename(oldPath, newPath)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Search allows for glob search on the index.
|
||||
func (idx *Autoincrement) Search(pattern string) ([]string, error) {
|
||||
paths, err := filepath.Glob(path.Join(idx.indexRootDir, pattern))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(paths) == 0 {
|
||||
return nil, &idxerrs.NotFoundErr{TypeName: idx.typeName, Key: idx.indexBy, Value: pattern}
|
||||
}
|
||||
|
||||
res := make([]string, 0)
|
||||
for _, p := range paths {
|
||||
if err := isValidSymlink(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
src, err := os.Readlink(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = append(res, src)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// CaseInsensitive undocumented.
|
||||
func (idx *Autoincrement) CaseInsensitive() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IndexBy undocumented.
|
||||
func (idx *Autoincrement) IndexBy() string {
|
||||
return idx.indexBy
|
||||
}
|
||||
|
||||
// TypeName undocumented.
|
||||
func (idx *Autoincrement) TypeName() string {
|
||||
return idx.typeName
|
||||
}
|
||||
|
||||
// FilesDir undocumented.
|
||||
func (idx *Autoincrement) FilesDir() string {
|
||||
return idx.filesDir
|
||||
}
|
||||
|
||||
func (idx *Autoincrement) next() (int, error) {
|
||||
files, err := readDir(idx.indexRootDir)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return int(idx.bound.Lower), nil
|
||||
}
|
||||
|
||||
latest, err := lastValueFromTree(files)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if int64(latest) < idx.bound.Lower {
|
||||
return int(idx.bound.Lower), nil
|
||||
}
|
||||
|
||||
return latest + 1, nil
|
||||
}
|
||||
|
||||
// Delete deletes the index root folder from the configured storage.
|
||||
func (idx *Autoincrement) Delete() error {
|
||||
return os.RemoveAll(idx.indexRootDir)
|
||||
}
|
||||
|
||||
func lastValueFromTree(files []os.FileInfo) (int, error) {
|
||||
latest, err := strconv.Atoi(path.Base(files[len(files)-1].Name()))
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return latest, nil
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/option"
|
||||
//. "github.com/owncloud/ocis/ocis-pkg/indexer/test"
|
||||
"github.com/owncloud/ocis/accounts/pkg/proto/v0"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsValidKind(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
panics bool
|
||||
name string
|
||||
indexBy string
|
||||
entity struct {
|
||||
Number int
|
||||
Name string
|
||||
NumberFloat float32
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "valid autoincrement index",
|
||||
panics: false,
|
||||
indexBy: "Number",
|
||||
entity: struct {
|
||||
Number int
|
||||
Name string
|
||||
NumberFloat float32
|
||||
}{
|
||||
Name: "tesy-mc-testace",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create autoincrement index on a non-existing field",
|
||||
panics: true,
|
||||
indexBy: "Age",
|
||||
entity: struct {
|
||||
Number int
|
||||
Name string
|
||||
NumberFloat float32
|
||||
}{
|
||||
Name: "tesy-mc-testace",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "attempt to create an autoincrement index with no entity",
|
||||
panics: true,
|
||||
indexBy: "Age",
|
||||
},
|
||||
{
|
||||
name: "create autoincrement index on a non-numeric field",
|
||||
panics: true,
|
||||
indexBy: "Name",
|
||||
entity: struct {
|
||||
Number int
|
||||
Name string
|
||||
NumberFloat float32
|
||||
}{
|
||||
Name: "tesy-mc-testace",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
if scenario.panics {
|
||||
assert.Panics(t, func() {
|
||||
_ = NewAutoincrementIndex(
|
||||
option.WithEntity(scenario.entity),
|
||||
option.WithIndexBy(scenario.indexBy),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
assert.NotPanics(t, func() {
|
||||
_ = NewAutoincrementIndex(
|
||||
option.WithEntity(scenario.entity),
|
||||
option.WithIndexBy(scenario.indexBy),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNext(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
expected int
|
||||
indexBy string
|
||||
entity interface{}
|
||||
}{
|
||||
{
|
||||
name: "get next value",
|
||||
expected: 0,
|
||||
indexBy: "Number",
|
||||
entity: struct {
|
||||
Number int
|
||||
Name string
|
||||
NumberFloat float32
|
||||
}{
|
||||
Name: "tesy-mc-testace",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
tmpDir, err := createTmpDirStr()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(tmpDir, "data"), 0777)
|
||||
assert.NoError(t, err)
|
||||
|
||||
i := NewAutoincrementIndex(
|
||||
option.WithBounds(&option.Bound{
|
||||
Lower: 0,
|
||||
Upper: 0,
|
||||
}),
|
||||
option.WithDataDir(tmpDir),
|
||||
option.WithFilesDir(filepath.Join(tmpDir, "data")),
|
||||
option.WithEntity(scenario.entity),
|
||||
option.WithTypeName("LambdaType"),
|
||||
option.WithIndexBy(scenario.indexBy),
|
||||
)
|
||||
|
||||
err = i.Init()
|
||||
assert.NoError(t, err)
|
||||
|
||||
tmpFile, err := os.Create(filepath.Join(tmpDir, "data", "test-example"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, tmpFile.Close())
|
||||
|
||||
oldName, err := i.Add("test-example", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "0", filepath.Base(oldName))
|
||||
|
||||
oldName, err = i.Add("test-example", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1", filepath.Base(oldName))
|
||||
|
||||
oldName, err = i.Add("test-example", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2", filepath.Base(oldName))
|
||||
t.Log(oldName)
|
||||
|
||||
_ = os.RemoveAll(tmpDir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowerBound(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
expected int
|
||||
indexBy string
|
||||
entity interface{}
|
||||
}{
|
||||
{
|
||||
name: "get next value with a lower bound specified",
|
||||
expected: 0,
|
||||
indexBy: "Number",
|
||||
entity: struct {
|
||||
Number int
|
||||
Name string
|
||||
NumberFloat float32
|
||||
}{
|
||||
Name: "tesy-mc-testace",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
tmpDir, err := createTmpDirStr()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(tmpDir, "data"), 0777)
|
||||
assert.NoError(t, err)
|
||||
|
||||
i := NewAutoincrementIndex(
|
||||
option.WithBounds(&option.Bound{
|
||||
Lower: 1000,
|
||||
}),
|
||||
option.WithDataDir(tmpDir),
|
||||
option.WithFilesDir(filepath.Join(tmpDir, "data")),
|
||||
option.WithEntity(scenario.entity),
|
||||
option.WithTypeName("LambdaType"),
|
||||
option.WithIndexBy(scenario.indexBy),
|
||||
)
|
||||
|
||||
err = i.Init()
|
||||
assert.NoError(t, err)
|
||||
|
||||
tmpFile, err := os.Create(filepath.Join(tmpDir, "data", "test-example"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, tmpFile.Close())
|
||||
|
||||
oldName, err := i.Add("test-example", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1000", filepath.Base(oldName))
|
||||
|
||||
oldName, err = i.Add("test-example", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1001", filepath.Base(oldName))
|
||||
|
||||
oldName, err = i.Add("test-example", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1002", filepath.Base(oldName))
|
||||
t.Log(oldName)
|
||||
|
||||
_ = os.RemoveAll(tmpDir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
tmpDir, err := createTmpDirStr()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(tmpDir, "data"), 0777)
|
||||
assert.NoError(t, err)
|
||||
|
||||
tmpFile, err := os.Create(filepath.Join(tmpDir, "data", "test-example"))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, tmpFile.Close())
|
||||
|
||||
i := NewAutoincrementIndex(
|
||||
option.WithBounds(&option.Bound{
|
||||
Lower: 0,
|
||||
Upper: 0,
|
||||
}),
|
||||
option.WithDataDir(tmpDir),
|
||||
option.WithFilesDir(filepath.Join(tmpDir, "data")),
|
||||
option.WithEntity(&proto.Account{}),
|
||||
option.WithTypeName("owncloud.Account"),
|
||||
option.WithIndexBy("UidNumber"),
|
||||
)
|
||||
|
||||
err = i.Init()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = i.Add("test-example", "")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAdd(b *testing.B) {
|
||||
tmpDir, err := createTmpDirStr()
|
||||
assert.NoError(b, err)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(tmpDir, "data"), 0777)
|
||||
assert.NoError(b, err)
|
||||
|
||||
tmpFile, err := os.Create(filepath.Join(tmpDir, "data", "test-example"))
|
||||
assert.NoError(b, err)
|
||||
assert.NoError(b, tmpFile.Close())
|
||||
|
||||
i := NewAutoincrementIndex(
|
||||
option.WithBounds(&option.Bound{
|
||||
Lower: 0,
|
||||
Upper: 0,
|
||||
}),
|
||||
option.WithDataDir(tmpDir),
|
||||
option.WithFilesDir(filepath.Join(tmpDir, "data")),
|
||||
option.WithEntity(struct {
|
||||
Number int
|
||||
Name string
|
||||
NumberFloat float32
|
||||
}{}),
|
||||
option.WithTypeName("LambdaType"),
|
||||
option.WithIndexBy("Number"),
|
||||
)
|
||||
|
||||
err = i.Init()
|
||||
assert.NoError(b, err)
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err := i.Add("test-example", "")
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
assert.NoError(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func createTmpDirStr() (string, error) {
|
||||
name, err := ioutil.TempDir("/var/tmp", "testfiles-*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
validKinds = []reflect.Kind{
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
}
|
||||
)
|
||||
|
||||
// verifies an autoincrement field kind on the target struct.
|
||||
func isValidKind(k reflect.Kind) bool {
|
||||
for _, v := range validKinds {
|
||||
if k == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getKind(i interface{}, field string) (reflect.Kind, error) {
|
||||
r := reflect.ValueOf(i)
|
||||
return reflect.Indirect(r).FieldByName(field).Kind(), nil
|
||||
}
|
||||
|
||||
// readDir is an implementation of os.ReadDir but with different sorting.
|
||||
func readDir(dirname string) ([]os.FileInfo, error) {
|
||||
f, err := os.Open(dirname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := f.Readdir(-1)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
a, _ := strconv.Atoi(list[i].Name())
|
||||
b, _ := strconv.Atoi(list[j].Name())
|
||||
return a < b
|
||||
})
|
||||
return list, nil
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
idxerrs "github.com/owncloud/ocis/ocis-pkg/indexer/errors"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/index"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/option"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/registry"
|
||||
)
|
||||
|
||||
// NonUnique is able to index an document by a key which might contain non-unique values
|
||||
//
|
||||
// /var/tmp/testfiles-395764020/index.disk/PetByColor/
|
||||
// ├── Brown
|
||||
// │ └── rebef-123 -> /var/tmp/testfiles-395764020/pets/rebef-123
|
||||
// ├── Green
|
||||
// │ ├── goefe-789 -> /var/tmp/testfiles-395764020/pets/goefe-789
|
||||
// │ └── xadaf-189 -> /var/tmp/testfiles-395764020/pets/xadaf-189
|
||||
// └── White
|
||||
// └── wefwe-456 -> /var/tmp/testfiles-395764020/pets/wefwe-456
|
||||
type NonUnique struct {
|
||||
caseInsensitive bool
|
||||
indexBy string
|
||||
typeName string
|
||||
filesDir string
|
||||
indexBaseDir string
|
||||
indexRootDir string
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.IndexConstructorRegistry["disk"]["non_unique"] = NewNonUniqueIndexWithOptions
|
||||
}
|
||||
|
||||
// NewNonUniqueIndexWithOptions instantiates a new UniqueIndex instance. Init() should be
|
||||
// called afterward to ensure correct on-disk structure.
|
||||
func NewNonUniqueIndexWithOptions(o ...option.Option) index.Index {
|
||||
opts := &option.Options{}
|
||||
for _, opt := range o {
|
||||
opt(opts)
|
||||
}
|
||||
|
||||
return &NonUnique{
|
||||
caseInsensitive: opts.CaseInsensitive,
|
||||
indexBy: opts.IndexBy,
|
||||
typeName: opts.TypeName,
|
||||
filesDir: opts.FilesDir,
|
||||
indexBaseDir: path.Join(opts.DataDir, "index.disk"),
|
||||
indexRootDir: path.Join(path.Join(opts.DataDir, "index.disk"), strings.Join([]string{"non_unique", opts.TypeName, opts.IndexBy}, ".")),
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes a unique index.
|
||||
func (idx *NonUnique) Init() error {
|
||||
if _, err := os.Stat(idx.filesDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(idx.indexRootDir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup exact lookup by value.
|
||||
func (idx *NonUnique) Lookup(v string) ([]string, error) {
|
||||
if idx.caseInsensitive {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
searchPath := path.Join(idx.indexRootDir, v)
|
||||
fi, err := ioutil.ReadDir(searchPath)
|
||||
if os.IsNotExist(err) {
|
||||
return []string{}, &idxerrs.NotFoundErr{TypeName: idx.typeName, Key: idx.indexBy, Value: v}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
var ids []string = nil
|
||||
for _, f := range fi {
|
||||
ids = append(ids, f.Name())
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return []string{}, &idxerrs.NotFoundErr{TypeName: idx.typeName, Key: idx.indexBy, Value: v}
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// Add adds a value to the index, returns the path to the root-document
|
||||
func (idx *NonUnique) Add(id, v string) (string, error) {
|
||||
if v == "" {
|
||||
return "", nil
|
||||
}
|
||||
if idx.caseInsensitive {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
oldName := path.Join(idx.filesDir, id)
|
||||
newName := path.Join(idx.indexRootDir, v, id)
|
||||
|
||||
if err := os.MkdirAll(path.Join(idx.indexRootDir, v), 0777); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err := os.Symlink(oldName, newName)
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return "", &idxerrs.AlreadyExistsErr{TypeName: idx.typeName, Key: idx.indexBy, Value: v}
|
||||
}
|
||||
|
||||
return newName, err
|
||||
|
||||
}
|
||||
|
||||
// Remove a value v from an index.
|
||||
func (idx *NonUnique) Remove(id string, v string) error {
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
if idx.caseInsensitive {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
res, err := filepath.Glob(path.Join(idx.indexRootDir, "/*/", id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range res {
|
||||
if err := os.Remove(p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove value directory if it is empty
|
||||
valueDir := path.Join(idx.indexRootDir, v)
|
||||
fi, err := ioutil.ReadDir(valueDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(fi) == 0 {
|
||||
if err := os.RemoveAll(valueDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update index from <oldV> to <newV>.
|
||||
func (idx *NonUnique) Update(id, oldV, newV string) (err error) {
|
||||
if idx.caseInsensitive {
|
||||
oldV = strings.ToLower(oldV)
|
||||
newV = strings.ToLower(newV)
|
||||
}
|
||||
oldDir := path.Join(idx.indexRootDir, oldV)
|
||||
oldPath := path.Join(oldDir, id)
|
||||
newDir := path.Join(idx.indexRootDir, newV)
|
||||
newPath := path.Join(newDir, id)
|
||||
|
||||
if _, err = os.Stat(oldPath); os.IsNotExist(err) {
|
||||
return &idxerrs.NotFoundErr{TypeName: idx.typeName, Key: idx.indexBy, Value: oldV}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(newDir, 0777); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Rename(oldPath, newPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
di, err := ioutil.ReadDir(oldDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(di) == 0 {
|
||||
err = os.RemoveAll(oldDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Search allows for glob search on the index.
|
||||
func (idx *NonUnique) Search(pattern string) ([]string, error) {
|
||||
if idx.caseInsensitive {
|
||||
pattern = strings.ToLower(pattern)
|
||||
}
|
||||
paths, err := filepath.Glob(path.Join(idx.indexRootDir, pattern, "*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(paths) == 0 {
|
||||
return nil, &idxerrs.NotFoundErr{TypeName: idx.typeName, Key: idx.indexBy, Value: pattern}
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// CaseInsensitive undocumented.
|
||||
func (idx *NonUnique) CaseInsensitive() bool {
|
||||
return idx.caseInsensitive
|
||||
}
|
||||
|
||||
// IndexBy undocumented.
|
||||
func (idx *NonUnique) IndexBy() string {
|
||||
return idx.indexBy
|
||||
}
|
||||
|
||||
// TypeName undocumented.
|
||||
func (idx *NonUnique) TypeName() string {
|
||||
return idx.typeName
|
||||
}
|
||||
|
||||
// FilesDir undocumented.
|
||||
func (idx *NonUnique) FilesDir() string {
|
||||
return idx.filesDir
|
||||
}
|
||||
|
||||
// Delete deletes the index folder from its storage.
|
||||
func (idx *NonUnique) Delete() error {
|
||||
return os.RemoveAll(idx.indexRootDir)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/errors"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/index"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/option"
|
||||
. "github.com/owncloud/ocis/ocis-pkg/indexer/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNonUniqueIndexAdd(t *testing.T) {
|
||||
sut, dataPath := getNonUniqueIdxSut(t, Pet{}, "Color")
|
||||
|
||||
ids, err := sut.Lookup("Green")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, []string{"goefe-789", "xadaf-189"}, ids)
|
||||
|
||||
ids, err = sut.Lookup("White")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, []string{"wefwe-456"}, ids)
|
||||
|
||||
ids, err = sut.Lookup("Cyan")
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, []string{}, ids)
|
||||
|
||||
_ = os.RemoveAll(dataPath)
|
||||
|
||||
}
|
||||
|
||||
func TestNonUniqueIndexUpdate(t *testing.T) {
|
||||
sut, dataPath := getNonUniqueIdxSut(t, Pet{}, "Color")
|
||||
|
||||
err := sut.Update("goefe-789", "Green", "Black")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = sut.Update("xadaf-189", "Green", "Black")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.DirExists(t, path.Join(dataPath, fmt.Sprintf("index.disk/non_unique.%v.Color/Black", GetTypeFQN(Pet{}))))
|
||||
assert.NoDirExists(t, path.Join(dataPath, fmt.Sprintf("index.disk/non_unique.%v.Color/Green", GetTypeFQN(Pet{}))))
|
||||
|
||||
_ = os.RemoveAll(dataPath)
|
||||
}
|
||||
|
||||
func TestNonUniqueIndexDelete(t *testing.T) {
|
||||
sut, dataPath := getNonUniqueIdxSut(t, Pet{}, "Color")
|
||||
assert.FileExists(t, path.Join(dataPath, fmt.Sprintf("index.disk/non_unique.%v.Color/Green/goefe-789", GetTypeFQN(Pet{}))))
|
||||
|
||||
err := sut.Remove("goefe-789", "Green")
|
||||
assert.NoError(t, err)
|
||||
assert.NoFileExists(t, path.Join(dataPath, fmt.Sprintf("index.disk/non_unique.%v.Color/Green/goefe-789", GetTypeFQN(Pet{}))))
|
||||
assert.FileExists(t, path.Join(dataPath, fmt.Sprintf("index.disk/non_unique.%v.Color/Green/xadaf-189", GetTypeFQN(Pet{}))))
|
||||
|
||||
_ = os.RemoveAll(dataPath)
|
||||
}
|
||||
|
||||
func TestNonUniqueIndexSearch(t *testing.T) {
|
||||
sut, dataPath := getNonUniqueIdxSut(t, Pet{}, "Email")
|
||||
|
||||
res, err := sut.Search("Gr*")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 2)
|
||||
|
||||
assert.Equal(t, "goefe-789", path.Base(res[0]))
|
||||
assert.Equal(t, "xadaf-189", path.Base(res[1]))
|
||||
|
||||
_, err = sut.Search("does-not-exist@example.com")
|
||||
assert.Error(t, err)
|
||||
assert.IsType(t, &errors.NotFoundErr{}, err)
|
||||
|
||||
_ = os.RemoveAll(dataPath)
|
||||
}
|
||||
|
||||
// entity: used to get the fully qualified name for the index root path.
|
||||
func getNonUniqueIdxSut(t *testing.T, entity interface{}, indexBy string) (index.Index, string) {
|
||||
dataPath, _ := WriteIndexTestData(Data, "ID", "")
|
||||
cfg := config.Config{
|
||||
Repo: config.Repo{
|
||||
Disk: config.Disk{
|
||||
Path: dataPath,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sut := NewNonUniqueIndexWithOptions(
|
||||
option.WithTypeName(GetTypeFQN(entity)),
|
||||
option.WithIndexBy(indexBy),
|
||||
option.WithFilesDir(path.Join(cfg.Repo.Disk.Path, "pets")),
|
||||
option.WithDataDir(cfg.Repo.Disk.Path),
|
||||
)
|
||||
err := sut.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, u := range Data["pets"] {
|
||||
pkVal := ValueOf(u, "ID")
|
||||
idxByVal := ValueOf(u, "Color")
|
||||
_, err := sut.Add(pkVal, idxByVal)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return sut, dataPath
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
idxerrs "github.com/owncloud/ocis/ocis-pkg/indexer/errors"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/index"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/option"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/registry"
|
||||
)
|
||||
|
||||
// Unique ensures that only one document of the same type and key-value combination can exist in the index.
|
||||
//
|
||||
// Modeled by creating a indexer-folder per entity and key with symlinks which point to respective documents which contain
|
||||
// the link-filename as value.
|
||||
//
|
||||
// Directory Layout
|
||||
//
|
||||
// /var/data/index.disk/UniqueUserByEmail/
|
||||
// ├── jacky@example.com -> /var/data/users/ewf4ofk-555
|
||||
// ├── jones@example.com -> /var/data/users/rulan54-777
|
||||
// └── mikey@example.com -> /var/data/users/abcdefg-123
|
||||
//
|
||||
// Example user
|
||||
//
|
||||
// {
|
||||
// "Id": "ewf4ofk-555",
|
||||
// "UserName": "jacky",
|
||||
// "Email": "jacky@example.com"
|
||||
// }
|
||||
//
|
||||
type Unique struct {
|
||||
caseInsensitive bool
|
||||
indexBy string
|
||||
typeName string
|
||||
filesDir string
|
||||
indexBaseDir string
|
||||
indexRootDir string
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.IndexConstructorRegistry["disk"]["unique"] = NewUniqueIndexWithOptions
|
||||
}
|
||||
|
||||
// NewUniqueIndexWithOptions instantiates a new UniqueIndex instance. Init() should be
|
||||
// called afterward to ensure correct on-disk structure.
|
||||
func NewUniqueIndexWithOptions(o ...option.Option) index.Index {
|
||||
opts := &option.Options{}
|
||||
for _, opt := range o {
|
||||
opt(opts)
|
||||
}
|
||||
|
||||
return &Unique{
|
||||
caseInsensitive: opts.CaseInsensitive,
|
||||
indexBy: opts.IndexBy,
|
||||
typeName: opts.TypeName,
|
||||
filesDir: opts.FilesDir,
|
||||
indexBaseDir: path.Join(opts.DataDir, "index.disk"),
|
||||
indexRootDir: path.Join(path.Join(opts.DataDir, "index.disk"), strings.Join([]string{"unique", opts.TypeName, opts.IndexBy}, ".")),
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes a unique index.
|
||||
func (idx *Unique) Init() error {
|
||||
if _, err := os.Stat(idx.filesDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(idx.indexRootDir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add adds a value to the index, returns the path to the root-document
|
||||
func (idx *Unique) Add(id, v string) (string, error) {
|
||||
if v == "" {
|
||||
return "", nil
|
||||
}
|
||||
if idx.caseInsensitive {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
oldName := path.Join(idx.filesDir, id)
|
||||
newName := path.Join(idx.indexRootDir, v)
|
||||
err := os.Symlink(oldName, newName)
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return "", &idxerrs.AlreadyExistsErr{TypeName: idx.typeName, Key: idx.indexBy, Value: v}
|
||||
}
|
||||
|
||||
return newName, err
|
||||
}
|
||||
|
||||
// Remove a value v from an index.
|
||||
func (idx *Unique) Remove(id string, v string) (err error) {
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
if idx.caseInsensitive {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
searchPath := path.Join(idx.indexRootDir, v)
|
||||
return os.Remove(searchPath)
|
||||
}
|
||||
|
||||
// Lookup exact lookup by value.
|
||||
func (idx *Unique) Lookup(v string) (resultPath []string, err error) {
|
||||
if idx.caseInsensitive {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
searchPath := path.Join(idx.indexRootDir, v)
|
||||
if err = isValidSymlink(searchPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = &idxerrs.NotFoundErr{TypeName: idx.typeName, Key: idx.indexBy, Value: v}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
p, err := os.Readlink(searchPath)
|
||||
if err != nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return []string{p}, err
|
||||
}
|
||||
|
||||
// Update index from <oldV> to <newV>.
|
||||
func (idx *Unique) Update(id, oldV, newV string) (err error) {
|
||||
if idx.caseInsensitive {
|
||||
oldV = strings.ToLower(oldV)
|
||||
newV = strings.ToLower(newV)
|
||||
}
|
||||
oldPath := path.Join(idx.indexRootDir, oldV)
|
||||
if err = isValidSymlink(oldPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &idxerrs.NotFoundErr{TypeName: idx.TypeName(), Key: idx.IndexBy(), Value: oldV}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newPath := path.Join(idx.indexRootDir, newV)
|
||||
if err = isValidSymlink(newPath); err == nil {
|
||||
return &idxerrs.AlreadyExistsErr{TypeName: idx.typeName, Key: idx.indexBy, Value: newV}
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
err = os.Rename(oldPath, newPath)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Search allows for glob search on the index.
|
||||
func (idx *Unique) Search(pattern string) ([]string, error) {
|
||||
if idx.caseInsensitive {
|
||||
pattern = strings.ToLower(pattern)
|
||||
}
|
||||
paths, err := filepath.Glob(path.Join(idx.indexRootDir, pattern))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(paths) == 0 {
|
||||
return nil, &idxerrs.NotFoundErr{TypeName: idx.typeName, Key: idx.indexBy, Value: pattern}
|
||||
}
|
||||
|
||||
res := make([]string, 0)
|
||||
for _, p := range paths {
|
||||
if err := isValidSymlink(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
src, err := os.Readlink(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = append(res, src)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// CaseInsensitive undocumented.
|
||||
func (idx *Unique) CaseInsensitive() bool {
|
||||
return idx.caseInsensitive
|
||||
}
|
||||
|
||||
// IndexBy undocumented.
|
||||
func (idx *Unique) IndexBy() string {
|
||||
return idx.indexBy
|
||||
}
|
||||
|
||||
// TypeName undocumented.
|
||||
func (idx *Unique) TypeName() string {
|
||||
return idx.typeName
|
||||
}
|
||||
|
||||
// FilesDir undocumented.
|
||||
func (idx *Unique) FilesDir() string {
|
||||
return idx.filesDir
|
||||
}
|
||||
|
||||
func isValidSymlink(path string) (err error) {
|
||||
var symInfo os.FileInfo
|
||||
if symInfo, err = os.Lstat(path); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if symInfo.Mode()&os.ModeSymlink == 0 {
|
||||
err = fmt.Errorf("%s is not a valid symlink (bug/corruption?)", path)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Delete deletes the index folder from its storage.
|
||||
func (idx *Unique) Delete() error {
|
||||
return os.RemoveAll(idx.indexRootDir)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/owncloud/ocis/accounts/pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/errors"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/index"
|
||||
"github.com/owncloud/ocis/ocis-pkg/indexer/option"
|
||||
. "github.com/owncloud/ocis/ocis-pkg/indexer/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUniqueLookupSingleEntry(t *testing.T) {
|
||||
uniq, dataDir := getUniqueIdxSut(t, "Email", User{})
|
||||
filesDir := path.Join(dataDir, "users")
|
||||
|
||||
t.Log("existing lookup")
|
||||
resultPath, err := uniq.Lookup("mikey@example.com")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []string{path.Join(filesDir, "abcdefg-123")}, resultPath)
|
||||
|
||||
t.Log("non-existing lookup")
|
||||
resultPath, err = uniq.Lookup("doesnotExists@example.com")
|
||||
assert.Error(t, err)
|
||||
assert.IsType(t, &errors.NotFoundErr{}, err)
|
||||
assert.Empty(t, resultPath)
|
||||
|
||||
_ = os.RemoveAll(dataDir)
|
||||
|
||||
}
|
||||
|
||||
func TestUniqueUniqueConstraint(t *testing.T) {
|
||||
uniq, dataDir := getUniqueIdxSut(t, "Email", User{})
|
||||
|
||||
_, err := uniq.Add("abcdefg-123", "mikey@example.com")
|
||||
assert.Error(t, err)
|
||||
assert.IsType(t, &errors.AlreadyExistsErr{}, err)
|
||||
|
||||
_ = os.RemoveAll(dataDir)
|
||||
}
|
||||
|
||||
func TestUniqueRemove(t *testing.T) {
|
||||
uniq, dataDir := getUniqueIdxSut(t, "Email", User{})
|
||||
|
||||
err := uniq.Remove("", "mikey@example.com")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = uniq.Lookup("mikey@example.com")
|
||||
assert.Error(t, err)
|
||||
assert.IsType(t, &errors.NotFoundErr{}, err)
|
||||
|
||||
_ = os.RemoveAll(dataDir)
|
||||
}
|
||||
|
||||
func TestUniqueUpdate(t *testing.T) {
|
||||
uniq, dataDir := getUniqueIdxSut(t, "Email", User{})
|
||||
|
||||
t.Log("successful update")
|
||||
err := uniq.Update("", "mikey@example.com", "mikey2@example.com")
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Log("failed update because already exists")
|
||||
err = uniq.Update("", "mikey2@example.com", "mikey2@example.com")
|
||||
assert.Error(t, err)
|
||||
assert.IsType(t, &errors.AlreadyExistsErr{}, err)
|
||||
|
||||
t.Log("failed update because not found")
|
||||
err = uniq.Update("", "nonexisting@example.com", "something2@example.com")
|
||||
assert.Error(t, err)
|
||||
assert.IsType(t, &errors.NotFoundErr{}, err)
|
||||
|
||||
_ = os.RemoveAll(dataDir)
|
||||
}
|
||||
|
||||
func TestUniqueIndexSearch(t *testing.T) {
|
||||
sut, dataDir := getUniqueIdxSut(t, "Email", User{})
|
||||
|
||||
res, err := sut.Search("j*@example.com")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 2)
|
||||
|
||||
assert.Equal(t, "ewf4ofk-555", path.Base(res[0]))
|
||||
assert.Equal(t, "rulan54-777", path.Base(res[1]))
|
||||
|
||||
_, err = sut.Search("does-not-exist@example.com")
|
||||
assert.Error(t, err)
|
||||
assert.IsType(t, &errors.NotFoundErr{}, err)
|
||||
|
||||
_ = os.RemoveAll(dataDir)
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
assert.True(t, errors.IsAlreadyExistsErr(&errors.AlreadyExistsErr{}))
|
||||
assert.True(t, errors.IsNotFoundErr(&errors.NotFoundErr{}))
|
||||
}
|
||||
|
||||
func getUniqueIdxSut(t *testing.T, indexBy string, entityType interface{}) (index.Index, string) {
|
||||
dataPath, _ := WriteIndexTestData(Data, "ID", "")
|
||||
cfg := config.Config{
|
||||
Repo: config.Repo{
|
||||
Disk: config.Disk{
|
||||
Path: dataPath,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sut := NewUniqueIndexWithOptions(
|
||||
option.WithTypeName(GetTypeFQN(entityType)),
|
||||
option.WithIndexBy(indexBy),
|
||||
option.WithFilesDir(path.Join(cfg.Repo.Disk.Path, "users")),
|
||||
option.WithDataDir(cfg.Repo.Disk.Path),
|
||||
)
|
||||
err := sut.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, u := range Data["users"] {
|
||||
pkVal := ValueOf(u, "ID")
|
||||
idxByVal := ValueOf(u, "Email")
|
||||
_, err := sut.Add(pkVal, idxByVal)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return sut, dataPath
|
||||
}
|
||||
Reference in New Issue
Block a user