diff --git a/accounts/pkg/indexer/index/disk/autoincrement.go b/accounts/pkg/indexer/index/disk/autoincrement.go new file mode 100644 index 0000000000..aef85afb54 --- /dev/null +++ b/accounts/pkg/indexer/index/disk/autoincrement.go @@ -0,0 +1,186 @@ +package disk + +import ( + "errors" + "os" + "path" + "path/filepath" + "reflect" + "sort" + "strconv" + "strings" + + idxerrs "github.com/owncloud/ocis/accounts/pkg/indexer/errors" + + "github.com/owncloud/ocis/accounts/pkg/indexer/index" + "github.com/owncloud/ocis/accounts/pkg/indexer/option" + "github.com/owncloud/ocis/accounts/pkg/indexer/registry" +) + +type Autoincrement struct { + indexBy string + typeName string + filesDir string + indexBaseDir string + indexRootDir string + entity interface{} +} + +// - Creating an autoincrement index has to be thread safe. +// - Validation: autoincrement indexes should only work on integers. + +func init() { + registry.IndexConstructorRegistry["disk"]["autoincrement"] = NewAutoincrementIndex +} + +// NewAutoincrementIndex instantiates a new UniqueIndex instance. Init() should be +// called afterward to ensure correct on-disk structure. +func NewAutoincrementIndex(o ...option.Option) index.Index { + opts := &option.Options{} + for _, opt := range o { + opt(opts) + } + + // validate the field + if opts.Entity == nil { + // return error: entity needed for field validation + } + + k, err := getKind(opts.Entity, opts.IndexBy) + if !isValidKind(k) || err != nil { + panic("invalid index in non-numeric field") + } + + return &Autoincrement{ + 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{"autoincrement", opts.TypeName, opts.IndexBy}, ".")), + } +} + +var ( + validKinds = []reflect.Kind{ + reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + } +) + +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 +} + +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 +} + +func (idx Autoincrement) Add(id, v string) (string, error) { + oldName := filepath.Join(idx.filesDir, id) + newName := filepath.Join(idx.indexRootDir, strconv.Itoa(idx.next())) + 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 +} + +func (idx Autoincrement) Remove(id string, v string) error { + panic("implement me") +} + +func (idx Autoincrement) Update(id, oldV, newV string) error { + panic("implement me") +} + +func (idx Autoincrement) Search(pattern string) ([]string, error) { + panic("implement me") +} + +func (idx Autoincrement) IndexBy() string { + panic("implement me") +} + +func (idx Autoincrement) TypeName() string { + panic("implement me") +} + +func (idx Autoincrement) FilesDir() string { + panic("implement me") +} + +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) + // TODO reflect.FieldByName panics. Recover from it. + // further read: https://blog.golang.org/defer-panic-and-recover + return reflect.Indirect(r).FieldByName(field).Kind(), nil +} + +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 +} + +func (idx Autoincrement) next() int { + files, err := readDir(idx.indexRootDir) + if err != nil { + // hello handle me pls. + } + + if len(files) == 0 { + return 0 + } + + latest, err := strconv.Atoi(path.Base(files[len(files)-1].Name())) // would returning a string be a better interface? + if err != nil { + // handle me daddy + } + return latest + 1 +} diff --git a/accounts/pkg/indexer/index/disk/autoincrement_test.go b/accounts/pkg/indexer/index/disk/autoincrement_test.go new file mode 100644 index 0000000000..c951d5da16 --- /dev/null +++ b/accounts/pkg/indexer/index/disk/autoincrement_test.go @@ -0,0 +1,193 @@ +package disk + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/owncloud/ocis/accounts/pkg/indexer/option" + //. "github.com/owncloud/ocis/accounts/pkg/indexer/test" + "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.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, filepath.Base(oldName), 0) + + oldName, err = i.Add("test-example", "") + assert.NoError(t, err) + assert.Equal(t, filepath.Base(oldName), 1) + + oldName, err = i.Add("test-example", "") + assert.NoError(t, err) + assert.Equal(t, filepath.Base(oldName), 2) + t.Log(oldName) + + _ = os.RemoveAll(tmpDir) + }) + } +} + +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.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 +} diff --git a/accounts/pkg/indexer/option/option.go b/accounts/pkg/indexer/option/option.go index 12e665a71a..80f695128a 100644 --- a/accounts/pkg/indexer/option/option.go +++ b/accounts/pkg/indexer/option/option.go @@ -12,6 +12,7 @@ type Options struct { IndexBaseDir string DataDir string EntityDirName string + Entity interface{} // CS3 options DataURL string @@ -20,6 +21,13 @@ type Options struct { ProviderAddr string } +// WithEntity sets the JWTSecret field. +func WithEntity(val interface{}) Option { + return func(o *Options) { + o.Entity = val + } +} + // WithJWTSecret sets the JWTSecret field. func WithJWTSecret(val string) Option { return func(o *Options) {