rename folder extensions -> services
Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/command"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config/defaults"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := command.Execute(defaults.DefaultConfig()); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/logging"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Health is the entrypoint for the health command.
|
||||
func Health(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "health",
|
||||
Usage: "check health status",
|
||||
Category: "info",
|
||||
Before: func(c *cli.Context) error {
|
||||
return parser.ParseConfig(cfg)
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
|
||||
resp, err := http.Get(
|
||||
fmt.Sprintf(
|
||||
"http://%s/healthz",
|
||||
cfg.Debug.Addr,
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed to request health check")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Fatal().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health seems to be in bad state")
|
||||
}
|
||||
|
||||
logger.Debug().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health got a good state")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
|
||||
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
)
|
||||
|
||||
// Index is the entrypoint for the server command.
|
||||
func Index(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "index",
|
||||
Usage: "index the files for one one more users",
|
||||
Category: "index management",
|
||||
Aliases: []string{"i"},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "space",
|
||||
Aliases: []string{"s"},
|
||||
Required: true,
|
||||
Usage: "the id of the space to travers and index the files of",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "user",
|
||||
Aliases: []string{"u"},
|
||||
Required: true,
|
||||
Usage: "the username of the user tha shall be used to access the files",
|
||||
},
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
return parser.ParseConfig(cfg)
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
client := searchsvc.NewSearchProviderService("com.owncloud.api.search", grpc.DefaultClient)
|
||||
_, err := client.IndexSpace(context.Background(), &searchsvc.IndexSpaceRequest{
|
||||
SpaceId: c.String("space"),
|
||||
UserId: c.String("user"),
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("failed to index space: " + err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/clihelper"
|
||||
"github.com/thejerf/suture/v4"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// GetCommands provides all commands for this service
|
||||
func GetCommands(cfg *config.Config) cli.Commands {
|
||||
return []*cli.Command{
|
||||
// start this service
|
||||
Server(cfg),
|
||||
|
||||
// interaction with this service
|
||||
Index(cfg),
|
||||
|
||||
// infos about this service
|
||||
Health(cfg),
|
||||
Version(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute is the entry point for the ocis-search command.
|
||||
func Execute(cfg *config.Config) error {
|
||||
app := clihelper.DefaultApp(&cli.App{
|
||||
Name: "search",
|
||||
Usage: "Serve search API for oCIS",
|
||||
Commands: GetCommands(cfg),
|
||||
})
|
||||
cli.HelpFlag = &cli.BoolFlag{
|
||||
Name: "help,h",
|
||||
Usage: "Show the help",
|
||||
}
|
||||
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
|
||||
// SutureService allows for the search command to be embedded and supervised by a suture supervisor tree.
|
||||
type SutureService struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewSutureService creates a new search.SutureService
|
||||
func NewSutureService(cfg *ociscfg.Config) suture.Service {
|
||||
cfg.Search.Commons = cfg.Commons
|
||||
return SutureService{
|
||||
cfg: cfg.Search,
|
||||
}
|
||||
}
|
||||
|
||||
func (s SutureService) Serve(ctx context.Context) error {
|
||||
s.cfg.Context = ctx
|
||||
if err := Execute(s.cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/oklog/run"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config/parser"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/logging"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/metrics"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/server/debug"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/server/grpc"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/tracing"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
func Server(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "server",
|
||||
Usage: fmt.Sprintf("start %s extension without runtime (unsupervised mode)", cfg.Service.Name),
|
||||
Category: "server",
|
||||
Before: func(c *cli.Context) error {
|
||||
err := parser.ParseConfig(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return err
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
logger := logging.Configure(cfg.Service.Name, cfg.Log)
|
||||
err := tracing.Configure(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gr := run.Group{}
|
||||
ctx, cancel := func() (context.Context, context.CancelFunc) {
|
||||
if cfg.Context == nil {
|
||||
return context.WithCancel(context.Background())
|
||||
}
|
||||
return context.WithCancel(cfg.Context)
|
||||
}()
|
||||
defer cancel()
|
||||
|
||||
mtrcs := metrics.New()
|
||||
mtrcs.BuildInfo.WithLabelValues(version.GetString()).Set(1)
|
||||
|
||||
grpcServer := grpc.Server(
|
||||
grpc.Config(cfg),
|
||||
grpc.Logger(logger),
|
||||
grpc.Name(cfg.Service.Name),
|
||||
grpc.Context(ctx),
|
||||
grpc.Metrics(mtrcs),
|
||||
)
|
||||
|
||||
gr.Add(grpcServer.Run, func(_ error) {
|
||||
logger.Info().Str("server", "grpc").Msg("shutting down server")
|
||||
cancel()
|
||||
})
|
||||
|
||||
server, err := debug.Server(
|
||||
debug.Logger(logger),
|
||||
debug.Context(ctx),
|
||||
debug.Config(cfg),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server")
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(server.ListenAndServe, func(_ error) {
|
||||
_ = server.Shutdown(ctx)
|
||||
cancel()
|
||||
})
|
||||
|
||||
return gr.Run()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Version prints the service versions of all running instances.
|
||||
func Version(cfg *config.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "version",
|
||||
Usage: "print the version of this binary and the running extension instances",
|
||||
Category: "info",
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println("Version: " + version.GetString())
|
||||
fmt.Printf("Compiled: %s\n", version.Compiled())
|
||||
fmt.Println("")
|
||||
|
||||
reg := registry.GetRegistry()
|
||||
services, err := reg.GetService(cfg.GRPC.Namespace + "." + cfg.Service.Name)
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(services) == 0 {
|
||||
fmt.Println("No running " + cfg.Service.Name + " service found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
}
|
||||
}
|
||||
table.Render()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
)
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
type Config struct {
|
||||
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
|
||||
|
||||
Service Service `yaml:"-"`
|
||||
|
||||
Tracing *Tracing `yaml:"tracing"`
|
||||
Log *Log `yaml:"log"`
|
||||
Debug Debug `yaml:"debug"`
|
||||
|
||||
GRPC GRPC `yaml:"grpc"`
|
||||
|
||||
Datapath string `yaml:"data_path" env:"SEARCH_DATA_PATH"`
|
||||
Reva Reva `yaml:"reva"`
|
||||
Events Events `yaml:"events"`
|
||||
|
||||
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;SEARCH_MACHINE_AUTH_API_KEY" desc: "Machine auth API key used for accessing the 'auth-machine' service to impersonate users."`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
|
||||
// Events combines the configuration options for the event bus.
|
||||
type Events struct {
|
||||
Endpoint string `yaml:"endpoint" env:"SEARCH_EVENTS_ENDPOINT" desc:"the address of the streaming service"`
|
||||
Cluster string `yaml:"cluster" env:"SEARCH_EVENTS_CLUSTER" desc:"the clusterID of the streaming service. Mandatory when using nats"`
|
||||
ConsumerGroup string `yaml:"group" env:"SEARCH_EVENTS_GROUP" desc:"the customergroup of the service. One group will only get one copy of an event"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Debug defines the available debug configuration.
|
||||
type Debug struct {
|
||||
Addr string `ocisConfig:"addr" env:"SEARCH_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed."`
|
||||
Token string `ocisConfig:"token" env:"SEARCH_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint"`
|
||||
Pprof bool `ocisConfig:"pprof" env:"SEARCH_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling"`
|
||||
Zpages bool `ocisConfig:"zpages" env:"SEARCH_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces."`
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
|
||||
)
|
||||
|
||||
func FullDefaultConfig() *config.Config {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
EnsureDefaults(cfg)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func DefaultConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Debug: config.Debug{
|
||||
Addr: "127.0.0.1:9224",
|
||||
Token: "",
|
||||
},
|
||||
GRPC: config.GRPC{
|
||||
Addr: "127.0.0.1:9220",
|
||||
Namespace: "com.owncloud.api",
|
||||
},
|
||||
Service: config.Service{
|
||||
Name: "search",
|
||||
},
|
||||
Datapath: path.Join(defaults.BaseDataPath(), "search"),
|
||||
Reva: config.Reva{
|
||||
Address: "127.0.0.1:9142",
|
||||
},
|
||||
Events: config.Events{
|
||||
Endpoint: "127.0.0.1:9233",
|
||||
Cluster: "ocis-cluster",
|
||||
ConsumerGroup: "search",
|
||||
},
|
||||
MachineAuthAPIKey: "",
|
||||
}
|
||||
}
|
||||
|
||||
func EnsureDefaults(cfg *config.Config) {
|
||||
// provide with defaults for shared logging, since we need a valid destination address for BindEnv.
|
||||
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
|
||||
cfg.Log = &config.Log{
|
||||
Level: cfg.Commons.Log.Level,
|
||||
Pretty: cfg.Commons.Log.Pretty,
|
||||
Color: cfg.Commons.Log.Color,
|
||||
File: cfg.Commons.Log.File,
|
||||
}
|
||||
} else if cfg.Log == nil {
|
||||
cfg.Log = &config.Log{}
|
||||
}
|
||||
// provide with defaults for shared tracing, since we need a valid destination address for BindEnv.
|
||||
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
|
||||
cfg.Tracing = &config.Tracing{
|
||||
Enabled: cfg.Commons.Tracing.Enabled,
|
||||
Type: cfg.Commons.Tracing.Type,
|
||||
Endpoint: cfg.Commons.Tracing.Endpoint,
|
||||
Collector: cfg.Commons.Tracing.Collector,
|
||||
}
|
||||
} else if cfg.Tracing == nil {
|
||||
cfg.Tracing = &config.Tracing{}
|
||||
}
|
||||
|
||||
if cfg.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
|
||||
cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
|
||||
}
|
||||
}
|
||||
|
||||
func Sanitize(cfg *config.Config) {
|
||||
// no http endpoint to be sanitized
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
// GRPC defines the available grpc configuration.
|
||||
type GRPC struct {
|
||||
Addr string `ocisConfig:"addr" env:"SEARCH_GRPC_ADDR" desc:"The address of the grpc service."`
|
||||
Namespace string `ocisConfig:"-" yaml:"-"`
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
// HTTP defines the available http configuration.
|
||||
type HTTP struct {
|
||||
Addr string `ocisConfig:"addr" env:"SEARCH_HTTP_ADDR"`
|
||||
Namespace string `ocisConfig:"-" yaml:"-"`
|
||||
Root string `ocisConfig:"root" env:"SEARCH_HTTP_ROOT"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Log defines the available log configuration.
|
||||
type Log struct {
|
||||
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;SEARCH_LOG_LEVEL" desc:"The log level. Valid values are: \"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\"."`
|
||||
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;SEARCH_LOG_PRETTY" desc:"Activates pretty log output."`
|
||||
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;SEARCH_LOG_COLOR" desc:"Activates colorized log output."`
|
||||
File string `mapstructure:"file" env:"OCIS_LOG_FILE;SEARCH_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set."`
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config/defaults"
|
||||
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode"
|
||||
)
|
||||
|
||||
// ParseConfig loads configuration from known paths.
|
||||
func ParseConfig(cfg *config.Config) error {
|
||||
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaults.EnsureDefaults(cfg)
|
||||
|
||||
// load all env variables relevant to the config in the current context.
|
||||
if err := envdecode.Decode(cfg); err != nil {
|
||||
// no environment variable set for this config is an expected "error"
|
||||
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defaults.Sanitize(cfg)
|
||||
|
||||
return Validate(cfg)
|
||||
}
|
||||
|
||||
func Validate(cfg *config.Config) error {
|
||||
if cfg.MachineAuthAPIKey == "" {
|
||||
return shared.MissingMachineAuthApiKeyError(cfg.Service.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// Reva defines all available REVA configuration.
|
||||
type Reva struct {
|
||||
Address string `ocisConfig:"address" env:"REVA_GATEWAY" desc:"The CS3 gateway endpoint."`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `ocisConfig:"-" yaml:"-"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Tracing defines the available tracing configuration.
|
||||
type Tracing struct {
|
||||
Enabled bool `ocisConfig:"enabled" env:"OCIS_TRACING_ENABLED;SEARCH_TRACING_ENABLED" desc:"Activates tracing."`
|
||||
Type string `ocisConfig:"type" env:"OCIS_TRACING_TYPE;SEARCH_TRACING_TYPE" desc:"The type of tracing. Defaults to \"\", which is the same as \"jaeger\". Allowed tracing types are \"jaeger\" and \"\" as of now."`
|
||||
Endpoint string `ocisConfig:"endpoint" env:"OCIS_TRACING_ENDPOINT;SEARCH_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent."`
|
||||
Collector string `ocisConfig:"collector" env:"OCIS_TRACING_COLLECTOR;SEARCH_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset."`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// LoggerFromConfig initializes a service-specific logger instance.
|
||||
func Configure(name string, cfg *config.Log) log.Logger {
|
||||
return log.NewLogger(
|
||||
log.Name(name),
|
||||
log.Level(cfg.Level),
|
||||
log.Pretty(cfg.Pretty),
|
||||
log.Color(cfg.Color),
|
||||
log.File(cfg.File),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package metrics
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
var (
|
||||
// Namespace defines the namespace for the defines metrics.
|
||||
Namespace = "ocis"
|
||||
|
||||
// Subsystem defines the subsystem for the defines metrics.
|
||||
Subsystem = "search"
|
||||
)
|
||||
|
||||
// Metrics defines the available metrics of this service.
|
||||
type Metrics struct {
|
||||
// Counter *prometheus.CounterVec
|
||||
BuildInfo *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
// New initializes the available metrics.
|
||||
func New() *Metrics {
|
||||
m := &Metrics{
|
||||
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "build_info",
|
||||
Help: "Build information",
|
||||
}, []string{"version"}),
|
||||
}
|
||||
|
||||
_ = prometheus.Register(m.BuildInfo)
|
||||
// TODO: implement metrics
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
// Copyright 2018-2022 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package index
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
|
||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/single"
|
||||
"github.com/blevesearch/bleve/v2/mapping"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0"
|
||||
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
)
|
||||
|
||||
type indexDocument struct {
|
||||
RootID string
|
||||
Path string
|
||||
ID string
|
||||
|
||||
Name string
|
||||
Size uint64
|
||||
Mtime string
|
||||
MimeType string
|
||||
Type uint64
|
||||
|
||||
Deleted bool
|
||||
}
|
||||
|
||||
// Index represents a bleve based search index
|
||||
type Index struct {
|
||||
bleveIndex bleve.Index
|
||||
}
|
||||
|
||||
// NewPersisted returns a new instance of Index with the data being persisted in the given directory
|
||||
func NewPersisted(path string) (*Index, error) {
|
||||
mapping, err := BuildMapping()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bi, err := bleve.New(path, mapping)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return New(bi)
|
||||
}
|
||||
|
||||
// New returns a new instance of Index using the given bleve Index as the backend
|
||||
func New(bleveIndex bleve.Index) (*Index, error) {
|
||||
return &Index{
|
||||
bleveIndex: bleveIndex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DocCount returns the number of elemenst in the index
|
||||
func (i *Index) DocCount() (uint64, error) {
|
||||
return i.bleveIndex.DocCount()
|
||||
}
|
||||
|
||||
// Add adds a new entity to the Index
|
||||
func (i *Index) Add(ref *sprovider.Reference, ri *sprovider.ResourceInfo) error {
|
||||
entity := toEntity(ref, ri)
|
||||
return i.bleveIndex.Index(idToBleveId(ri.Id), entity)
|
||||
}
|
||||
|
||||
// Delete marks an entity from the index as deleten (still keeping it around)
|
||||
func (i *Index) Delete(id *sprovider.ResourceId) error {
|
||||
return i.markAsDeleted(idToBleveId(id), true)
|
||||
}
|
||||
|
||||
// Restore marks an entity from the index as not being deleted
|
||||
func (i *Index) Restore(id *sprovider.ResourceId) error {
|
||||
return i.markAsDeleted(idToBleveId(id), false)
|
||||
}
|
||||
|
||||
func (i *Index) markAsDeleted(id string, deleted bool) error {
|
||||
doc, err := i.updateEntity(id, func(doc *indexDocument) {
|
||||
doc.Deleted = deleted
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if doc.Type == uint64(sprovider.ResourceType_RESOURCE_TYPE_CONTAINER) {
|
||||
query := bleve.NewConjunctionQuery(
|
||||
bleve.NewQueryStringQuery("RootID:"+doc.RootID),
|
||||
bleve.NewQueryStringQuery("Path:"+queryEscape(doc.Path+"/*")),
|
||||
)
|
||||
bleveReq := bleve.NewSearchRequest(query)
|
||||
bleveReq.Size = math.MaxInt
|
||||
bleveReq.Fields = []string{"*"}
|
||||
res, err := i.bleveIndex.Search(bleveReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, h := range res.Hits {
|
||||
_, err := i.updateEntity(h.ID, func(doc *indexDocument) {
|
||||
doc.Deleted = deleted
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Index) updateEntity(id string, mutateFunc func(doc *indexDocument)) (*indexDocument, error) {
|
||||
doc, err := i.getEntity(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mutateFunc(doc)
|
||||
err = i.bleveIndex.Index(doc.ID, doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func (i *Index) getEntity(id string) (*indexDocument, error) {
|
||||
req := bleve.NewSearchRequest(bleve.NewDocIDQuery([]string{id}))
|
||||
req.Fields = []string{"*"}
|
||||
res, err := i.bleveIndex.Search(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.Hits.Len() == 0 {
|
||||
return nil, errors.New("entity not found")
|
||||
}
|
||||
return fieldsToEntity(res.Hits[0].Fields), nil
|
||||
}
|
||||
|
||||
// Purge removes an entity from the index
|
||||
func (i *Index) Purge(id *sprovider.ResourceId) error {
|
||||
return i.bleveIndex.Delete(idToBleveId(id))
|
||||
}
|
||||
|
||||
// Move update the path of an entry and all its children
|
||||
func (i *Index) Move(id *sprovider.ResourceId, fullPath string) error {
|
||||
bleveId := idToBleveId(id)
|
||||
doc, err := i.getEntity(bleveId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldName := doc.Path
|
||||
newName := utils.MakeRelativePath(fullPath)
|
||||
|
||||
doc, err = i.updateEntity(bleveId, func(doc *indexDocument) {
|
||||
doc.Path = newName
|
||||
doc.Name = path.Base(newName)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if doc.Type == uint64(sprovider.ResourceType_RESOURCE_TYPE_CONTAINER) {
|
||||
query := bleve.NewConjunctionQuery(
|
||||
bleve.NewQueryStringQuery("RootID:"+doc.RootID),
|
||||
bleve.NewQueryStringQuery("Path:"+queryEscape(oldName+"/*")),
|
||||
)
|
||||
bleveReq := bleve.NewSearchRequest(query)
|
||||
bleveReq.Size = math.MaxInt
|
||||
bleveReq.Fields = []string{"*"}
|
||||
res, err := i.bleveIndex.Search(bleveReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, h := range res.Hits {
|
||||
_, err := i.updateEntity(h.ID, func(doc *indexDocument) {
|
||||
doc.Path = strings.Replace(doc.Path, oldName, newName, 1)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search searches the index according to the criteria specified in the given SearchIndexRequest
|
||||
func (i *Index) Search(ctx context.Context, req *searchsvc.SearchIndexRequest) (*searchsvc.SearchIndexResponse, error) {
|
||||
deletedQuery := bleve.NewBoolFieldQuery(false)
|
||||
deletedQuery.SetField("Deleted")
|
||||
query := bleve.NewConjunctionQuery(
|
||||
bleve.NewQueryStringQuery(req.Query),
|
||||
deletedQuery, // Skip documents that have been marked as deleted
|
||||
bleve.NewQueryStringQuery("RootID:"+req.Ref.ResourceId.StorageId+"!"+req.Ref.ResourceId.OpaqueId), // Limit search to the space
|
||||
bleve.NewQueryStringQuery("Path:"+queryEscape(utils.MakeRelativePath(path.Join(req.Ref.Path, "/"))+"*")), // Limit search to this directory in the space
|
||||
)
|
||||
bleveReq := bleve.NewSearchRequest(query)
|
||||
bleveReq.Size = 200
|
||||
bleveReq.Fields = []string{"*"}
|
||||
res, err := i.bleveIndex.Search(bleveReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matches := []*searchmsg.Match{}
|
||||
for _, h := range res.Hits {
|
||||
match, err := fromFields(h.Fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matches = append(matches, match)
|
||||
}
|
||||
|
||||
return &searchsvc.SearchIndexResponse{
|
||||
Matches: matches,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildMapping builds a bleve index mapping which can be used for indexing
|
||||
func BuildMapping() (mapping.IndexMapping, error) {
|
||||
nameMapping := bleve.NewTextFieldMapping()
|
||||
nameMapping.Analyzer = "lowercaseKeyword"
|
||||
|
||||
docMapping := bleve.NewDocumentMapping()
|
||||
docMapping.AddFieldMappingsAt("Name", nameMapping)
|
||||
|
||||
indexMapping := bleve.NewIndexMapping()
|
||||
indexMapping.DefaultAnalyzer = keyword.Name
|
||||
indexMapping.DefaultMapping = docMapping
|
||||
err := indexMapping.AddCustomAnalyzer("lowercaseKeyword",
|
||||
map[string]interface{}{
|
||||
"type": custom.Name,
|
||||
"tokenizer": single.Name,
|
||||
"token_filters": []string{
|
||||
lowercase.Name,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return indexMapping, nil
|
||||
}
|
||||
|
||||
func toEntity(ref *sprovider.Reference, ri *sprovider.ResourceInfo) *indexDocument {
|
||||
doc := &indexDocument{
|
||||
RootID: idToBleveId(ref.ResourceId),
|
||||
Path: ref.Path,
|
||||
ID: idToBleveId(ri.Id),
|
||||
Name: ri.Path,
|
||||
Size: ri.Size,
|
||||
MimeType: ri.MimeType,
|
||||
Type: uint64(ri.Type),
|
||||
Deleted: false,
|
||||
}
|
||||
|
||||
if ri.Mtime != nil {
|
||||
doc.Mtime = time.Unix(int64(ri.Mtime.Seconds), int64(ri.Mtime.Nanos)).UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
func fieldsToEntity(fields map[string]interface{}) *indexDocument {
|
||||
doc := &indexDocument{
|
||||
RootID: fields["RootID"].(string),
|
||||
Path: fields["Path"].(string),
|
||||
ID: fields["ID"].(string),
|
||||
Name: fields["Name"].(string),
|
||||
Size: uint64(fields["Size"].(float64)),
|
||||
Mtime: fields["Mtime"].(string),
|
||||
MimeType: fields["MimeType"].(string),
|
||||
Type: uint64(fields["Type"].(float64)),
|
||||
Deleted: fields["Deleted"].(bool),
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
func fromFields(fields map[string]interface{}) (*searchmsg.Match, error) {
|
||||
rootIDParts := strings.SplitN(fields["RootID"].(string), "!", 2)
|
||||
IDParts := strings.SplitN(fields["ID"].(string), "!", 2)
|
||||
|
||||
match := &searchmsg.Match{
|
||||
Entity: &searchmsg.Entity{
|
||||
Ref: &searchmsg.Reference{
|
||||
ResourceId: &searchmsg.ResourceID{
|
||||
StorageId: rootIDParts[0],
|
||||
OpaqueId: rootIDParts[1],
|
||||
},
|
||||
Path: fields["Path"].(string),
|
||||
},
|
||||
Id: &searchmsg.ResourceID{
|
||||
StorageId: IDParts[0],
|
||||
OpaqueId: IDParts[1],
|
||||
},
|
||||
Name: fields["Name"].(string),
|
||||
Size: uint64(fields["Size"].(float64)),
|
||||
Type: uint64(fields["Type"].(float64)),
|
||||
MimeType: fields["MimeType"].(string),
|
||||
Deleted: fields["Deleted"].(bool),
|
||||
},
|
||||
}
|
||||
|
||||
if mtime, err := time.Parse(time.RFC3339, fields["Mtime"].(string)); err == nil {
|
||||
match.Entity.LastModifiedTime = ×tamppb.Timestamp{Seconds: mtime.Unix(), Nanos: int32(mtime.Nanosecond())}
|
||||
}
|
||||
|
||||
return match, nil
|
||||
}
|
||||
|
||||
func idToBleveId(id *sprovider.ResourceId) string {
|
||||
if id == nil {
|
||||
return ""
|
||||
}
|
||||
return id.StorageId + "!" + id.OpaqueId
|
||||
}
|
||||
|
||||
func queryEscape(s string) string {
|
||||
re := regexp.MustCompile(`([` + regexp.QuoteMeta(`+=&|><!(){}[]^\"~*?:\/`) + `\-\s])`)
|
||||
return re.ReplaceAllString(s, "\\$1")
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package index_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Index Suite")
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
package index_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/search/index"
|
||||
searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0"
|
||||
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Index", func() {
|
||||
var (
|
||||
i *index.Index
|
||||
bleveIndex bleve.Index
|
||||
ctx context.Context
|
||||
|
||||
rootId = &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "rootopaqueid",
|
||||
}
|
||||
filename string
|
||||
ref *sprovider.Reference
|
||||
ri *sprovider.ResourceInfo
|
||||
parentRef = &sprovider.Reference{
|
||||
ResourceId: rootId,
|
||||
Path: "./my/sub d!r",
|
||||
}
|
||||
parentRi = &sprovider.ResourceInfo{
|
||||
Id: &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "parentopaqueid",
|
||||
},
|
||||
Path: "sub d!r",
|
||||
Size: 12345,
|
||||
Type: sprovider.ResourceType_RESOURCE_TYPE_CONTAINER,
|
||||
Mtime: &typesv1beta1.Timestamp{Seconds: 4000},
|
||||
}
|
||||
childRef = &sprovider.Reference{
|
||||
ResourceId: rootId,
|
||||
Path: "./my/sub d!r/child.pdf",
|
||||
}
|
||||
childRi = &sprovider.ResourceInfo{
|
||||
Id: &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "childopaqueid",
|
||||
},
|
||||
ParentId: &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "parentopaqueid",
|
||||
},
|
||||
Path: "child.pdf",
|
||||
Size: 12345,
|
||||
Type: sprovider.ResourceType_RESOURCE_TYPE_FILE,
|
||||
Mtime: &typesv1beta1.Timestamp{Seconds: 4000},
|
||||
}
|
||||
|
||||
assertDocCount = func(rootId *sprovider.ResourceId, query string, expectedCount int) []*searchmsg.Match {
|
||||
res, err := i.Search(ctx, &searchsvc.SearchIndexRequest{
|
||||
Query: query,
|
||||
Ref: &searchmsg.Reference{
|
||||
ResourceId: &searchmsg.ResourceID{
|
||||
StorageId: rootId.StorageId,
|
||||
OpaqueId: rootId.OpaqueId,
|
||||
},
|
||||
},
|
||||
})
|
||||
ExpectWithOffset(1, err).ToNot(HaveOccurred())
|
||||
ExpectWithOffset(1, len(res.Matches)).To(Equal(expectedCount), "query returned unexpected number of results: "+query)
|
||||
return res.Matches
|
||||
}
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
filename = "Foo.pdf"
|
||||
|
||||
mapping, err := index.BuildMapping()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
bleveIndex, err = bleve.NewMemOnly(mapping)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
i, err = index.New(bleveIndex)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
JustBeforeEach(func() {
|
||||
ref = &sprovider.Reference{
|
||||
ResourceId: rootId,
|
||||
Path: "./" + filename,
|
||||
}
|
||||
ri = &sprovider.ResourceInfo{
|
||||
Id: &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "opaqueid",
|
||||
},
|
||||
ParentId: &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "someopaqueid",
|
||||
},
|
||||
Path: filename,
|
||||
Size: 12345,
|
||||
Type: sprovider.ResourceType_RESOURCE_TYPE_FILE,
|
||||
MimeType: "application/pdf",
|
||||
Mtime: &typesv1beta1.Timestamp{Seconds: 4000},
|
||||
}
|
||||
})
|
||||
|
||||
Describe("New", func() {
|
||||
It("returns a new index instance", func() {
|
||||
i, err := index.New(bleveIndex)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(i).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("NewPersisted", func() {
|
||||
It("returns a new index instance", func() {
|
||||
i, err := index.NewPersisted("")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(i).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Search", func() {
|
||||
Context("by other fields than filename", func() {
|
||||
JustBeforeEach(func() {
|
||||
err := i.Add(ref, ri)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("finds files by size", func() {
|
||||
assertDocCount(ref.ResourceId, `Size:12345`, 1)
|
||||
assertDocCount(ref.ResourceId, `Size:>1000`, 1)
|
||||
assertDocCount(ref.ResourceId, `Size:<100000`, 1)
|
||||
|
||||
assertDocCount(ref.ResourceId, `Size:12344`, 0)
|
||||
assertDocCount(ref.ResourceId, `Size:<1000`, 0)
|
||||
assertDocCount(ref.ResourceId, `Size:>100000`, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Context("by filename", func() {
|
||||
It("finds files with spaces in the filename", func() {
|
||||
ri.Path = "Foo oo.pdf"
|
||||
ref.Path = "./" + ri.Path
|
||||
err := i.Add(ref, ri)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
assertDocCount(ref.ResourceId, `Name:foo\ o*`, 1)
|
||||
})
|
||||
|
||||
It("finds files by digits in the filename", func() {
|
||||
ri.Path = "12345.pdf"
|
||||
ref.Path = "./" + ri.Path
|
||||
err := i.Add(ref, ri)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
assertDocCount(ref.ResourceId, `Name:1234*`, 1)
|
||||
})
|
||||
|
||||
Context("with a file in the root of the space", func() {
|
||||
JustBeforeEach(func() {
|
||||
err := i.Add(ref, ri)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("scopes the search to the specified space", func() {
|
||||
resourceId := &sprovider.ResourceId{
|
||||
StorageId: "differentstorageid",
|
||||
OpaqueId: "differentopaqueid",
|
||||
}
|
||||
assertDocCount(resourceId, `Name:foo.pdf`, 0)
|
||||
})
|
||||
|
||||
It("limits the search to the specified fields", func() {
|
||||
assertDocCount(ref.ResourceId, "Name:*"+ref.ResourceId.OpaqueId+"*", 0)
|
||||
})
|
||||
|
||||
It("returns all desired fields", func() {
|
||||
matches := assertDocCount(ref.ResourceId, "Name:foo.pdf", 1)
|
||||
match := matches[0]
|
||||
Expect(match.Entity.Ref.ResourceId.OpaqueId).To(Equal(ref.ResourceId.OpaqueId))
|
||||
Expect(match.Entity.Ref.Path).To(Equal(ref.Path))
|
||||
Expect(match.Entity.Id.OpaqueId).To(Equal(ri.Id.OpaqueId))
|
||||
Expect(match.Entity.Name).To(Equal(ri.Path))
|
||||
Expect(match.Entity.Size).To(Equal(ri.Size))
|
||||
Expect(match.Entity.Type).To(Equal(uint64(ri.Type)))
|
||||
Expect(match.Entity.MimeType).To(Equal(ri.MimeType))
|
||||
Expect(match.Entity.Deleted).To(BeFalse())
|
||||
Expect(uint64(match.Entity.LastModifiedTime.AsTime().Unix())).To(Equal(ri.Mtime.Seconds))
|
||||
})
|
||||
|
||||
It("finds files by name, prefix or substring match", func() {
|
||||
queries := []string{"foo.pdf", "foo*", "*oo.p*"}
|
||||
for _, query := range queries {
|
||||
matches := assertDocCount(ref.ResourceId, query, 1)
|
||||
Expect(matches[0].Entity.Ref.ResourceId.OpaqueId).To(Equal(ref.ResourceId.OpaqueId))
|
||||
Expect(matches[0].Entity.Ref.Path).To(Equal(ref.Path))
|
||||
Expect(matches[0].Entity.Id.OpaqueId).To(Equal(ri.Id.OpaqueId))
|
||||
Expect(matches[0].Entity.Name).To(Equal(ri.Path))
|
||||
Expect(matches[0].Entity.Size).To(Equal(ri.Size))
|
||||
}
|
||||
})
|
||||
|
||||
It("uses a lower-case index", func() {
|
||||
assertDocCount(ref.ResourceId, "Name:foo*", 1)
|
||||
assertDocCount(ref.ResourceId, "Name:Foo*", 0)
|
||||
})
|
||||
|
||||
Context("and an additional file in a subdirectory", func() {
|
||||
var (
|
||||
nestedRef *sprovider.Reference
|
||||
nestedRI *sprovider.ResourceInfo
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
nestedRef = &sprovider.Reference{
|
||||
ResourceId: &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "rootopaqueid",
|
||||
},
|
||||
Path: "./nested/nestedpdf.pdf",
|
||||
}
|
||||
nestedRI = &sprovider.ResourceInfo{
|
||||
Id: &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "nestedopaqueid",
|
||||
},
|
||||
Path: "nestedpdf.pdf",
|
||||
Size: 12345,
|
||||
}
|
||||
err := i.Add(nestedRef, nestedRI)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("finds files living deeper in the tree by filename, prefix or substring match", func() {
|
||||
queries := []string{"nestedpdf.pdf", "nested*", "*tedpdf.*"}
|
||||
for _, query := range queries {
|
||||
assertDocCount(ref.ResourceId, query, 1)
|
||||
}
|
||||
})
|
||||
|
||||
It("does not find the higher levels when limiting the searched directory", func() {
|
||||
res, err := i.Search(ctx, &searchsvc.SearchIndexRequest{
|
||||
Ref: &searchmsg.Reference{
|
||||
ResourceId: &searchmsg.ResourceID{
|
||||
StorageId: ref.ResourceId.StorageId,
|
||||
OpaqueId: ref.ResourceId.OpaqueId,
|
||||
},
|
||||
Path: "./nested/",
|
||||
},
|
||||
Query: "Name:foo.pdf",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res).ToNot(BeNil())
|
||||
Expect(len(res.Matches)).To(Equal(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Add", func() {
|
||||
It("adds a resourceInfo to the index", func() {
|
||||
err := i.Add(ref, ri)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
count, err := bleveIndex.DocCount()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(count).To(Equal(uint64(1)))
|
||||
|
||||
query := bleve.NewMatchQuery("foo.pdf")
|
||||
res, err := bleveIndex.Search(bleve.NewSearchRequest(query))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Hits.Len()).To(Equal(1))
|
||||
})
|
||||
|
||||
It("updates an existing resource in the index", func() {
|
||||
err := i.Add(ref, ri)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
count, _ := bleveIndex.DocCount()
|
||||
Expect(count).To(Equal(uint64(1)))
|
||||
|
||||
err = i.Add(ref, ri)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
count, _ = bleveIndex.DocCount()
|
||||
Expect(count).To(Equal(uint64(1)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Delete", func() {
|
||||
It("marks a resource as deleted", func() {
|
||||
err := i.Add(parentRef, parentRi)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
assertDocCount(rootId, `sub\ d!r`, 1)
|
||||
|
||||
err = i.Delete(parentRi.Id)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
assertDocCount(rootId, `sub\ d!r`, 0)
|
||||
})
|
||||
|
||||
It("also marks child resources as deleted", func() {
|
||||
err := i.Add(parentRef, parentRi)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = i.Add(childRef, childRi)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
assertDocCount(rootId, `sub\ d\!r`, 1)
|
||||
assertDocCount(rootId, "child.pdf", 1)
|
||||
|
||||
err = i.Delete(parentRi.Id)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
assertDocCount(rootId, `sub\ d\!r`, 0)
|
||||
assertDocCount(rootId, "child.pdf", 0)
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Restore", func() {
|
||||
It("also marks child resources as restored", func() {
|
||||
err := i.Add(parentRef, parentRi)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = i.Add(childRef, childRi)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = i.Delete(parentRi.Id)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
assertDocCount(rootId, `sub\ d!r`, 0)
|
||||
assertDocCount(rootId, "child.pdf", 0)
|
||||
|
||||
err = i.Restore(parentRi.Id)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
assertDocCount(rootId, `sub\ d!r`, 1)
|
||||
assertDocCount(rootId, "child.pdf", 1)
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Move", func() {
|
||||
It("moves the parent and its child resources", func() {
|
||||
err := i.Add(parentRef, parentRi)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = i.Add(childRef, childRi)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
parentRi.Path = "newname"
|
||||
err = i.Move(parentRi.Id, "./somewhere/else/newname")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
assertDocCount(rootId, `sub\ d!r`, 0)
|
||||
|
||||
matches := assertDocCount(rootId, "Name:child.pdf", 1)
|
||||
Expect(matches[0].Entity.Ref.Path).To(Equal("./somewhere/else/newname/child.pdf"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,415 @@
|
||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
bleve "github.com/blevesearch/bleve/v2"
|
||||
|
||||
index "github.com/blevesearch/bleve_index_api"
|
||||
|
||||
mapping "github.com/blevesearch/bleve/v2/mapping"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// BleveIndex is an autogenerated mock type for the BleveIndex type
|
||||
type BleveIndex struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Advanced provides a mock function with given fields:
|
||||
func (_m *BleveIndex) Advanced() (index.Index, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 index.Index
|
||||
if rf, ok := ret.Get(0).(func() index.Index); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(index.Index)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Batch provides a mock function with given fields: b
|
||||
func (_m *BleveIndex) Batch(b *bleve.Batch) error {
|
||||
ret := _m.Called(b)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*bleve.Batch) error); ok {
|
||||
r0 = rf(b)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *BleveIndex) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: id
|
||||
func (_m *BleveIndex) Delete(id string) error {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteInternal provides a mock function with given fields: key
|
||||
func (_m *BleveIndex) DeleteInternal(key []byte) error {
|
||||
ret := _m.Called(key)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func([]byte) error); ok {
|
||||
r0 = rf(key)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DocCount provides a mock function with given fields:
|
||||
func (_m *BleveIndex) DocCount() (uint64, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 uint64
|
||||
if rf, ok := ret.Get(0).(func() uint64); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Document provides a mock function with given fields: id
|
||||
func (_m *BleveIndex) Document(id string) (index.Document, error) {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 index.Document
|
||||
if rf, ok := ret.Get(0).(func(string) index.Document); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(index.Document)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// FieldDict provides a mock function with given fields: field
|
||||
func (_m *BleveIndex) FieldDict(field string) (index.FieldDict, error) {
|
||||
ret := _m.Called(field)
|
||||
|
||||
var r0 index.FieldDict
|
||||
if rf, ok := ret.Get(0).(func(string) index.FieldDict); ok {
|
||||
r0 = rf(field)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(index.FieldDict)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(field)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// FieldDictPrefix provides a mock function with given fields: field, termPrefix
|
||||
func (_m *BleveIndex) FieldDictPrefix(field string, termPrefix []byte) (index.FieldDict, error) {
|
||||
ret := _m.Called(field, termPrefix)
|
||||
|
||||
var r0 index.FieldDict
|
||||
if rf, ok := ret.Get(0).(func(string, []byte) index.FieldDict); ok {
|
||||
r0 = rf(field, termPrefix)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(index.FieldDict)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, []byte) error); ok {
|
||||
r1 = rf(field, termPrefix)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// FieldDictRange provides a mock function with given fields: field, startTerm, endTerm
|
||||
func (_m *BleveIndex) FieldDictRange(field string, startTerm []byte, endTerm []byte) (index.FieldDict, error) {
|
||||
ret := _m.Called(field, startTerm, endTerm)
|
||||
|
||||
var r0 index.FieldDict
|
||||
if rf, ok := ret.Get(0).(func(string, []byte, []byte) index.FieldDict); ok {
|
||||
r0 = rf(field, startTerm, endTerm)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(index.FieldDict)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, []byte, []byte) error); ok {
|
||||
r1 = rf(field, startTerm, endTerm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Fields provides a mock function with given fields:
|
||||
func (_m *BleveIndex) Fields() ([]string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetInternal provides a mock function with given fields: key
|
||||
func (_m *BleveIndex) GetInternal(key []byte) ([]byte, error) {
|
||||
ret := _m.Called(key)
|
||||
|
||||
var r0 []byte
|
||||
if rf, ok := ret.Get(0).(func([]byte) []byte); ok {
|
||||
r0 = rf(key)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func([]byte) error); ok {
|
||||
r1 = rf(key)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Index provides a mock function with given fields: id, data
|
||||
func (_m *BleveIndex) Index(id string, data interface{}) error {
|
||||
ret := _m.Called(id, data)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
|
||||
r0 = rf(id, data)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Mapping provides a mock function with given fields:
|
||||
func (_m *BleveIndex) Mapping() mapping.IndexMapping {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 mapping.IndexMapping
|
||||
if rf, ok := ret.Get(0).(func() mapping.IndexMapping); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(mapping.IndexMapping)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Name provides a mock function with given fields:
|
||||
func (_m *BleveIndex) Name() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewBatch provides a mock function with given fields:
|
||||
func (_m *BleveIndex) NewBatch() *bleve.Batch {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *bleve.Batch
|
||||
if rf, ok := ret.Get(0).(func() *bleve.Batch); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*bleve.Batch)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Search provides a mock function with given fields: req
|
||||
func (_m *BleveIndex) Search(req *bleve.SearchRequest) (*bleve.SearchResult, error) {
|
||||
ret := _m.Called(req)
|
||||
|
||||
var r0 *bleve.SearchResult
|
||||
if rf, ok := ret.Get(0).(func(*bleve.SearchRequest) *bleve.SearchResult); ok {
|
||||
r0 = rf(req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*bleve.SearchResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*bleve.SearchRequest) error); ok {
|
||||
r1 = rf(req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SearchInContext provides a mock function with given fields: ctx, req
|
||||
func (_m *BleveIndex) SearchInContext(ctx context.Context, req *bleve.SearchRequest) (*bleve.SearchResult, error) {
|
||||
ret := _m.Called(ctx, req)
|
||||
|
||||
var r0 *bleve.SearchResult
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *bleve.SearchRequest) *bleve.SearchResult); ok {
|
||||
r0 = rf(ctx, req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*bleve.SearchResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *bleve.SearchRequest) error); ok {
|
||||
r1 = rf(ctx, req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetInternal provides a mock function with given fields: key, val
|
||||
func (_m *BleveIndex) SetInternal(key []byte, val []byte) error {
|
||||
ret := _m.Called(key, val)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func([]byte, []byte) error); ok {
|
||||
r0 = rf(key, val)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetName provides a mock function with given fields: _a0
|
||||
func (_m *BleveIndex) SetName(_a0 string) {
|
||||
_m.Called(_a0)
|
||||
}
|
||||
|
||||
// Stats provides a mock function with given fields:
|
||||
func (_m *BleveIndex) Stats() *bleve.IndexStat {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *bleve.IndexStat
|
||||
if rf, ok := ret.Get(0).(func() *bleve.IndexStat); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*bleve.IndexStat)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// StatsMap provides a mock function with given fields:
|
||||
func (_m *BleveIndex) StatsMap() map[string]interface{} {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 map[string]interface{}
|
||||
if rf, ok := ret.Get(0).(func() map[string]interface{}); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
)
|
||||
|
||||
// IndexClient is an autogenerated mock type for the IndexClient type
|
||||
type IndexClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Add provides a mock function with given fields: ref, ri
|
||||
func (_m *IndexClient) Add(ref *providerv1beta1.Reference, ri *providerv1beta1.ResourceInfo) error {
|
||||
ret := _m.Called(ref, ri)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*providerv1beta1.Reference, *providerv1beta1.ResourceInfo) error); ok {
|
||||
r0 = rf(ref, ri)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: id
|
||||
func (_m *IndexClient) Delete(id *providerv1beta1.ResourceId) error {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*providerv1beta1.ResourceId) error); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DocCount provides a mock function with given fields:
|
||||
func (_m *IndexClient) DocCount() (uint64, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 uint64
|
||||
if rf, ok := ret.Get(0).(func() uint64); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Move provides a mock function with given fields: id, fullPath
|
||||
func (_m *IndexClient) Move(id *providerv1beta1.ResourceId, fullPath string) error {
|
||||
ret := _m.Called(id, fullPath)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*providerv1beta1.ResourceId, string) error); ok {
|
||||
r0 = rf(id, fullPath)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Purge provides a mock function with given fields: id
|
||||
func (_m *IndexClient) Purge(id *providerv1beta1.ResourceId) error {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*providerv1beta1.ResourceId) error); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Restore provides a mock function with given fields: id
|
||||
func (_m *IndexClient) Restore(id *providerv1beta1.ResourceId) error {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*providerv1beta1.ResourceId) error); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Search provides a mock function with given fields: ctx, req
|
||||
func (_m *IndexClient) Search(ctx context.Context, req *v0.SearchIndexRequest) (*v0.SearchIndexResponse, error) {
|
||||
ret := _m.Called(ctx, req)
|
||||
|
||||
var r0 *v0.SearchIndexResponse
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0.SearchIndexRequest) *v0.SearchIndexResponse); ok {
|
||||
r0 = rf(ctx, req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v0.SearchIndexResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v0.SearchIndexRequest) error); ok {
|
||||
r1 = rf(ctx, req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
)
|
||||
|
||||
// ProviderClient is an autogenerated mock type for the ProviderClient type
|
||||
type ProviderClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// IndexSpace provides a mock function with given fields: ctx, req
|
||||
func (_m *ProviderClient) IndexSpace(ctx context.Context, req *v0.IndexSpaceRequest) (*v0.IndexSpaceResponse, error) {
|
||||
ret := _m.Called(ctx, req)
|
||||
|
||||
var r0 *v0.IndexSpaceResponse
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0.IndexSpaceRequest) *v0.IndexSpaceResponse); ok {
|
||||
r0 = rf(ctx, req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v0.IndexSpaceResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v0.IndexSpaceRequest) error); ok {
|
||||
r1 = rf(ctx, req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Search provides a mock function with given fields: ctx, req
|
||||
func (_m *ProviderClient) Search(ctx context.Context, req *v0.SearchRequest) (*v0.SearchResponse, error) {
|
||||
ret := _m.Called(ctx, req)
|
||||
|
||||
var r0 *v0.SearchResponse
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0.SearchRequest) *v0.SearchResponse); ok {
|
||||
r0 = rf(ctx, req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v0.SearchResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v0.SearchRequest) error); ok {
|
||||
r1 = rf(ctx, req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/errtypes"
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func (p *Provider) handleEvent(ev interface{}) {
|
||||
var ref *provider.Reference
|
||||
var owner *user.User
|
||||
switch e := ev.(type) {
|
||||
case events.ItemTrashed:
|
||||
p.logger.Debug().Interface("event", ev).Msg("marking document as deleted")
|
||||
err := p.indexClient.Delete(e.ID)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Interface("Id", e.ID).Msg("failed to remove item from index")
|
||||
}
|
||||
return
|
||||
case events.ItemRestored:
|
||||
p.logger.Debug().Interface("event", ev).Msg("marking document as restored")
|
||||
ref = e.Ref
|
||||
owner = &user.User{
|
||||
Id: e.Executant,
|
||||
}
|
||||
|
||||
statRes, err := p.statResource(ref, owner)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("failed to stat the changed resource")
|
||||
return
|
||||
}
|
||||
|
||||
switch statRes.Status.Code {
|
||||
case rpc.Code_CODE_OK:
|
||||
err = p.indexClient.Restore(statRes.Info.Id)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("failed to restore the changed resource in the index")
|
||||
}
|
||||
default:
|
||||
p.logger.Error().Interface("statRes", statRes).Msg("failed to stat the changed resource")
|
||||
}
|
||||
|
||||
return
|
||||
case events.ItemMoved:
|
||||
p.logger.Debug().Interface("event", ev).Msg("resource has been moved, updating the document")
|
||||
ref = e.Ref
|
||||
owner = &user.User{
|
||||
Id: e.Executant,
|
||||
}
|
||||
|
||||
statRes, err := p.statResource(ref, owner)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("failed to stat the moved resource")
|
||||
return
|
||||
}
|
||||
if statRes.Status.Code != rpc.Code_CODE_OK {
|
||||
p.logger.Error().Interface("statRes", statRes).Msg("failed to stat the moved resource")
|
||||
return
|
||||
}
|
||||
|
||||
gpRes, err := p.getPath(statRes.Info.Id, owner)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Interface("ref", ref).Msg("failed to get path for moved resource")
|
||||
return
|
||||
}
|
||||
if gpRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
p.logger.Error().Interface("status", gpRes.Status).Interface("ref", ref).Msg("failed to get path for moved resource")
|
||||
return
|
||||
}
|
||||
|
||||
err = p.indexClient.Move(statRes.Info.Id, gpRes.Path)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("failed to move the changed resource in the index")
|
||||
}
|
||||
return
|
||||
case events.ContainerCreated:
|
||||
ref = e.Ref
|
||||
owner = &user.User{
|
||||
Id: e.Executant,
|
||||
}
|
||||
case events.FileUploaded:
|
||||
ref = e.Ref
|
||||
owner = &user.User{
|
||||
Id: e.Executant,
|
||||
}
|
||||
case events.FileTouched:
|
||||
ref = e.Ref
|
||||
owner = &user.User{
|
||||
Id: e.Executant,
|
||||
}
|
||||
case events.FileVersionRestored:
|
||||
ref = e.Ref
|
||||
owner = &user.User{
|
||||
Id: e.Executant,
|
||||
}
|
||||
default:
|
||||
// Not sure what to do here. Skip.
|
||||
return
|
||||
}
|
||||
p.logger.Debug().Interface("event", ev).Msg("resource has been changed, updating the document")
|
||||
|
||||
statRes, err := p.statResource(ref, owner)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("failed to stat the changed resource")
|
||||
return
|
||||
}
|
||||
if statRes.Status.Code != rpc.Code_CODE_OK {
|
||||
p.logger.Error().Interface("statRes", statRes).Msg("failed to stat the changed resource")
|
||||
return
|
||||
}
|
||||
|
||||
err = p.indexClient.Add(ref, statRes.Info)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("error adding updating the resource in the index")
|
||||
} else {
|
||||
p.logDocCount()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) statResource(ref *provider.Reference, owner *user.User) (*provider.StatResponse, error) {
|
||||
ownerCtx, err := p.getAuthContext(owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.gwClient.Stat(ownerCtx, &provider.StatRequest{Ref: ref})
|
||||
}
|
||||
|
||||
func (p *Provider) getPath(id *provider.ResourceId, owner *user.User) (*provider.GetPathResponse, error) {
|
||||
ownerCtx, err := p.getAuthContext(owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.gwClient.GetPath(ownerCtx, &provider.GetPathRequest{ResourceId: id})
|
||||
}
|
||||
|
||||
func (p *Provider) getAuthContext(owner *user.User) (context.Context, error) {
|
||||
ownerCtx := ctxpkg.ContextSetUser(context.Background(), owner)
|
||||
authRes, err := p.gwClient.Authenticate(ownerCtx, &gateway.AuthenticateRequest{
|
||||
Type: "machine",
|
||||
ClientId: "userid:" + owner.Id.OpaqueId,
|
||||
ClientSecret: p.machineAuthAPIKey,
|
||||
})
|
||||
if err == nil && authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
err = errtypes.NewErrtypeFromStatus(authRes.Status)
|
||||
}
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Interface("authRes", authRes).Msg("error using machine auth")
|
||||
return nil, err
|
||||
}
|
||||
return metadata.AppendToOutgoingContext(ownerCtx, ctxpkg.TokenHeader, authRes.Token), nil
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
|
||||
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/search/mocks"
|
||||
provider "github.com/owncloud/ocis/v2/extensions/search/pkg/search/provider"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
var _ = Describe("Searchprovider", func() {
|
||||
var (
|
||||
p *provider.Provider
|
||||
gwClient *cs3mocks.GatewayAPIClient
|
||||
indexClient *mocks.IndexClient
|
||||
|
||||
ctx context.Context
|
||||
eventsChan chan interface{}
|
||||
|
||||
logger = log.NewLogger()
|
||||
user = &userv1beta1.User{
|
||||
Id: &userv1beta1.UserId{
|
||||
OpaqueId: "user",
|
||||
},
|
||||
}
|
||||
|
||||
ref = &sprovider.Reference{
|
||||
ResourceId: &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "rootopaqueid",
|
||||
},
|
||||
Path: "./foo.pdf",
|
||||
}
|
||||
ri = &sprovider.ResourceInfo{
|
||||
Id: &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "opaqueid",
|
||||
},
|
||||
Path: "foo.pdf",
|
||||
Size: 12345,
|
||||
}
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
eventsChan = make(chan interface{})
|
||||
gwClient = &cs3mocks.GatewayAPIClient{}
|
||||
indexClient = &mocks.IndexClient{}
|
||||
|
||||
p = provider.New(gwClient, indexClient, "", eventsChan, logger)
|
||||
|
||||
gwClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Token: "authtoken",
|
||||
}, nil)
|
||||
gwClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
|
||||
Status: status.NewOK(context.Background()),
|
||||
Info: ri,
|
||||
}, nil)
|
||||
indexClient.On("DocCount").Return(uint64(1), nil)
|
||||
})
|
||||
|
||||
Describe("New", func() {
|
||||
It("returns a new instance", func() {
|
||||
p = provider.New(gwClient, indexClient, "", eventsChan, logger)
|
||||
Expect(p).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("events", func() {
|
||||
It("triggers an index update when a file has been uploaded", func() {
|
||||
called := false
|
||||
indexClient.On("Add", mock.Anything, mock.MatchedBy(func(riToIndex *sprovider.ResourceInfo) bool {
|
||||
return riToIndex.Id.OpaqueId == ri.Id.OpaqueId
|
||||
})).Return(nil).Run(func(args mock.Arguments) {
|
||||
called = true
|
||||
})
|
||||
eventsChan <- events.FileUploaded{
|
||||
Ref: ref,
|
||||
Executant: user.Id,
|
||||
}
|
||||
|
||||
Eventually(func() bool {
|
||||
return called
|
||||
}, "2s").Should(BeTrue())
|
||||
})
|
||||
|
||||
It("triggers an index update when a file has been touched", func() {
|
||||
called := false
|
||||
indexClient.On("Add", mock.Anything, mock.MatchedBy(func(riToIndex *sprovider.ResourceInfo) bool {
|
||||
return riToIndex.Id.OpaqueId == ri.Id.OpaqueId
|
||||
})).Return(nil).Run(func(args mock.Arguments) {
|
||||
called = true
|
||||
})
|
||||
eventsChan <- events.FileTouched{
|
||||
Ref: ref,
|
||||
Executant: user.Id,
|
||||
}
|
||||
|
||||
Eventually(func() bool {
|
||||
return called
|
||||
}, "2s").Should(BeTrue())
|
||||
})
|
||||
|
||||
It("removes an entry from the index when the file has been deleted", func() {
|
||||
called := false
|
||||
gwClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
|
||||
Status: status.NewNotFound(context.Background(), ""),
|
||||
}, nil)
|
||||
indexClient.On("Delete", mock.MatchedBy(func(id *sprovider.ResourceId) bool {
|
||||
return id.OpaqueId == ri.Id.OpaqueId
|
||||
})).Return(nil).Run(func(args mock.Arguments) {
|
||||
called = true
|
||||
})
|
||||
eventsChan <- events.ItemTrashed{
|
||||
Ref: ref,
|
||||
ID: ri.Id,
|
||||
Executant: user.Id,
|
||||
}
|
||||
|
||||
Eventually(func() bool {
|
||||
return called
|
||||
}, "2s").Should(BeTrue())
|
||||
})
|
||||
|
||||
It("indexes items when they are being restored", func() {
|
||||
called := false
|
||||
indexClient.On("Restore", mock.MatchedBy(func(id *sprovider.ResourceId) bool {
|
||||
return id.OpaqueId == ri.Id.OpaqueId
|
||||
})).Return(nil).Run(func(args mock.Arguments) {
|
||||
called = true
|
||||
})
|
||||
eventsChan <- events.ItemRestored{
|
||||
Ref: ref,
|
||||
Executant: user.Id,
|
||||
}
|
||||
|
||||
Eventually(func() bool {
|
||||
return called
|
||||
}, "2s").Should(BeTrue())
|
||||
})
|
||||
|
||||
It("indexes items when a version has been restored", func() {
|
||||
called := false
|
||||
indexClient.On("Add", mock.Anything, mock.MatchedBy(func(riToIndex *sprovider.ResourceInfo) bool {
|
||||
return riToIndex.Id.OpaqueId == ri.Id.OpaqueId
|
||||
})).Return(nil).Run(func(args mock.Arguments) {
|
||||
called = true
|
||||
})
|
||||
eventsChan <- events.FileVersionRestored{
|
||||
Ref: ref,
|
||||
Executant: user.Id,
|
||||
}
|
||||
|
||||
Eventually(func() bool {
|
||||
return called
|
||||
}, "2s").Should(BeTrue())
|
||||
})
|
||||
|
||||
It("indexes items when they are being moved", func() {
|
||||
called := false
|
||||
gwClient.On("GetPath", mock.Anything, mock.Anything).Return(&sprovider.GetPathResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Path: "./new/path.pdf",
|
||||
}, nil)
|
||||
indexClient.On("Move", mock.MatchedBy(func(id *sprovider.ResourceId) bool {
|
||||
return id.OpaqueId == ri.Id.OpaqueId
|
||||
}), "./new/path.pdf").Return(nil).Run(func(args mock.Arguments) {
|
||||
called = true
|
||||
})
|
||||
ref.Path = "./new/path.pdf"
|
||||
eventsChan <- events.ItemMoved{
|
||||
Ref: ref,
|
||||
Executant: user.Id,
|
||||
}
|
||||
|
||||
Eventually(func() bool {
|
||||
return called
|
||||
}, "2s").Should(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestProvider(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Provider Suite")
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/errtypes"
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
sdk "github.com/cs3org/reva/v2/pkg/sdk/common"
|
||||
"github.com/cs3org/reva/v2/pkg/storage/utils/walker"
|
||||
"github.com/cs3org/reva/v2/pkg/storagespace"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/search"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0"
|
||||
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
)
|
||||
|
||||
var ListenEvents = []events.Unmarshaller{
|
||||
events.ItemTrashed{},
|
||||
events.ItemRestored{},
|
||||
events.ItemMoved{},
|
||||
events.ContainerCreated{},
|
||||
events.FileUploaded{},
|
||||
events.FileTouched{},
|
||||
events.FileVersionRestored{},
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
logger log.Logger
|
||||
gwClient gateway.GatewayAPIClient
|
||||
indexClient search.IndexClient
|
||||
machineAuthAPIKey string
|
||||
}
|
||||
|
||||
func New(gwClient gateway.GatewayAPIClient, indexClient search.IndexClient, machineAuthAPIKey string, eventsChan <-chan interface{}, logger log.Logger) *Provider {
|
||||
p := &Provider{
|
||||
gwClient: gwClient,
|
||||
indexClient: indexClient,
|
||||
machineAuthAPIKey: machineAuthAPIKey,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
ev := <-eventsChan
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second) // Give some time to let everything settle down before trying to access it when indexing
|
||||
p.handleEvent(ev)
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Provider) Search(ctx context.Context, req *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) {
|
||||
if req.Query == "" {
|
||||
return nil, errtypes.PreconditionFailed("empty query provided")
|
||||
}
|
||||
p.logger.Debug().Str("query", req.Query).Msg("performing a search")
|
||||
|
||||
listSpacesRes, err := p.gwClient.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{
|
||||
Filters: []*provider.ListStorageSpacesRequest_Filter{
|
||||
{
|
||||
Type: provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
|
||||
Term: &provider.ListStorageSpacesRequest_Filter_SpaceType{SpaceType: "+grant"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("failed to list the user's storage spaces")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mountpointMap := map[string]string{}
|
||||
for _, space := range listSpacesRes.StorageSpaces {
|
||||
if space.SpaceType != "mountpoint" {
|
||||
continue
|
||||
}
|
||||
opaqueMap := sdk.DecodeOpaqueMap(space.Opaque)
|
||||
grantSpaceId := storagespace.FormatResourceID(provider.ResourceId{
|
||||
StorageId: opaqueMap["grantStorageID"],
|
||||
OpaqueId: opaqueMap["grantOpaqueID"],
|
||||
})
|
||||
mountpointMap[grantSpaceId] = space.Id.OpaqueId
|
||||
}
|
||||
|
||||
matches := []*searchmsg.Match{}
|
||||
for _, space := range listSpacesRes.StorageSpaces {
|
||||
var mountpointRootId *searchmsg.ResourceID
|
||||
mountpointPrefix := ""
|
||||
switch space.SpaceType {
|
||||
case "mountpoint":
|
||||
continue // mountpoint spaces are only "links" to the shared spaces. we have to search the shared "grant" space instead
|
||||
case "grant":
|
||||
mountpointId, ok := mountpointMap[space.Id.OpaqueId]
|
||||
if !ok {
|
||||
p.logger.Warn().Interface("space", space).Msg("could not find mountpoint space for grant space")
|
||||
continue
|
||||
}
|
||||
gpRes, err := p.gwClient.GetPath(ctx, &provider.GetPathRequest{
|
||||
ResourceId: space.Root,
|
||||
})
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Str("space", space.Id.OpaqueId).Msg("failed to get path for grant space root")
|
||||
continue
|
||||
}
|
||||
if gpRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
p.logger.Error().Interface("status", gpRes.Status).Str("space", space.Id.OpaqueId).Msg("failed to get path for grant space root")
|
||||
continue
|
||||
}
|
||||
mountpointPrefix = utils.MakeRelativePath(gpRes.Path)
|
||||
sid, oid, err := storagespace.SplitID(mountpointId)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Str("space", space.Id.OpaqueId).Str("mountpointId", mountpointId).Msg("invalid mountpoint space id")
|
||||
continue
|
||||
}
|
||||
mountpointRootId = &searchmsg.ResourceID{
|
||||
StorageId: sid,
|
||||
OpaqueId: oid,
|
||||
}
|
||||
p.logger.Debug().Interface("grantSpace", space).Interface("mountpointRootId", mountpointRootId).Msg("searching a grant")
|
||||
}
|
||||
|
||||
_, rootStorageID := storagespace.SplitStorageID(space.Root.StorageId)
|
||||
res, err := p.indexClient.Search(ctx, &searchsvc.SearchIndexRequest{
|
||||
Query: formatQuery(req.Query),
|
||||
Ref: &searchmsg.Reference{
|
||||
ResourceId: &searchmsg.ResourceID{
|
||||
StorageId: space.Root.StorageId,
|
||||
OpaqueId: rootStorageID,
|
||||
},
|
||||
Path: mountpointPrefix,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Str("space", space.Id.OpaqueId).Msg("failed to search the index")
|
||||
return nil, err
|
||||
}
|
||||
p.logger.Debug().Str("space", space.Id.OpaqueId).Int("hits", len(res.Matches)).Msg("space search done")
|
||||
|
||||
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
|
||||
}
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
|
||||
return &searchsvc.SearchResponse{
|
||||
Matches: matches,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Provider) IndexSpace(ctx context.Context, req *searchsvc.IndexSpaceRequest) (*searchsvc.IndexSpaceResponse, error) {
|
||||
// get user
|
||||
res, err := p.gwClient.GetUserByClaim(context.Background(), &user.GetUserByClaimRequest{
|
||||
Claim: "username",
|
||||
Value: req.UserId,
|
||||
})
|
||||
if err != nil || res.Status.Code != rpc.Code_CODE_OK {
|
||||
fmt.Println("error: Could not get user by userid")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get auth context
|
||||
ownerCtx := ctxpkg.ContextSetUser(context.Background(), res.User)
|
||||
authRes, err := p.gwClient.Authenticate(ownerCtx, &gateway.AuthenticateRequest{
|
||||
Type: "machine",
|
||||
ClientId: "userid:" + res.User.Id.OpaqueId,
|
||||
ClientSecret: p.machineAuthAPIKey,
|
||||
})
|
||||
if err != nil || authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return nil, fmt.Errorf("could not get authenticated context for user")
|
||||
}
|
||||
ownerCtx = metadata.AppendToOutgoingContext(ownerCtx, ctxpkg.TokenHeader, authRes.Token)
|
||||
|
||||
// Walk the space and index all files
|
||||
walker := walker.NewWalker(p.gwClient)
|
||||
rootId := &provider.ResourceId{StorageId: req.SpaceId, OpaqueId: req.SpaceId}
|
||||
err = walker.Walk(ownerCtx, rootId, func(wd string, info *provider.ResourceInfo, err error) error {
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("error walking the tree")
|
||||
}
|
||||
ref := &provider.Reference{
|
||||
Path: utils.MakeRelativePath(filepath.Join(wd, info.Path)),
|
||||
ResourceId: rootId,
|
||||
}
|
||||
err = p.indexClient.Add(ref, info)
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("error adding resource to the index")
|
||||
} else {
|
||||
p.logger.Debug().Interface("ref", ref).Msg("added resource to index")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.logDocCount()
|
||||
return &searchsvc.IndexSpaceResponse{}, nil
|
||||
}
|
||||
|
||||
func (p *Provider) logDocCount() {
|
||||
c, err := p.indexClient.DocCount()
|
||||
if err != nil {
|
||||
p.logger.Error().Err(err).Msg("error getting document count from the index")
|
||||
}
|
||||
p.logger.Debug().Interface("count", c).Msg("new document count")
|
||||
}
|
||||
|
||||
func formatQuery(q string) string {
|
||||
query := q
|
||||
fields := []string{"RootID", "Path", "ID", "Name", "Size", "Mtime", "MimeType", "Type"}
|
||||
for _, field := range fields {
|
||||
query = strings.ReplaceAll(query, strings.ToLower(field)+":", field+":")
|
||||
}
|
||||
|
||||
if strings.Contains(query, ":") {
|
||||
return query // Sophisticated field based search
|
||||
}
|
||||
|
||||
// this is a basic filename search
|
||||
return "Name:*" + strings.ReplaceAll(strings.ToLower(query), " ", `\ `) + "*"
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
|
||||
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/search/mocks"
|
||||
provider "github.com/owncloud/ocis/v2/extensions/search/pkg/search/provider"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0"
|
||||
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
)
|
||||
|
||||
var _ = Describe("Searchprovider", func() {
|
||||
var (
|
||||
p *provider.Provider
|
||||
gwClient *cs3mocks.GatewayAPIClient
|
||||
indexClient *mocks.IndexClient
|
||||
|
||||
ctx context.Context
|
||||
eventsChan chan interface{}
|
||||
|
||||
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: "personalspace"},
|
||||
Root: &sprovider.ResourceId{StorageId: "storageid", OpaqueId: "storageid"},
|
||||
Name: "personalspace",
|
||||
}
|
||||
|
||||
ri = &sprovider.ResourceInfo{
|
||||
Id: &sprovider.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "opaqueid",
|
||||
},
|
||||
Path: "foo.pdf",
|
||||
Size: 12345,
|
||||
}
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
eventsChan = make(chan interface{})
|
||||
gwClient = &cs3mocks.GatewayAPIClient{}
|
||||
indexClient = &mocks.IndexClient{}
|
||||
|
||||
p = provider.New(gwClient, indexClient, "", eventsChan, logger)
|
||||
|
||||
gwClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Token: "authtoken",
|
||||
}, nil)
|
||||
gwClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
|
||||
Status: status.NewOK(context.Background()),
|
||||
Info: ri,
|
||||
}, nil)
|
||||
indexClient.On("DocCount").Return(uint64(1), nil)
|
||||
})
|
||||
|
||||
Describe("New", func() {
|
||||
It("returns a new instance", func() {
|
||||
p := provider.New(gwClient, indexClient, "", eventsChan, logger)
|
||||
Expect(p).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("IndexSpace", func() {
|
||||
It("walks the space and indexes all files", func() {
|
||||
gwClient.On("GetUserByClaim", mock.Anything, mock.Anything).Return(&userv1beta1.GetUserByClaimResponse{
|
||||
Status: status.NewOK(context.Background()),
|
||||
User: user,
|
||||
}, nil)
|
||||
indexClient.On("Add", mock.Anything, mock.MatchedBy(func(riToIndex *sprovider.ResourceInfo) bool {
|
||||
return riToIndex.Id.OpaqueId == ri.Id.OpaqueId
|
||||
})).Return(nil)
|
||||
|
||||
res, err := p.IndexSpace(ctx, &searchsvc.IndexSpaceRequest{
|
||||
SpaceId: "storageid",
|
||||
UserId: "user",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Search", func() {
|
||||
It("fails when an empty query is given", func() {
|
||||
res, err := p.Search(ctx, &searchsvc.SearchRequest{
|
||||
Query: "",
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(res).To(BeNil())
|
||||
})
|
||||
|
||||
Context("with a personal space", func() {
|
||||
BeforeEach(func() {
|
||||
gwClient.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{
|
||||
Matches: []*searchmsg.Match{
|
||||
{
|
||||
Entity: &searchmsg.Entity{
|
||||
Ref: &searchmsg.Reference{
|
||||
ResourceId: &searchmsg.ResourceID{
|
||||
StorageId: personalSpace.Root.StorageId,
|
||||
OpaqueId: personalSpace.Root.OpaqueId,
|
||||
},
|
||||
Path: "./path/to/Foo.pdf",
|
||||
},
|
||||
Id: &searchmsg.ResourceID{
|
||||
StorageId: personalSpace.Root.StorageId,
|
||||
OpaqueId: "foo-id",
|
||||
},
|
||||
Name: "Foo.pdf",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
})
|
||||
|
||||
It("lowercases the filename", func() {
|
||||
p.Search(ctx, &searchsvc.SearchRequest{
|
||||
Query: "Foo.pdf",
|
||||
})
|
||||
indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
|
||||
return req.Query == "Name:*foo.pdf*"
|
||||
}))
|
||||
})
|
||||
|
||||
It("does not mess with field-based searches", func() {
|
||||
p.Search(ctx, &searchsvc.SearchRequest{
|
||||
Query: "Size:<10",
|
||||
})
|
||||
indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
|
||||
return req.Query == "Size:<10"
|
||||
}))
|
||||
})
|
||||
|
||||
It("uppercases field names", func() {
|
||||
tests := []struct {
|
||||
Original string
|
||||
Expected string
|
||||
}{
|
||||
{Original: "size:<100", Expected: "Size:<100"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
p.Search(ctx, &searchsvc.SearchRequest{
|
||||
Query: test.Original,
|
||||
})
|
||||
indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
|
||||
return req.Query == test.Expected
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
It("escapes special characters", func() {
|
||||
p.Search(ctx, &searchsvc.SearchRequest{
|
||||
Query: "Foo oo.pdf",
|
||||
})
|
||||
indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
|
||||
return req.Query == `Name:*foo\ oo.pdf*`
|
||||
}))
|
||||
})
|
||||
|
||||
It("searches the personal user space", func() {
|
||||
res, err := p.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("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"))
|
||||
|
||||
indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
|
||||
return req.Query == "Name:*foo*" && req.Ref.ResourceId.OpaqueId == personalSpace.Root.OpaqueId && req.Ref.Path == ""
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
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: "otherspaceroot!otherspacegrant"},
|
||||
Root: &sprovider.ResourceId{StorageId: "otherspaceroot", OpaqueId: "otherspacegrant"},
|
||||
Name: "grantspace",
|
||||
}
|
||||
mountpointSpace = &sprovider.StorageSpace{
|
||||
SpaceType: "mountpoint",
|
||||
Owner: otherUser,
|
||||
Id: &sprovider.StorageSpaceId{OpaqueId: "otherspaceroot!otherspacemountpoint"},
|
||||
Root: &sprovider.ResourceId{StorageId: "otherspaceroot", OpaqueId: "otherspacemountpoint"},
|
||||
Name: "mountpointspace",
|
||||
Opaque: &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"grantStorageID": {Decoder: "plain", Value: []byte("otherspaceroot")},
|
||||
"grantOpaqueID": {Decoder: "plain", Value: []byte("otherspacegrant")},
|
||||
},
|
||||
},
|
||||
}
|
||||
gwClient.On("GetPath", mock.Anything, mock.Anything).Return(&sprovider.GetPathResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Path: "/grant/path",
|
||||
}, nil)
|
||||
})
|
||||
|
||||
It("searches the received spaces", func() {
|
||||
gwClient.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{
|
||||
Matches: []*searchmsg.Match{
|
||||
{
|
||||
Entity: &searchmsg.Entity{
|
||||
Ref: &searchmsg.Reference{
|
||||
ResourceId: &searchmsg.ResourceID{
|
||||
StorageId: grantSpace.Root.StorageId,
|
||||
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 := p.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"))
|
||||
|
||||
indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
|
||||
return req.Query == "Name:*foo*" && req.Ref.ResourceId.StorageId == grantSpace.Root.StorageId && req.Ref.Path == "./grant/path"
|
||||
}))
|
||||
})
|
||||
|
||||
It("finds matches in both the personal space AND the grant", func() {
|
||||
gwClient.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.StorageId == grantSpace.Root.StorageId
|
||||
})).Return(&searchsvc.SearchIndexResponse{
|
||||
Matches: []*searchmsg.Match{
|
||||
{
|
||||
Entity: &searchmsg.Entity{
|
||||
Ref: &searchmsg.Reference{
|
||||
ResourceId: &searchmsg.ResourceID{
|
||||
StorageId: grantSpace.Root.StorageId,
|
||||
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)
|
||||
indexClient.On("Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool {
|
||||
return req.Ref.ResourceId.StorageId == personalSpace.Root.StorageId
|
||||
})).Return(&searchsvc.SearchIndexResponse{
|
||||
Matches: []*searchmsg.Match{
|
||||
{
|
||||
Entity: &searchmsg.Entity{
|
||||
Ref: &searchmsg.Reference{
|
||||
ResourceId: &searchmsg.ResourceID{
|
||||
StorageId: personalSpace.Root.StorageId,
|
||||
OpaqueId: personalSpace.Root.OpaqueId,
|
||||
},
|
||||
Path: "./path/to/Foo.pdf",
|
||||
},
|
||||
Id: &searchmsg.ResourceID{
|
||||
StorageId: personalSpace.Root.StorageId,
|
||||
OpaqueId: "foo-id",
|
||||
},
|
||||
Name: "Foo.pdf",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
res, err := p.Search(ctx, &searchsvc.SearchRequest{
|
||||
Query: "foo",
|
||||
})
|
||||
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(ConsistOf("foo-id", "grant-shared-id"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2018-2022 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
)
|
||||
|
||||
//go:generate mockery --name=ProviderClient
|
||||
//go:generate mockery --name=IndexClient
|
||||
|
||||
// ProviderClient is the interface to the search provider service
|
||||
type ProviderClient interface {
|
||||
Search(ctx context.Context, req *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error)
|
||||
IndexSpace(ctx context.Context, req *searchsvc.IndexSpaceRequest) (*searchsvc.IndexSpaceResponse, error)
|
||||
}
|
||||
|
||||
// IndexClient is the interface to the search index
|
||||
type IndexClient interface {
|
||||
Search(ctx context.Context, req *searchsvc.SearchIndexRequest) (*searchsvc.SearchIndexResponse, error)
|
||||
Add(ref *providerv1beta1.Reference, ri *providerv1beta1.ResourceInfo) error
|
||||
Move(id *providerv1beta1.ResourceId, fullPath string) error
|
||||
Delete(id *providerv1beta1.ResourceId) error
|
||||
Restore(id *providerv1beta1.ResourceId) error
|
||||
Purge(id *providerv1beta1.ResourceId) error
|
||||
DocCount() (uint64, error)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package search_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Search Suite")
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/debug"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
)
|
||||
|
||||
// Server initializes the debug service and server.
|
||||
func Server(opts ...Option) (*http.Server, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
return debug.NewService(
|
||||
debug.Logger(options.Logger),
|
||||
debug.Name(options.Config.Service.Name),
|
||||
debug.Version(version.GetString()),
|
||||
debug.Address(options.Config.Debug.Addr),
|
||||
debug.Token(options.Config.Debug.Token),
|
||||
debug.Pprof(options.Config.Debug.Pprof),
|
||||
debug.Zpages(options.Config.Debug.Zpages),
|
||||
debug.Health(health(options.Config)),
|
||||
debug.Ready(ready(options.Config)),
|
||||
), nil
|
||||
}
|
||||
|
||||
// health implements the health check.
|
||||
func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// TODO: check if services are up and running
|
||||
|
||||
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
|
||||
// io.WriteString should not fail but if it does we want to know.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ready implements the ready check.
|
||||
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// TODO: check if services are up and running
|
||||
|
||||
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
|
||||
// io.WriteString should not fail but if it does we want to know.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/metrics"
|
||||
svc "github.com/owncloud/ocis/v2/extensions/search/pkg/service/v0"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Name string
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
Metrics *metrics.Metrics
|
||||
Flags []cli.Flag
|
||||
Handler *svc.Service
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Name provides a name for the service.
|
||||
func Name(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = val
|
||||
}
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics provides a function to set the metrics option.
|
||||
func Metrics(val *metrics.Metrics) Option {
|
||||
return func(o *Options) {
|
||||
o.Metrics = val
|
||||
}
|
||||
}
|
||||
|
||||
// Flags provides a function to set the flags option.
|
||||
func Flags(val []cli.Flag) Option {
|
||||
return func(o *Options) {
|
||||
o.Flags = append(o.Flags, val...)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler provides a function to set the handler option.
|
||||
func Handler(val *svc.Service) Option {
|
||||
return func(o *Options) {
|
||||
o.Handler = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
svc "github.com/owncloud/ocis/v2/extensions/search/pkg/service/v0"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
)
|
||||
|
||||
// Server initializes a new go-micro service ready to run
|
||||
func Server(opts ...Option) grpc.Service {
|
||||
options := newOptions(opts...)
|
||||
|
||||
service := grpc.NewService(
|
||||
grpc.Name(options.Config.Service.Name),
|
||||
grpc.Context(options.Context),
|
||||
grpc.Address(options.Config.GRPC.Addr),
|
||||
grpc.Namespace(options.Config.GRPC.Namespace),
|
||||
grpc.Logger(options.Logger),
|
||||
grpc.Flags(options.Flags...),
|
||||
grpc.Version(version.GetString()),
|
||||
)
|
||||
|
||||
handle, err := svc.NewHandler(
|
||||
svc.Config(options.Config),
|
||||
svc.Logger(options.Logger),
|
||||
)
|
||||
if err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Msg("Error initializing search service")
|
||||
return grpc.Service{}
|
||||
}
|
||||
_ = searchsvc.RegisterSearchProviderHandler(
|
||||
service.Server(),
|
||||
handle,
|
||||
)
|
||||
return service
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the Logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the Config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/cs3org/reva/v2/pkg/events/server"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/go-micro/plugins/v4/events/natsjs"
|
||||
"go-micro.dev/v4/metadata"
|
||||
grpcmetadata "google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/search"
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/search/index"
|
||||
searchprovider "github.com/owncloud/ocis/v2/extensions/search/pkg/search/provider"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
|
||||
)
|
||||
|
||||
// NewHandler returns a service implementation for Service.
|
||||
func NewHandler(opts ...Option) (searchsvc.SearchProviderHandler, error) {
|
||||
options := newOptions(opts...)
|
||||
logger := options.Logger
|
||||
cfg := options.Config
|
||||
|
||||
// Connect to nats to listen for changes that need to trigger an index update
|
||||
evtsCfg := cfg.Events
|
||||
client, err := server.NewNatsStream(
|
||||
natsjs.Address(evtsCfg.Endpoint),
|
||||
natsjs.ClusterID(evtsCfg.Cluster),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
evts, err := events.Consume(client, evtsCfg.ConsumerGroup, searchprovider.ListenEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexDir := filepath.Join(cfg.Datapath, "index.bleve")
|
||||
bleveIndex, err := bleve.Open(indexDir)
|
||||
if err != nil {
|
||||
mapping, err := index.BuildMapping()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bleveIndex, err = bleve.New(indexDir, mapping)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
index, err := index.New(bleveIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gwclient, err := pool.GetGatewayServiceClient(cfg.Reva.Address)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Str("addr", cfg.Reva.Address).Msg("could not get reva client")
|
||||
}
|
||||
|
||||
provider := searchprovider.New(gwclient, index, cfg.MachineAuthAPIKey, evts, logger)
|
||||
|
||||
return &Service{
|
||||
id: cfg.GRPC.Namespace + "." + cfg.Service.Name,
|
||||
log: logger,
|
||||
Config: cfg,
|
||||
provider: provider,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Service implements the searchServiceHandler interface
|
||||
type Service struct {
|
||||
id string
|
||||
log log.Logger
|
||||
Config *config.Config
|
||||
provider search.ProviderClient
|
||||
}
|
||||
|
||||
func (s Service) Search(ctx context.Context, in *searchsvc.SearchRequest, out *searchsvc.SearchResponse) error {
|
||||
// Get token from the context (go-micro) and make it known to the reva client too (grpc)
|
||||
t, ok := metadata.Get(ctx, revactx.TokenHeader)
|
||||
if !ok {
|
||||
s.log.Error().Msg("Could not get token from context")
|
||||
return errors.New("could not get token from context")
|
||||
}
|
||||
ctx = grpcmetadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, t)
|
||||
|
||||
res, err := s.provider.Search(ctx, &searchsvc.SearchRequest{
|
||||
Query: in.Query,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out.Matches = res.Matches
|
||||
out.NextPageToken = res.NextPageToken
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Service) IndexSpace(ctx context.Context, in *searchsvc.IndexSpaceRequest, out *searchsvc.IndexSpaceResponse) error {
|
||||
_, err := s.provider.IndexSpace(ctx, in)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/extensions/search/pkg/config"
|
||||
pkgtrace "github.com/owncloud/ocis/v2/ocis-pkg/tracing"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
// TraceProvider is the global trace provider for the proxy service.
|
||||
TraceProvider = trace.NewNoopTracerProvider()
|
||||
)
|
||||
|
||||
func Configure(cfg *config.Config) error {
|
||||
var err error
|
||||
if cfg.Tracing.Enabled {
|
||||
if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user