diff --git a/changelog/unreleased/search-extension.md b/changelog/unreleased/search-extension.md new file mode 100644 index 00000000000..c4e7bffa4bb --- /dev/null +++ b/changelog/unreleased/search-extension.md @@ -0,0 +1,5 @@ +Enhancement: Add initial version of the search extensions + +It is now possible to search for files and directories by their name using the web UI. Therefor new search extension indexes files in a persistent local index. + +https://github.com/owncloud/ocis/pull/3635 diff --git a/docs/extensions/port-ranges.md b/docs/extensions/port-ranges.md index ac28f54c53c..bc3143c08e7 100644 --- a/docs/extensions/port-ranges.md +++ b/docs/extensions/port-ranges.md @@ -42,7 +42,7 @@ We also suggest to use the last port in your extensions' range as a debug/metric | 9205-9209 | [markdown-editor](https://github.com/owncloud/ocis-markdown-editor) | | 9210-9214 | [reva](https://github.com/owncloud/ocis-reva) unused? | | 9215-9219 | reva metadata storage | -| 9220-9224 | FREE | +| 9220-9224 | search | | 9225-9229 | photoprism (state: PoC) | | 9230-9234 | [nats](https://github.com/owncloud/ocis/tree/master/nats) | | 9235-9239 | idm TBD | diff --git a/docs/extensions/storage/ports.md b/docs/extensions/storage/ports.md index 2ab34f5dd92..62bd6ac82e1 100644 --- a/docs/extensions/storage/ports.md +++ b/docs/extensions/storage/ports.md @@ -34,10 +34,13 @@ For now, the storage service uses these ports to preconfigure those services: | 9159 | storage users debug | | 9160 | groups | | 9161 | groups debug | -| 9164 | storage appprovider | -| 9165 | storage appprovider debug | +| 9164 | storage appprovider | +| 9165 | storage appprovider debug | | 9178 | storage public link | | 9179 | storage public link data | +| 9180 | accounts grpc | +| 9181 | accounts http | +| 9182 | accounts debug | | 9215 | storage meta grpc | | 9216 | storage meta http | | 9217 | storage meta debug | diff --git a/extensions/audit/pkg/types/events.go b/extensions/audit/pkg/types/events.go index 1862b1f8070..2fe9d9aef6f 100644 --- a/extensions/audit/pkg/types/events.go +++ b/extensions/audit/pkg/types/events.go @@ -16,6 +16,7 @@ func RegisteredEvents() []events.Unmarshaller { events.ReceivedShareUpdated{}, events.LinkAccessed{}, events.LinkAccessFailed{}, + events.ContainerCreated{}, events.FileUploaded{}, events.FileDownloaded{}, events.ItemTrashed{}, diff --git a/extensions/frontend/pkg/command/command.go b/extensions/frontend/pkg/command/command.go index b613e8fc76a..d14917d1c6a 100644 --- a/extensions/frontend/pkg/command/command.go +++ b/extensions/frontend/pkg/command/command.go @@ -236,7 +236,9 @@ func frontendConfigFromStruct(c *cli.Context, cfg *config.Config, filesCfg map[s "preferred_upload_type": cfg.Checksums.PreferredUploadType, }, "files": filesCfg, - "dav": map[string]interface{}{}, + "dav": map[string]interface{}{ + "reports": []string{"search-files"}, + }, "files_sharing": map[string]interface{}{ "api_enabled": true, "resharing": false, diff --git a/extensions/proxy/pkg/config/config.go b/extensions/proxy/pkg/config/config.go index b1959b9ccd5..f6264f413d6 100644 --- a/extensions/proxy/pkg/config/config.go +++ b/extensions/proxy/pkg/config/config.go @@ -45,8 +45,10 @@ type Policy struct { // Route defines forwarding routes type Route struct { - Type RouteType `yaml:"type"` - Endpoint string `yaml:"endpoint"` + Type RouteType `yaml:"type"` + // Method optionally limits the route to this HTTP method + Method string `yaml:"method"` + Endpoint string `yaml:"endpoint"` // Backend is a static URL to forward the request to Backend string `yaml:"backend"` // Service name to look up in the registry diff --git a/extensions/proxy/pkg/config/defaults/defaultconfig.go b/extensions/proxy/pkg/config/defaults/defaultconfig.go index 1b45e273f84..c04ed2e27c4 100644 --- a/extensions/proxy/pkg/config/defaults/defaultconfig.go +++ b/extensions/proxy/pkg/config/defaults/defaultconfig.go @@ -97,6 +97,15 @@ func DefaultPolicies() []config.Policy { Endpoint: "/remote.php/?preview=1", Backend: "http://localhost:9115", }, + { + // TODO the actual REPORT goes to /dav/files/{username}, which is user specific ... how would this work in a spaces world? + // TODO what paths are returned? the href contains the full path so it should be possible to return urls from other spaces? + // TODO or we allow a REPORT on /dav/spaces to search all spaces and /dav/space/{spaceid} to search a specific space + // send webdav REPORT requests to search service + Method: "REPORT", + Endpoint: "/remote.php/dav/", + Backend: "http://localhost:9115", // TODO use registry? + }, { Endpoint: "/remote.php/", Service: "ocdav", diff --git a/extensions/proxy/pkg/proxy/proxy.go b/extensions/proxy/pkg/proxy/proxy.go index ba9ce4addc4..b910452c256 100644 --- a/extensions/proxy/pkg/proxy/proxy.go +++ b/extensions/proxy/pkg/proxy/proxy.go @@ -29,7 +29,8 @@ import ( // MultiHostReverseProxy extends "httputil" to support multiple hosts with different policies type MultiHostReverseProxy struct { httputil.ReverseProxy - Directors map[string]map[config.RouteType]map[string]func(req *http.Request) + // Directors holds policy route type method endpoint Director + Directors map[string]map[config.RouteType]map[string]map[string]func(req *http.Request) PolicySelector policy.Selector logger log.Logger config *config.Config @@ -40,7 +41,7 @@ func NewMultiHostReverseProxy(opts ...Option) *MultiHostReverseProxy { options := newOptions(opts...) rp := &MultiHostReverseProxy{ - Directors: make(map[string]map[config.RouteType]map[string]func(req *http.Request)), + Directors: make(map[string]map[config.RouteType]map[string]map[string]func(req *http.Request)), logger: options.Logger, config: options.Config, } @@ -124,6 +125,7 @@ func (p *MultiHostReverseProxy) directorSelectionDirector(r *http.Request) { return } + method := "" // find matching director for _, rt := range config.RouteTypes { var handler func(string, url.URL) bool @@ -137,25 +139,36 @@ func (p *MultiHostReverseProxy) directorSelectionDirector(r *http.Request) { default: handler = p.prefixRouteMatcher } - for endpoint := range p.Directors[pol][rt] { + if p.Directors[pol][rt][r.Method] != nil { + // use specific method + method = r.Method + } + for endpoint := range p.Directors[pol][rt][method] { if handler(endpoint, *r.URL) { p.logger.Debug(). Str("policy", pol). + Str("method", r.Method). Str("prefix", endpoint). Str("path", r.URL.Path). Str("routeType", string(rt)). Msg("director found") - p.Directors[pol][rt][endpoint](r) + p.Directors[pol][rt][method][endpoint](r) return } } } // override default director with root. If any - if p.Directors[pol][config.PrefixRoute]["/"] != nil { - p.Directors[pol][config.PrefixRoute]["/"](r) + switch { + case p.Directors[pol][config.PrefixRoute][method]["/"] != nil: + // try specific method + p.Directors[pol][config.PrefixRoute][method]["/"](r) + return + case p.Directors[pol][config.PrefixRoute][""]["/"] != nil: + // fallback to unspecific method + p.Directors[pol][config.PrefixRoute][""]["/"](r) return } @@ -182,20 +195,23 @@ func singleJoiningSlash(a, b string) string { func (p *MultiHostReverseProxy) AddHost(policy string, target *url.URL, rt config.Route) { targetQuery := target.RawQuery if p.Directors[policy] == nil { - p.Directors[policy] = make(map[config.RouteType]map[string]func(req *http.Request)) + p.Directors[policy] = make(map[config.RouteType]map[string]map[string]func(req *http.Request)) } routeType := config.DefaultRouteType if rt.Type != "" { routeType = rt.Type } if p.Directors[policy][routeType] == nil { - p.Directors[policy][routeType] = make(map[string]func(req *http.Request)) + p.Directors[policy][routeType] = make(map[string]map[string]func(req *http.Request)) + } + if p.Directors[policy][routeType][rt.Method] == nil { + p.Directors[policy][routeType][rt.Method] = make(map[string]func(req *http.Request)) } reg := registry.GetRegistry() sel := selector.NewSelector(selector.Registry(reg)) - p.Directors[policy][routeType][rt.Endpoint] = func(req *http.Request) { + p.Directors[policy][routeType][rt.Method][rt.Endpoint] = func(req *http.Request) { if rt.Service != "" { // select next node next, err := sel.Select(rt.Service) diff --git a/extensions/proxy/pkg/proxy/proxy_test.go b/extensions/proxy/pkg/proxy/proxy_test.go index a618ffc1b50..2805ada186e 100644 --- a/extensions/proxy/pkg/proxy/proxy_test.go +++ b/extensions/proxy/pkg/proxy/proxy_test.go @@ -1,15 +1,19 @@ package proxy import ( + "fmt" + "net/http" + "net/http/httptest" "net/url" "testing" + "github.com/owncloud/ocis/extensions/proxy/pkg/config" "github.com/owncloud/ocis/extensions/proxy/pkg/config/defaults" ) type matchertest struct { - endpoint, target string - matches bool + method, endpoint, target string + matches bool } func TestPrefixRouteMatcher(t *testing.T) { @@ -99,3 +103,35 @@ func TestSingleJoiningSlash(t *testing.T) { } } } + +func TestDirectorSelectionDirector(t *testing.T) { + + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "ok") + })) + defer svr.Close() + + p := NewMultiHostReverseProxy(Config(&config.Config{ + PolicySelector: &config.PolicySelector{ + Static: &config.StaticSelectorConf{ + Policy: "default", + }, + }, + })) + p.AddHost("default", &url.URL{Host: "ocdav"}, config.Route{Type: config.PrefixRoute, Method: "", Endpoint: "/dav", Backend: "ocdav"}) + p.AddHost("default", &url.URL{Host: "ocis-webdav"}, config.Route{Type: config.PrefixRoute, Method: "REPORT", Endpoint: "/dav", Backend: "ocis-webdav"}) + + table := []matchertest{ + {method: "PROPFIND", endpoint: "/dav/files/demo/", target: "ocdav"}, + {method: "REPORT", endpoint: "/dav/files/demo/", target: "ocis-webdav"}, + } + + for _, test := range table { + r := httptest.NewRequest(test.method, "/dav/files/demo/", nil) + p.directorSelectionDirector(r) + if r.URL.Host != test.target { + t.Errorf("TestDirectorSelectionDirector got host %s expected %s", r.Host, test.target) + + } + } +} diff --git a/extensions/search/cmd/search/main.go b/extensions/search/cmd/search/main.go new file mode 100644 index 00000000000..aa078dc41c8 --- /dev/null +++ b/extensions/search/cmd/search/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "github.com/owncloud/ocis/extensions/search/pkg/command" + "github.com/owncloud/ocis/extensions/search/pkg/config/defaults" +) + +func main() { + if err := command.Execute(defaults.DefaultConfig()); err != nil { + os.Exit(1) + } +} diff --git a/extensions/search/pkg/command/health.go b/extensions/search/pkg/command/health.go new file mode 100644 index 00000000000..ff4e0ec3010 --- /dev/null +++ b/extensions/search/pkg/command/health.go @@ -0,0 +1,53 @@ +package command + +import ( + "fmt" + "net/http" + + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/extensions/search/pkg/config/parser" + "github.com/owncloud/ocis/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 + }, + } +} diff --git a/extensions/search/pkg/command/index.go b/extensions/search/pkg/command/index.go new file mode 100644 index 00000000000..3d57e926425 --- /dev/null +++ b/extensions/search/pkg/command/index.go @@ -0,0 +1,52 @@ +package command + +import ( + "context" + "fmt" + + "github.com/urfave/cli/v2" + + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/extensions/search/pkg/config/parser" + "github.com/owncloud/ocis/ocis-pkg/service/grpc" + searchsvc "github.com/owncloud/ocis/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 + }, + } +} diff --git a/extensions/search/pkg/command/root.go b/extensions/search/pkg/command/root.go new file mode 100644 index 00000000000..a43261631fe --- /dev/null +++ b/extensions/search/pkg/command/root.go @@ -0,0 +1,65 @@ +package command + +import ( + "context" + "os" + + "github.com/owncloud/ocis/ocis-pkg/clihelper" + "github.com/thejerf/suture/v4" + + "github.com/owncloud/ocis/extensions/search/pkg/config" + ociscfg "github.com/owncloud/ocis/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: "ocis-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 +} diff --git a/extensions/search/pkg/command/server.go b/extensions/search/pkg/command/server.go new file mode 100644 index 00000000000..2582213c493 --- /dev/null +++ b/extensions/search/pkg/command/server.go @@ -0,0 +1,83 @@ +package command + +import ( + "context" + "fmt" + + "github.com/oklog/run" + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/extensions/search/pkg/config/parser" + "github.com/owncloud/ocis/extensions/search/pkg/logging" + "github.com/owncloud/ocis/extensions/search/pkg/metrics" + "github.com/owncloud/ocis/extensions/search/pkg/server/debug" + "github.com/owncloud/ocis/extensions/search/pkg/server/grpc" + "github.com/owncloud/ocis/extensions/search/pkg/tracing" + "github.com/owncloud/ocis/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) + } + 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.String).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() + }, + } +} diff --git a/extensions/search/pkg/command/version.go b/extensions/search/pkg/command/version.go new file mode 100644 index 00000000000..12326e77d99 --- /dev/null +++ b/extensions/search/pkg/command/version.go @@ -0,0 +1,50 @@ +package command + +import ( + "fmt" + "os" + + "github.com/owncloud/ocis/ocis-pkg/registry" + "github.com/owncloud/ocis/ocis-pkg/version" + + tw "github.com/olekukonko/tablewriter" + "github.com/owncloud/ocis/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.String) + 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 + }, + } +} diff --git a/extensions/search/pkg/config/config.go b/extensions/search/pkg/config/config.go new file mode 100644 index 00000000000..7c7ac33ff61 --- /dev/null +++ b/extensions/search/pkg/config/config.go @@ -0,0 +1,35 @@ +package config + +import ( + "context" + + "github.com/owncloud/ocis/ocis-pkg/shared" +) + +// Config combines all available configuration parts. +type Config struct { + *shared.Commons `ocisConfig:"-" yaml:"-"` + + Service Service `ocisConfig:"-" yaml:"-"` + + Tracing *Tracing `ocisConfig:"tracing"` + Log *Log `ocisConfig:"log"` + Debug Debug `ocisConfig:"debug"` + + GRPC GRPC `ocisConfig:"grpc"` + + Datapath string `yaml:"data_path" env:"SEARCH_DATA_PATH"` + Reva Reva `ocisConfig:"reva"` + Events Events `yaml:"events"` + + MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;SEARCH_MACHINE_AUTH_API_KEY"` + + Context context.Context `ocisConfig:"-" yaml:"-"` +} + +// Events combines the configuration options for the event bus. +type Events struct { + Endpoint string `yaml:"events_endpoint" env:"SEARCH_EVENTS_ENDPOINT" desc:"the address of the streaming service"` + Cluster string `yaml:"events_cluster" env:"SEARCH_EVENTS_CLUSTER" desc:"the clusterID of the streaming service. Mandatory when using nats"` + ConsumerGroup string `yaml:"events_group" env:"SEARCH_EVENTS_GROUP" desc:"the customergroup of the service. One group will only get one copy of an event"` +} diff --git a/extensions/search/pkg/config/debug.go b/extensions/search/pkg/config/debug.go new file mode 100644 index 00000000000..a6ab80cbdaa --- /dev/null +++ b/extensions/search/pkg/config/debug.go @@ -0,0 +1,9 @@ +package config + +// Debug defines the available debug configuration. +type Debug struct { + Addr string `ocisConfig:"addr" env:"SEARCH_DEBUG_ADDR"` + Token string `ocisConfig:"token" env:"SEARCH_DEBUG_TOKEN"` + Pprof bool `ocisConfig:"pprof" env:"SEARCH_DEBUG_PPROF"` + Zpages bool `ocisConfig:"zpages" env:"SEARCH_DEBUG_ZPAGES"` +} diff --git a/extensions/search/pkg/config/defaults/defaultconfig.go b/extensions/search/pkg/config/defaults/defaultconfig.go new file mode 100644 index 00000000000..c3c40a7fea4 --- /dev/null +++ b/extensions/search/pkg/config/defaults/defaultconfig.go @@ -0,0 +1,75 @@ +package defaults + +import ( + "path" + + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/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: "change-me-please", + } +} + +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 +} diff --git a/extensions/search/pkg/config/grpc.go b/extensions/search/pkg/config/grpc.go new file mode 100644 index 00000000000..148fda92505 --- /dev/null +++ b/extensions/search/pkg/config/grpc.go @@ -0,0 +1,7 @@ +package config + +// GRPC defines the available grpc configuration. +type GRPC struct { + Addr string `ocisConfig:"addr" env:"ACCOUNTS_GRPC_ADDR" desc:"The address of the grpc service."` + Namespace string `ocisConfig:"-" yaml:"-"` +} diff --git a/extensions/search/pkg/config/http.go b/extensions/search/pkg/config/http.go new file mode 100644 index 00000000000..018f8c551f1 --- /dev/null +++ b/extensions/search/pkg/config/http.go @@ -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"` +} diff --git a/extensions/search/pkg/config/log.go b/extensions/search/pkg/config/log.go new file mode 100644 index 00000000000..6e4d2fa9386 --- /dev/null +++ b/extensions/search/pkg/config/log.go @@ -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"` + Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;SEARCH_LOG_PRETTY"` + Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;SEARCH_LOG_COLOR"` + File string `mapstructure:"file" env:"OCIS_LOG_FILE;SEARCH_LOG_FILE"` +} diff --git a/extensions/search/pkg/config/parser/parse.go b/extensions/search/pkg/config/parser/parse.go new file mode 100644 index 00000000000..9183f5be88c --- /dev/null +++ b/extensions/search/pkg/config/parser/parse.go @@ -0,0 +1,41 @@ +package parser + +import ( + "errors" + + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/extensions/search/pkg/config/defaults" + ociscfg "github.com/owncloud/ocis/ocis-pkg/config" + "github.com/owncloud/ocis/ocis-pkg/shared" + + "github.com/owncloud/ocis/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 +} diff --git a/extensions/search/pkg/config/reva.go b/extensions/search/pkg/config/reva.go new file mode 100644 index 00000000000..2b299a0f651 --- /dev/null +++ b/extensions/search/pkg/config/reva.go @@ -0,0 +1,6 @@ +package config + +// Reva defines all available REVA configuration. +type Reva struct { + Address string `ocisConfig:"address" env:"REVA_GATEWAY"` +} diff --git a/extensions/search/pkg/config/service.go b/extensions/search/pkg/config/service.go new file mode 100644 index 00000000000..c019b73046e --- /dev/null +++ b/extensions/search/pkg/config/service.go @@ -0,0 +1,6 @@ +package config + +// Service defines the available service configuration. +type Service struct { + Name string `ocisConfig:"-" yaml:"-"` +} diff --git a/extensions/search/pkg/config/tracing.go b/extensions/search/pkg/config/tracing.go new file mode 100644 index 00000000000..50c49234d6c --- /dev/null +++ b/extensions/search/pkg/config/tracing.go @@ -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"` + Type string `ocisConfig:"type" env:"OCIS_TRACING_TYPE;SEARCH_TRACING_TYPE"` + Endpoint string `ocisConfig:"endpoint" env:"OCIS_TRACING_ENDPOINT;SEARCH_TRACING_ENDPOINT"` + Collector string `ocisConfig:"collector" env:"OCIS_TRACING_COLLECTOR;SEARCH_TRACING_COLLECTOR"` +} diff --git a/extensions/search/pkg/logging/logging.go b/extensions/search/pkg/logging/logging.go new file mode 100644 index 00000000000..7d80232313d --- /dev/null +++ b/extensions/search/pkg/logging/logging.go @@ -0,0 +1,17 @@ +package logging + +import ( + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/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), + ) +} diff --git a/extensions/search/pkg/metrics/metrics.go b/extensions/search/pkg/metrics/metrics.go new file mode 100644 index 00000000000..c3076bb0195 --- /dev/null +++ b/extensions/search/pkg/metrics/metrics.go @@ -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 +} diff --git a/extensions/search/pkg/search/index/index.go b/extensions/search/pkg/search/index/index.go new file mode 100644 index 00000000000..8cba7f7a6ca --- /dev/null +++ b/extensions/search/pkg/search/index/index.go @@ -0,0 +1,311 @@ +// 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" + "path" + "strings" + "time" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" + "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/protogen/gen/ocis/messages/search/v0" + searchsvc "github.com/owncloud/ocis/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) { + bi, err := bleve.New(path, BuildMapping()) + 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:"+doc.Path+"/*"), + ) + bleveReq := bleve.NewSearchRequest(query) + 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)) +} + +// Purge removes an entity from the index +func (i *Index) Move(ri *sprovider.ResourceInfo) error { + doc, err := i.getEntity(idToBleveId(ri.Id)) + if err != nil { + return err + } + oldName := doc.Path + newName := utils.MakeRelativePath(ri.Path) + + doc, err = i.updateEntity(idToBleveId(ri.Id), 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:"+oldName+"/*"), + ) + bleveReq := bleve.NewSearchRequest(query) + 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("Name:"+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:"+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 { + indexMapping := bleve.NewIndexMapping() + indexMapping.DefaultAnalyzer = keyword.Name + return indexMapping +} + +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)), + } + 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 +} diff --git a/extensions/search/pkg/search/index/index_suite_test.go b/extensions/search/pkg/search/index/index_suite_test.go new file mode 100644 index 00000000000..09099db1a4c --- /dev/null +++ b/extensions/search/pkg/search/index/index_suite_test.go @@ -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") +} diff --git a/extensions/search/pkg/search/index/index_test.go b/extensions/search/pkg/search/index/index_test.go new file mode 100644 index 00000000000..8624e55a81a --- /dev/null +++ b/extensions/search/pkg/search/index/index_test.go @@ -0,0 +1,371 @@ +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/extensions/search/pkg/search/index" + searchmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/search/v0" + searchsvc "github.com/owncloud/ocis/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", + } + ref = &sprovider.Reference{ + ResourceId: rootId, + Path: "./foo.pdf", + } + ri = &sprovider.ResourceInfo{ + Id: &sprovider.ResourceId{ + StorageId: "storageid", + OpaqueId: "opaqueid", + }, + ParentId: &sprovider.ResourceId{ + StorageId: "storageid", + OpaqueId: "someopaqueid", + }, + Path: "foo.pdf", + Size: 12345, + Type: sprovider.ResourceType_RESOURCE_TYPE_FILE, + MimeType: "application/pdf", + Mtime: &typesv1beta1.Timestamp{Seconds: 4000}, + } + parentRef = &sprovider.Reference{ + ResourceId: rootId, + Path: "./sudbir", + } + parentRi = &sprovider.ResourceInfo{ + Id: &sprovider.ResourceId{ + StorageId: "storageid", + OpaqueId: "parentopaqueid", + }, + Path: "subdir", + Size: 12345, + Type: sprovider.ResourceType_RESOURCE_TYPE_CONTAINER, + Mtime: &typesv1beta1.Timestamp{Seconds: 4000}, + } + childRef = &sprovider.Reference{ + ResourceId: rootId, + Path: "./sudbir/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) { + 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)) + } + ) + + BeforeEach(func() { + var err error + bleveIndex, err = bleve.NewMemOnly(index.BuildMapping()) + Expect(err).ToNot(HaveOccurred()) + + i, err = index.New(bleveIndex) + Expect(err).ToNot(HaveOccurred()) + }) + + 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("with a file in the root of the space", func() { + BeforeEach(func() { + err := i.Add(ref, ri) + Expect(err).ToNot(HaveOccurred()) + }) + + It("scopes the search to the specified space", func() { + res, err := i.Search(ctx, &searchsvc.SearchIndexRequest{ + Ref: &searchmsg.Reference{ + ResourceId: &searchmsg.ResourceID{ + StorageId: "differentstorageid", + OpaqueId: "differentopaqueid", + }, + }, + Query: "foo.pdf", + }) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(len(res.Matches)).To(Equal(0)) + }) + + It("limits the search to the relevant fields", func() { + res, err := i.Search(ctx, &searchsvc.SearchIndexRequest{ + Ref: &searchmsg.Reference{ + ResourceId: &searchmsg.ResourceID{ + StorageId: ref.ResourceId.StorageId, + OpaqueId: ref.ResourceId.OpaqueId, + }, + }, + Query: "*" + ref.ResourceId.OpaqueId + "*", + }) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(len(res.Matches)).To(Equal(0)) + }) + + It("returns all desired fields", func() { + res, err := i.Search(ctx, &searchsvc.SearchIndexRequest{ + Ref: &searchmsg.Reference{ + ResourceId: &searchmsg.ResourceID{ + StorageId: ref.ResourceId.StorageId, + OpaqueId: ref.ResourceId.OpaqueId, + }, + }, + Query: "foo.pdf", + }) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(len(res.Matches)).To(Equal(1)) + match := res.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 { + res, err := i.Search(ctx, &searchsvc.SearchIndexRequest{ + Ref: &searchmsg.Reference{ + ResourceId: &searchmsg.ResourceID{ + StorageId: ref.ResourceId.StorageId, + OpaqueId: ref.ResourceId.OpaqueId, + }, + }, + Query: query, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(len(res.Matches)).To(Equal(1), "query returned no result: "+query) + Expect(res.Matches[0].Entity.Ref.ResourceId.OpaqueId).To(Equal(ref.ResourceId.OpaqueId)) + Expect(res.Matches[0].Entity.Ref.Path).To(Equal(ref.Path)) + Expect(res.Matches[0].Entity.Id.OpaqueId).To(Equal(ri.Id.OpaqueId)) + Expect(res.Matches[0].Entity.Name).To(Equal(ri.Path)) + Expect(res.Matches[0].Entity.Size).To(Equal(ri.Size)) + } + }) + + 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 { + res, err := i.Search(ctx, &searchsvc.SearchIndexRequest{ + Ref: &searchmsg.Reference{ + ResourceId: &searchmsg.ResourceID{ + StorageId: ref.ResourceId.StorageId, + OpaqueId: ref.ResourceId.OpaqueId, + }, + }, + Query: query, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(len(res.Matches)).To(Equal(1), "query returned no result: "+query) + } + }) + + 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: "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, "subdir", 1) + + err = i.Delete(parentRi.Id) + Expect(err).ToNot(HaveOccurred()) + + assertDocCount(rootId, "subdir", 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, "subdir", 1) + assertDocCount(rootId, "child.pdf", 1) + + err = i.Delete(parentRi.Id) + Expect(err).ToNot(HaveOccurred()) + + assertDocCount(rootId, "subdir", 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, "subdir", 0) + assertDocCount(rootId, "child.pdf", 0) + + err = i.Restore(parentRi.Id) + Expect(err).ToNot(HaveOccurred()) + + assertDocCount(rootId, "subdir", 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) + Expect(err).ToNot(HaveOccurred()) + + assertDocCount(rootId, "subdir", 0) + + res, err := i.Search(ctx, &searchsvc.SearchIndexRequest{ + Query: "child.pdf", + Ref: &searchmsg.Reference{ + ResourceId: &searchmsg.ResourceID{ + StorageId: rootId.StorageId, + OpaqueId: rootId.OpaqueId, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(len(res.Matches)).To(Equal(1)) + Expect(res.Matches[0].Entity.Ref.Path).To(Equal("./newname/child.pdf")) + }) + }) +}) diff --git a/extensions/search/pkg/search/index/mocks/BleveIndex.go b/extensions/search/pkg/search/index/mocks/BleveIndex.go new file mode 100644 index 00000000000..1bb4f1b28d3 --- /dev/null +++ b/extensions/search/pkg/search/index/mocks/BleveIndex.go @@ -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 +} diff --git a/extensions/search/pkg/search/mocks/IndexClient.go b/extensions/search/pkg/search/mocks/IndexClient.go new file mode 100644 index 00000000000..f87fd726441 --- /dev/null +++ b/extensions/search/pkg/search/mocks/IndexClient.go @@ -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/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: ri +func (_m *IndexClient) Delete(ri *providerv1beta1.ResourceId) error { + ret := _m.Called(ri) + + var r0 error + if rf, ok := ret.Get(0).(func(*providerv1beta1.ResourceId) error); ok { + r0 = rf(ri) + } 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: ri +func (_m *IndexClient) Move(ri *providerv1beta1.ResourceInfo) error { + ret := _m.Called(ri) + + var r0 error + if rf, ok := ret.Get(0).(func(*providerv1beta1.ResourceInfo) error); ok { + r0 = rf(ri) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Purge provides a mock function with given fields: ri +func (_m *IndexClient) Purge(ri *providerv1beta1.ResourceId) error { + ret := _m.Called(ri) + + var r0 error + if rf, ok := ret.Get(0).(func(*providerv1beta1.ResourceId) error); ok { + r0 = rf(ri) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Restore provides a mock function with given fields: ri +func (_m *IndexClient) Restore(ri *providerv1beta1.ResourceId) error { + ret := _m.Called(ri) + + var r0 error + if rf, ok := ret.Get(0).(func(*providerv1beta1.ResourceId) error); ok { + r0 = rf(ri) + } 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 +} diff --git a/extensions/search/pkg/search/mocks/ProviderClient.go b/extensions/search/pkg/search/mocks/ProviderClient.go new file mode 100644 index 00000000000..d1d551909e6 --- /dev/null +++ b/extensions/search/pkg/search/mocks/ProviderClient.go @@ -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/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 +} diff --git a/extensions/search/pkg/search/provider/provider_suite_test.go b/extensions/search/pkg/search/provider/provider_suite_test.go new file mode 100644 index 00000000000..9187db0278c --- /dev/null +++ b/extensions/search/pkg/search/provider/provider_suite_test.go @@ -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") +} diff --git a/extensions/search/pkg/search/provider/searchprovider.go b/extensions/search/pkg/search/provider/searchprovider.go new file mode 100644 index 00000000000..3aa2f6cda1f --- /dev/null +++ b/extensions/search/pkg/search/provider/searchprovider.go @@ -0,0 +1,278 @@ +package provider + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + 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" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/errtypes" + "github.com/cs3org/reva/v2/pkg/events" + "github.com/cs3org/reva/v2/pkg/storage/utils/walker" + "github.com/cs3org/reva/v2/pkg/utils" + "github.com/owncloud/ocis/extensions/search/pkg/search" + "github.com/owncloud/ocis/ocis-pkg/log" + "google.golang.org/grpc/metadata" + + searchmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/search/v0" + searchsvc "github.com/owncloud/ocis/protogen/gen/ocis/services/search/v0" +) + +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 + var ref *provider.Reference + var owner *user.User + switch e := ev.(type) { + case events.ItemTrashed: + 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") + } + continue + case events.ItemRestored: + 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") + } + + 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") + } + + continue + case events.ItemMoved: + 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") + } + + switch statRes.Status.Code { + case rpc.Code_CODE_OK: + err = p.indexClient.Move(statRes.Info) + 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") + } + + continue + 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.FileVersionRestored: + ref = e.Ref + owner = &user.User{ + Id: e.Executant, + } + default: + // Not sure what to do here. Skip. + continue + } + + statRes, err := p.statResource(ref, owner) + if err != nil { + p.logger.Error().Err(err).Msg("failed to stat the changed resource") + } + + switch statRes.Status.Code { + case rpc.Code_CODE_OK: + 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() + } + default: + p.logger.Error().Interface("statRes", statRes).Msg("failed to stat the changed resource") + } + } + }() + + return p +} + +func (p *Provider) statResource(ref *provider.Reference, owner *user.User) (*provider.StatResponse, error) { + // Get auth + 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 { + p.logger.Error().Err(err).Interface("authRes", authRes).Msg("error using machine auth") + } + ownerCtx = metadata.AppendToOutgoingContext(ownerCtx, ctxpkg.TokenHeader, authRes.Token) + + // Stat changed resource resource + return p.gwClient.Stat(ownerCtx, &provider.StatRequest{Ref: ref}) +} + +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 (p *Provider) Search(ctx context.Context, req *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) { + if req.Query == "" { + return nil, errtypes.PreconditionFailed("empty query provided") + } + + listSpacesRes, err := p.gwClient.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{ + Opaque: &typesv1beta1.Opaque{Map: map[string]*typesv1beta1.OpaqueEntry{ + "path": { + Decoder: "plain", + Value: []byte("/"), + }, + }}, + }) + if err != nil { + return nil, err + } + + matches := []*searchmsg.Match{} + for _, space := range listSpacesRes.StorageSpaces { + pathPrefix := "" + if space.SpaceType == "grant" { + gpRes, err := p.gwClient.GetPath(ctx, &provider.GetPathRequest{ + ResourceId: space.Root, + }) + if err != nil { + return nil, err + } + if gpRes.Status.Code != rpcv1beta1.Code_CODE_OK { + return nil, errtypes.NewErrtypeFromStatus(gpRes.Status) + } + pathPrefix = utils.MakeRelativePath(gpRes.Path) + } + + res, err := p.indexClient.Search(ctx, &searchsvc.SearchIndexRequest{ + Query: req.Query, + Ref: &searchmsg.Reference{ + ResourceId: &searchmsg.ResourceID{ + StorageId: space.Root.StorageId, + OpaqueId: space.Root.OpaqueId, + }, + Path: pathPrefix, + }, + }) + if err != nil { + return nil, err + } + + for _, match := range res.Matches { + if pathPrefix != "" { + match.Entity.Ref.Path = utils.MakeRelativePath(strings.TrimPrefix(match.Entity.Ref.Path, pathPrefix)) + } + 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 +} diff --git a/extensions/search/pkg/search/provider/searchprovider_test.go b/extensions/search/pkg/search/provider/searchprovider_test.go new file mode 100644 index 00000000000..f6ea0de3952 --- /dev/null +++ b/extensions/search/pkg/search/provider/searchprovider_test.go @@ -0,0 +1,400 @@ +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/events" + "github.com/cs3org/reva/v2/pkg/rgrpc/status" + cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks" + "github.com/owncloud/ocis/extensions/search/pkg/search/mocks" + provider "github.com/owncloud/ocis/extensions/search/pkg/search/provider" + "github.com/owncloud/ocis/ocis-pkg/log" + searchmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/search/v0" + searchsvc "github.com/owncloud/ocis/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{OpaqueId: "personalspaceroot"}, + Name: "personalspace", + } + + 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("trigger 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 + }).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 + }).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 + }).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 + }).Should(BeTrue()) + }) + + It("indexes items when they are being moved", func() { + called := false + indexClient.On("Move", 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.ItemMoved{ + Ref: ref, + Executant: user.Id, + } + + Eventually(func() bool { + return called + }).Should(BeTrue()) + }) + }) + + 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.MatchedBy(func(req *sprovider.ListStorageSpacesRequest) bool { + p := string(req.Opaque.Map["path"].Value) + return p == "/" + })).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("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 == "foo" && req.Ref.ResourceId.OpaqueId == personalSpace.Root.OpaqueId && req.Ref.Path == "" + })) + }) + }) + + Context("with received shares", func() { + var ( + grantSpace *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", + } + gwClient.On("GetPath", mock.Anything, mock.Anything).Return(&sprovider.GetPathResponse{ + Status: status.NewOK(ctx), + Path: "/grant/path", + }, nil) + }) + + It("searches the received spaces (grants)", func() { + gwClient.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *sprovider.ListStorageSpacesRequest) bool { + p := string(req.Opaque.Map["path"].Value) + return p == "/" + })).Return(&sprovider.ListStorageSpacesResponse{ + Status: status.NewOK(ctx), + StorageSpaces: []*sprovider.StorageSpace{grantSpace}, + }, 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(grantSpace.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 == "foo" && req.Ref.ResourceId.OpaqueId == grantSpace.Root.OpaqueId && req.Ref.Path == "./grant/path" + })) + }) + + It("finds matches in both the personal space AND the grant", func() { + gwClient.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *sprovider.ListStorageSpacesRequest) bool { + p := string(req.Opaque.Map["path"].Value) + return p == "/" + })).Return(&sprovider.ListStorageSpacesResponse{ + Status: status.NewOK(ctx), + StorageSpaces: []*sprovider.StorageSpace{personalSpace, grantSpace}, + }, nil) + indexClient.On("Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool { + return req.Ref.ResourceId.OpaqueId == grantSpace.Root.OpaqueId + })).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.OpaqueId == personalSpace.Root.OpaqueId + })).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")) + }) + }) + }) +}) diff --git a/extensions/search/pkg/search/search.go b/extensions/search/pkg/search/search.go new file mode 100644 index 00000000000..14f0baf8c63 --- /dev/null +++ b/extensions/search/pkg/search/search.go @@ -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/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(ri *providerv1beta1.ResourceInfo) error + Delete(ri *providerv1beta1.ResourceId) error + Restore(ri *providerv1beta1.ResourceId) error + Purge(ri *providerv1beta1.ResourceId) error + DocCount() (uint64, error) +} diff --git a/extensions/search/pkg/search/search_suite_test.go b/extensions/search/pkg/search/search_suite_test.go new file mode 100644 index 00000000000..9b2060ec0da --- /dev/null +++ b/extensions/search/pkg/search/search_suite_test.go @@ -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") +} diff --git a/extensions/search/pkg/server/debug/option.go b/extensions/search/pkg/server/debug/option.go new file mode 100644 index 00000000000..33470673de6 --- /dev/null +++ b/extensions/search/pkg/server/debug/option.go @@ -0,0 +1,50 @@ +package debug + +import ( + "context" + + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/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 + } +} diff --git a/extensions/search/pkg/server/debug/server.go b/extensions/search/pkg/server/debug/server.go new file mode 100644 index 00000000000..8fe769df7a4 --- /dev/null +++ b/extensions/search/pkg/server/debug/server.go @@ -0,0 +1,59 @@ +package debug + +import ( + "io" + "net/http" + + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/ocis-pkg/service/debug" + "github.com/owncloud/ocis/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.String), + 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) + } + } +} diff --git a/extensions/search/pkg/server/grpc/option.go b/extensions/search/pkg/server/grpc/option.go new file mode 100644 index 00000000000..b5378458417 --- /dev/null +++ b/extensions/search/pkg/server/grpc/option.go @@ -0,0 +1,85 @@ +package grpc + +import ( + "context" + + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/extensions/search/pkg/metrics" + svc "github.com/owncloud/ocis/extensions/search/pkg/service/v0" + "github.com/owncloud/ocis/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 + } +} diff --git a/extensions/search/pkg/server/grpc/server.go b/extensions/search/pkg/server/grpc/server.go new file mode 100644 index 00000000000..ef1ea0c0760 --- /dev/null +++ b/extensions/search/pkg/server/grpc/server.go @@ -0,0 +1,39 @@ +package grpc + +import ( + svc "github.com/owncloud/ocis/extensions/search/pkg/service/v0" + "github.com/owncloud/ocis/ocis-pkg/service/grpc" + "github.com/owncloud/ocis/ocis-pkg/version" + searchsvc "github.com/owncloud/ocis/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.String), + ) + + 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 +} diff --git a/extensions/search/pkg/service/v0/option.go b/extensions/search/pkg/service/v0/option.go new file mode 100644 index 00000000000..1b592e3a3d5 --- /dev/null +++ b/extensions/search/pkg/service/v0/option.go @@ -0,0 +1,39 @@ +package service + +import ( + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/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 + } +} diff --git a/extensions/search/pkg/service/v0/service.go b/extensions/search/pkg/service/v0/service.go new file mode 100644 index 00000000000..90349b43937 --- /dev/null +++ b/extensions/search/pkg/service/v0/service.go @@ -0,0 +1,106 @@ +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/extensions/audit/pkg/types" + "github.com/owncloud/ocis/extensions/search/pkg/config" + "github.com/owncloud/ocis/extensions/search/pkg/search" + "github.com/owncloud/ocis/extensions/search/pkg/search/index" + searchprovider "github.com/owncloud/ocis/extensions/search/pkg/search/provider" + "github.com/owncloud/ocis/ocis-pkg/log" + searchsvc "github.com/owncloud/ocis/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, types.RegisteredEvents()...) + if err != nil { + return nil, err + } + + indexDir := filepath.Join(cfg.Datapath, "index.bleve") + bleveIndex, err := bleve.Open(indexDir) + if err != nil { + bleveIndex, err = bleve.New(indexDir, index.BuildMapping()) + 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 +} diff --git a/extensions/search/pkg/tracing/tracing.go b/extensions/search/pkg/tracing/tracing.go new file mode 100644 index 00000000000..2f1f8e513f0 --- /dev/null +++ b/extensions/search/pkg/tracing/tracing.go @@ -0,0 +1,23 @@ +package tracing + +import ( + "github.com/owncloud/ocis/extensions/search/pkg/config" + pkgtrace "github.com/owncloud/ocis/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 +} diff --git a/extensions/web/pkg/config/defaults/defaultconfig.go b/extensions/web/pkg/config/defaults/defaultconfig.go index f3411c2040b..baa74517b05 100644 --- a/extensions/web/pkg/config/defaults/defaultconfig.go +++ b/extensions/web/pkg/config/defaults/defaultconfig.go @@ -49,6 +49,9 @@ func DefaultConfig() *config.Config { Scope: "openid profile email", }, Apps: []string{"files", "search", "preview", "text-editor", "pdf-viewer", "external", "user-management"}, + Options: map[string]interface{}{ + "hideSearchBar": false, + }, }, }, } diff --git a/extensions/web/pkg/service/v0/service.go b/extensions/web/pkg/service/v0/service.go index 4a0cf6d0747..f77074a4399 100644 --- a/extensions/web/pkg/service/v0/service.go +++ b/extensions/web/pkg/service/v0/service.go @@ -66,12 +66,6 @@ func (p Web) getPayload() (payload []byte, err error) { if p.config.Web.Path == "" { // render dynamically using config - // provide default ocis-web options - if p.config.Web.Config.Options == nil { - p.config.Web.Config.Options = make(map[string]interface{}) - p.config.Web.Config.Options["hideSearchBar"] = true - } - // build theme url if themeServer, err := url.Parse(p.config.Web.ThemeServer); err == nil { p.config.Web.Config.Theme = themeServer.String() + p.config.Web.ThemePath diff --git a/extensions/webdav/pkg/errors/error.go b/extensions/webdav/pkg/errors/error.go new file mode 100644 index 00000000000..ba7f9b908ad --- /dev/null +++ b/extensions/webdav/pkg/errors/error.go @@ -0,0 +1,170 @@ +package errors + +import ( + "bytes" + "encoding/xml" + "net/http" + + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/cs3org/reva/v2/pkg/rgrpc/status" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +var sabreException = map[int]string{ + + // the commented states have no corresponding exception in sabre/dav, + // see https://github.com/sabre-io/dav/tree/master/lib/DAV/Exception + + // http.StatusMultipleChoices: "Multiple Choices", + // http.StatusMovedPermanently: "Moved Permanently", + // http.StatusFound: "Found", + // http.StatusSeeOther: "See Other", + // http.StatusNotModified: "Not Modified", + // http.StatusUseProxy: "Use Proxy", + // http.StatusTemporaryRedirect: "Temporary Redirect", + // http.StatusPermanentRedirect: "Permanent Redirect", + + http.StatusBadRequest: "Sabre\\DAV\\Exception\\BadRequest", + http.StatusUnauthorized: "Sabre\\DAV\\Exception\\NotAuthenticated", + http.StatusPaymentRequired: "Sabre\\DAV\\Exception\\PaymentRequired", + http.StatusForbidden: "Sabre\\DAV\\Exception\\Forbidden", // InvalidResourceType, InvalidSyncToken, TooManyMatches + http.StatusNotFound: "Sabre\\DAV\\Exception\\NotFound", + http.StatusMethodNotAllowed: "Sabre\\DAV\\Exception\\MethodNotAllowed", + // http.StatusNotAcceptable: "Not Acceptable", + // http.StatusProxyAuthRequired: "Proxy Authentication Required", + // http.StatusRequestTimeout: "Request Timeout", + http.StatusConflict: "Sabre\\DAV\\Exception\\Conflict", // LockTokenMatchesRequestUri + // http.StatusGone: "Gone", + http.StatusLengthRequired: "Sabre\\DAV\\Exception\\LengthRequired", + http.StatusPreconditionFailed: "Sabre\\DAV\\Exception\\PreconditionFailed", + // http.StatusRequestEntityTooLarge: "Request Entity Too Large", + // http.StatusRequestURITooLong: "Request URI Too Long", + http.StatusUnsupportedMediaType: "Sabre\\DAV\\Exception\\UnsupportedMediaType", // ReportNotSupported + http.StatusRequestedRangeNotSatisfiable: "Sabre\\DAV\\Exception\\RequestedRangeNotSatisfiable", + // http.StatusExpectationFailed: "Expectation Failed", + // http.StatusTeapot: "I'm a teapot", + // http.StatusMisdirectedRequest: "Misdirected Request", + // http.StatusUnprocessableEntity: "Unprocessable Entity", + http.StatusLocked: "Sabre\\DAV\\Exception\\Locked", // ConflictingLock + // http.StatusFailedDependency: "Failed Dependency", + // http.StatusTooEarly: "Too Early", + // http.StatusUpgradeRequired: "Upgrade Required", + // http.StatusPreconditionRequired: "Precondition Required", + // http.StatusTooManyRequests: "Too Many Requests", + // http.StatusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large", + // http.StatusUnavailableForLegalReasons: "Unavailable For Legal Reasons", + + // http.StatusInternalServerError: "Internal Server Error", + http.StatusNotImplemented: "Sabre\\DAV\\Exception\\NotImplemented", + // http.StatusBadGateway: "Bad Gateway", + http.StatusServiceUnavailable: "Sabre\\DAV\\Exception\\ServiceUnavailable", + // http.StatusGatewayTimeout: "Gateway Timeout", + // http.StatusHTTPVersionNotSupported: "HTTP Version Not Supported", + // http.StatusVariantAlsoNegotiates: "Variant Also Negotiates", + http.StatusInsufficientStorage: "Sabre\\DAV\\Exception\\InsufficientStorage", + // http.StatusLoopDetected: "Loop Detected", + // http.StatusNotExtended: "Not Extended", + // http.StatusNetworkAuthenticationRequired: "Network Authentication Required", +} + +// SabreException returns a sabre exception text for the HTTP status code. It returns the empty +// string if the code is unknown. +func SabreException(code int) string { + return sabreException[code] +} + +// Exception represents a ocdav exception +type Exception struct { + Code int + Message string + Header string +} + +// Marshal just calls the xml marshaller for a given exception. +func Marshal(code int, message string, header string) ([]byte, error) { + xmlstring, err := xml.Marshal(&ErrorXML{ + Xmlnsd: "DAV", + Xmlnss: "http://sabredav.org/ns", + Exception: sabreException[code], + Message: message, + Header: header, + }) + if err != nil { + return nil, err + } + var buf bytes.Buffer + buf.WriteString(xml.Header) + buf.Write(xmlstring) + return buf.Bytes(), err +} + +// ErrorXML holds the xml representation of an error +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error +type ErrorXML struct { + XMLName xml.Name `xml:"d:error"` + Xmlnsd string `xml:"xmlns:d,attr"` + Xmlnss string `xml:"xmlns:s,attr"` + Exception string `xml:"s:exception"` + Message string `xml:"s:message"` + InnerXML []byte `xml:",innerxml"` + // Header is used to indicate the conflicting request header + Header string `xml:"s:header,omitempty"` +} + +var ( + // ErrInvalidDepth is an invalid depth header error + ErrInvalidDepth = errors.New("webdav: invalid depth") + // ErrInvalidPropfind is an invalid propfind error + ErrInvalidPropfind = errors.New("webdav: invalid propfind") + // ErrInvalidProppatch is an invalid proppatch error + ErrInvalidProppatch = errors.New("webdav: invalid proppatch") + // ErrInvalidLockInfo is an invalid lock error + ErrInvalidLockInfo = errors.New("webdav: invalid lock info") + // ErrUnsupportedLockInfo is an unsupported lock error + ErrUnsupportedLockInfo = errors.New("webdav: unsupported lock info") + // ErrInvalidTimeout is an invalid timeout error + ErrInvalidTimeout = errors.New("webdav: invalid timeout") + // ErrInvalidIfHeader is an invalid if header error + ErrInvalidIfHeader = errors.New("webdav: invalid If header") + // ErrUnsupportedMethod is an unsupported method error + ErrUnsupportedMethod = errors.New("webdav: unsupported method") + // ErrInvalidLockToken is an invalid lock token error + ErrInvalidLockToken = errors.New("webdav: invalid lock token") + // ErrConfirmationFailed is returned by a LockSystem's Confirm method. + ErrConfirmationFailed = errors.New("webdav: confirmation failed") + // ErrForbidden is returned by a LockSystem's Unlock method. + ErrForbidden = errors.New("webdav: forbidden") + // ErrLocked is returned by a LockSystem's Create, Refresh and Unlock methods. + ErrLocked = errors.New("webdav: locked") + // ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods. + ErrNoSuchLock = errors.New("webdav: no such lock") + // ErrNotImplemented is returned when hitting not implemented code paths + ErrNotImplemented = errors.New("webdav: not implemented") +) + +// HandleErrorStatus checks the status code, logs a Debug or Error level message +// and writes an appropriate http status +func HandleErrorStatus(log *zerolog.Logger, w http.ResponseWriter, s *rpc.Status) { + hsc := status.HTTPStatusFromCode(s.Code) + if hsc == http.StatusInternalServerError { + log.Error().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc)) + } else { + log.Debug().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc)) + } + w.WriteHeader(hsc) +} + +// HandleWebdavError checks the status code, logs an error and creates a webdav response body +// if needed +func HandleWebdavError(log *zerolog.Logger, w http.ResponseWriter, b []byte, err error) { + if err != nil { + log.Error().Msgf("error marshaling xml response: %s", b) + w.WriteHeader(http.StatusInternalServerError) + return + } + _, err = w.Write(b) + if err != nil { + log.Err(err).Msg("error writing response") + } +} diff --git a/extensions/webdav/pkg/net/headers.go b/extensions/webdav/pkg/net/headers.go new file mode 100644 index 00000000000..5c22d8509d8 --- /dev/null +++ b/extensions/webdav/pkg/net/headers.go @@ -0,0 +1,48 @@ +package net + +// Common HTTP headers. +const ( + HeaderAcceptRanges = "Accept-Ranges" + HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" + HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" + HeaderContentDisposistion = "Content-Disposition" + HeaderContentLength = "Content-Length" + HeaderContentRange = "Content-Range" + HeaderContentType = "Content-Type" + HeaderETag = "ETag" + HeaderLastModified = "Last-Modified" + HeaderLocation = "Location" + HeaderRange = "Range" + HeaderIfMatch = "If-Match" +) + +// webdav headers +const ( + HeaderDav = "DAV" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.1 + HeaderDepth = "Depth" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.2 + HeaderDestination = "Destination" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.3 + HeaderIf = "If" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.4 + HeaderLockToken = "Lock-Token" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.5 + HeaderOverwrite = "Overwrite" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.6 + HeaderTimeout = "Timeout" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.7 +) + +// Non standard HTTP headers. +const ( + HeaderOCFileID = "OC-FileId" + HeaderOCETag = "OC-ETag" + HeaderOCChecksum = "OC-Checksum" + HeaderOCPermissions = "OC-Perm" + HeaderTusResumable = "Tus-Resumable" + HeaderTusVersion = "Tus-Version" + HeaderTusExtension = "Tus-Extension" + HeaderTusChecksumAlgorithm = "Tus-Checksum-Algorithm" + HeaderTusUploadExpires = "Upload-Expires" + HeaderUploadChecksum = "Upload-Checksum" + HeaderUploadLength = "Upload-Length" + HeaderUploadMetadata = "Upload-Metadata" + HeaderUploadOffset = "Upload-Offset" + HeaderOCMtime = "X-OC-Mtime" + HeaderExpectedEntityLength = "X-Expected-Entity-Length" + HeaderLitmus = "X-Litmus" +) diff --git a/extensions/webdav/pkg/net/net.go b/extensions/webdav/pkg/net/net.go new file mode 100644 index 00000000000..6ac66b74a67 --- /dev/null +++ b/extensions/webdav/pkg/net/net.go @@ -0,0 +1,12 @@ +package net + +import ( + "net/url" +) + +// EncodePath encodes the path of a url. +// +// slashes (/) are treated as path-separators. +func EncodePath(path string) string { + return (&url.URL{Path: path}).EscapedPath() +} diff --git a/extensions/webdav/pkg/prop/prop.go b/extensions/webdav/pkg/prop/prop.go new file mode 100644 index 00000000000..b34ce19fe81 --- /dev/null +++ b/extensions/webdav/pkg/prop/prop.go @@ -0,0 +1,125 @@ +package prop + +import ( + "bytes" + "encoding/xml" +) + +// PropertyXML represents a single DAV resource property as defined in RFC 4918. +// http://www.webdav.org/specs/rfc4918.html#data.model.for.resource.properties +type PropertyXML struct { + // XMLName is the fully qualified name that identifies this property. + XMLName xml.Name + + // Lang is an optional xml:lang attribute. + Lang string `xml:"xml:lang,attr,omitempty"` + + // InnerXML contains the XML representation of the property value. + // See http://www.webdav.org/specs/rfc4918.html#property_values + // + // Property values of complex type or mixed-content must have fully + // expanded XML namespaces or be self-contained with according + // XML namespace declarations. They must not rely on any XML + // namespace declarations within the scope of the XML document, + // even including the DAV: namespace. + InnerXML []byte `xml:",innerxml"` +} + +func xmlEscaped(val string) []byte { + buf := new(bytes.Buffer) + xml.Escape(buf, []byte(val)) + return buf.Bytes() +} + +// EscapedNS returns a new PropertyXML instance while xml-escaping the value +func EscapedNS(namespace string, local string, val string) PropertyXML { + return PropertyXML{ + XMLName: xml.Name{Space: namespace, Local: local}, + Lang: "", + InnerXML: xmlEscaped(val), + } +} + +// Escaped returns a new PropertyXML instance while xml-escaping the value +// TODO properly use the space +func Escaped(key, val string) PropertyXML { + return PropertyXML{ + XMLName: xml.Name{Space: "", Local: key}, + Lang: "", + InnerXML: xmlEscaped(val), + } +} + +// NotFound returns a new PropertyXML instance with an empty value +func NotFound(key string) PropertyXML { + return PropertyXML{ + XMLName: xml.Name{Space: "", Local: key}, + Lang: "", + } +} + +// NotFoundNS returns a new PropertyXML instance with the given namespace and an empty value +func NotFoundNS(namespace, key string) PropertyXML { + return PropertyXML{ + XMLName: xml.Name{Space: namespace, Local: key}, + Lang: "", + } +} + +// Raw returns a new PropertyXML instance for the given key/value pair +// TODO properly use the space +func Raw(key, val string) PropertyXML { + return PropertyXML{ + XMLName: xml.Name{Space: "", Local: key}, + Lang: "", + InnerXML: []byte(val), + } +} + +// Next returns the next token, if any, in the XML stream of d. +// RFC 4918 requires to ignore comments, processing instructions +// and directives. +// http://www.webdav.org/specs/rfc4918.html#property_values +// http://www.webdav.org/specs/rfc4918.html#xml-extensibility +func Next(d *xml.Decoder) (xml.Token, error) { + for { + t, err := d.Token() + if err != nil { + return t, err + } + switch t.(type) { + case xml.Comment, xml.Directive, xml.ProcInst: + continue + default: + return t, nil + } + } +} + +// ActiveLock holds active lock xml data +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_activelock +// +type ActiveLock struct { + XMLName xml.Name `xml:"activelock"` + Exclusive *struct{} `xml:"lockscope>exclusive,omitempty"` + Shared *struct{} `xml:"lockscope>shared,omitempty"` + Write *struct{} `xml:"locktype>write,omitempty"` + Depth string `xml:"depth"` + Owner Owner `xml:"owner,omitempty"` + Timeout string `xml:"timeout,omitempty"` + Locktoken string `xml:"locktoken>href"` + Lockroot string `xml:"lockroot>href,omitempty"` +} + +// Owner captures the inner UML of a lock owner element http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner +type Owner struct { + InnerXML string `xml:",innerxml"` +} + +// Escape repaces ", &, ', < and > with their xml representation +func Escape(s string) string { + b := bytes.NewBuffer(nil) + _ = xml.EscapeText(b, []byte(s)) + return b.String() +} diff --git a/extensions/webdav/pkg/propfind/propfind.go b/extensions/webdav/pkg/propfind/propfind.go new file mode 100644 index 00000000000..8be26112fb4 --- /dev/null +++ b/extensions/webdav/pkg/propfind/propfind.go @@ -0,0 +1,180 @@ +package propfind + +import ( + "encoding/xml" + "fmt" + "io" + "net/http" + + "github.com/owncloud/ocis/extensions/webdav/pkg/errors" + "github.com/owncloud/ocis/extensions/webdav/pkg/prop" +) + +const ( + _spaceTypeProject = "project" +) + +type countingReader struct { + n int + r io.Reader +} + +func (c *countingReader) Read(p []byte) (int, error) { + n, err := c.r.Read(p) + c.n += n + return n, err +} + +// Props represents properties related to a resource +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind) +type Props []xml.Name + +// XML holds the xml representation of a propfind +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind +type XML struct { + XMLName xml.Name `xml:"DAV: propfind"` + Allprop *struct{} `xml:"DAV: allprop"` + Propname *struct{} `xml:"DAV: propname"` + Prop Props `xml:"DAV: prop"` + Include Props `xml:"DAV: include"` +} + +// PropstatXML holds the xml representation of a propfind response +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat +type PropstatXML struct { + // Prop requires DAV: to be the default namespace in the enclosing + // XML. This is due to the standard encoding/xml package currently + // not honoring namespace declarations inside a xmltag with a + // parent element for anonymous slice elements. + // Use of multistatusWriter takes care of this. + Prop []prop.PropertyXML `xml:"d:prop>_ignored_"` + Status string `xml:"d:status"` + Error *errors.ErrorXML `xml:"d:error"` + ResponseDescription string `xml:"d:responsedescription,omitempty"` +} + +// ResponseXML holds the xml representation of a propfind response +type ResponseXML struct { + XMLName xml.Name `xml:"d:response"` + Href string `xml:"d:href"` + Propstat []PropstatXML `xml:"d:propstat"` + Status string `xml:"d:status,omitempty"` + Error *errors.ErrorXML `xml:"d:error"` + ResponseDescription string `xml:"d:responsedescription,omitempty"` +} + +// MultiStatusResponseXML holds the xml representation of a multistatus propfind response +type MultiStatusResponseXML struct { + XMLName xml.Name `xml:"d:multistatus"` + XmlnsS string `xml:"xmlns:s,attr,omitempty"` + XmlnsD string `xml:"xmlns:d,attr,omitempty"` + XmlnsOC string `xml:"xmlns:oc,attr,omitempty"` + + Responses []*ResponseXML `xml:"d:response"` +} + +// ResponseUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400 +type ResponseUnmarshalXML struct { + XMLName xml.Name `xml:"response"` + Href string `xml:"href"` + Propstat []PropstatUnmarshalXML `xml:"propstat"` + Status string `xml:"status,omitempty"` + Error *errors.ErrorXML `xml:"d:error"` + ResponseDescription string `xml:"responsedescription,omitempty"` +} + +// MultiStatusResponseUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400 +type MultiStatusResponseUnmarshalXML struct { + XMLName xml.Name `xml:"multistatus"` + XmlnsS string `xml:"xmlns:s,attr,omitempty"` + XmlnsD string `xml:"xmlns:d,attr,omitempty"` + XmlnsOC string `xml:"xmlns:oc,attr,omitempty"` + + Responses []*ResponseUnmarshalXML `xml:"response"` +} + +// PropstatUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400 +type PropstatUnmarshalXML struct { + // Prop requires DAV: to be the default namespace in the enclosing + // XML. This is due to the standard encoding/xml package currently + // not honoring namespace declarations inside a xmltag with a + // parent element for anonymous slice elements. + // Use of multistatusWriter takes care of this. + Prop []*prop.PropertyXML `xml:"prop"` + Status string `xml:"status"` + Error *errors.ErrorXML `xml:"d:error"` + ResponseDescription string `xml:"responsedescription,omitempty"` +} + +// NewMultiStatusResponseXML returns a preconfigured instance of MultiStatusResponseXML +func NewMultiStatusResponseXML() *MultiStatusResponseXML { + return &MultiStatusResponseXML{ + XmlnsD: "DAV:", + XmlnsS: "http://sabredav.org/ns", + XmlnsOC: "http://owncloud.org/ns", + } +} + +// ReadPropfind extracts and parses the propfind XML information from a Reader +// from https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/xml.go#L178-L205 +func ReadPropfind(r io.Reader) (pf XML, status int, err error) { + c := countingReader{r: r} + if err = xml.NewDecoder(&c).Decode(&pf); err != nil { + if err == io.EOF { + if c.n == 0 { + // An empty body means to propfind allprop. + // http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND + return XML{Allprop: new(struct{})}, 0, nil + } + err = errors.ErrInvalidPropfind + } + return XML{}, http.StatusBadRequest, err + } + + if pf.Allprop == nil && pf.Include != nil { + return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind + } + if pf.Allprop != nil && (pf.Prop != nil || pf.Propname != nil) { + return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind + } + if pf.Prop != nil && pf.Propname != nil { + return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind + } + if pf.Propname == nil && pf.Allprop == nil && pf.Prop == nil { + // jfd: I think is perfectly valid ... treat it as allprop + return XML{Allprop: new(struct{})}, 0, nil + } + return pf, 0, nil +} + +// UnmarshalXML appends the property names enclosed within start to pn. +// +// It returns an error if start does not contain any properties or if +// properties contain values. Character data between properties is ignored. +func (pn *Props) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + for { + t, err := prop.Next(d) + if err != nil { + return err + } + switch e := t.(type) { + case xml.EndElement: + // jfd: I think is perfectly valid ... treat it as allprop + /* + if len(*pn) == 0 { + return fmt.Errorf("%s must not be empty", start.Name.Local) + } + */ + return nil + case xml.StartElement: + t, err = prop.Next(d) + if err != nil { + return err + } + if _, ok := t.(xml.EndElement); !ok { + return fmt.Errorf("unexpected token %T", t) + } + *pn = append(*pn, e.Name) + } + } +} diff --git a/extensions/webdav/pkg/service/v0/search.go b/extensions/webdav/pkg/service/v0/search.go new file mode 100644 index 00000000000..5a7d15f5457 --- /dev/null +++ b/extensions/webdav/pkg/service/v0/search.go @@ -0,0 +1,221 @@ +package svc + +import ( + "context" + "encoding/xml" + "io" + "net/http" + "path" + "strconv" + "time" + + revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/owncloud/ocis/extensions/webdav/pkg/net" + "github.com/owncloud/ocis/extensions/webdav/pkg/prop" + "github.com/owncloud/ocis/extensions/webdav/pkg/propfind" + searchmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/search/v0" + searchsvc "github.com/owncloud/ocis/protogen/gen/ocis/services/search/v0" + merrors "go-micro.dev/v4/errors" + "go-micro.dev/v4/metadata" +) + +const ( + elementNameSearchFiles = "search-files" + // TODO elementNameFilterFiles = "filter-files" +) + +// Search is the endpoint for retrieving search results for REPORT requests +func (g Webdav) Search(w http.ResponseWriter, r *http.Request) { + rep, err := readReport(r.Body) + if err != nil { + renderError(w, r, errBadRequest(err.Error())) + g.log.Error().Err(err).Msg("error reading report") + return + } + + if rep.SearchFiles == nil { + renderError(w, r, errBadRequest("missing search-files tag")) + g.log.Error().Err(err).Msg("error reading report") + return + } + + t := r.Header.Get(TokenHeader) + ctx := revactx.ContextSetToken(r.Context(), t) + ctx = metadata.Set(ctx, revactx.TokenHeader, t) + rsp, err := g.searchClient.Search(ctx, &searchsvc.SearchRequest{ + Query: "*" + rep.SearchFiles.Search.Pattern + "*", + }) + if err != nil { + e := merrors.Parse(err.Error()) + switch e.Code { + case http.StatusBadRequest: + renderError(w, r, errBadRequest(err.Error())) + default: + renderError(w, r, errInternalError(err.Error())) + } + g.log.Error().Err(err).Msg("could not get search results") + return + } + + g.sendSearchResponse(rsp, w, r) +} + +func (g Webdav) sendSearchResponse(rsp *searchsvc.SearchResponse, w http.ResponseWriter, r *http.Request) { + responsesXML, err := multistatusResponse(r.Context(), rsp.Matches) + if err != nil { + g.log.Error().Err(err).Msg("error formatting propfind") + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol") + w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8") + w.WriteHeader(http.StatusMultiStatus) + if _, err := w.Write(responsesXML); err != nil { + g.log.Err(err).Msg("error writing response") + } +} + +// multistatusResponse converts a list of matches into a multistatus response string +func multistatusResponse(ctx context.Context, matches []*searchmsg.Match) ([]byte, error) { + responses := make([]*propfind.ResponseXML, 0, len(matches)) + for i := range matches { + res, err := matchToPropResponse(ctx, matches[i]) + if err != nil { + return nil, err + } + responses = append(responses, res) + } + + msr := propfind.NewMultiStatusResponseXML() + msr.Responses = responses + msg, err := xml.Marshal(msr) + if err != nil { + return nil, err + } + return msg, nil +} + +func matchToPropResponse(ctx context.Context, match *searchmsg.Match) (*propfind.ResponseXML, error) { + response := propfind.ResponseXML{ + Href: net.EncodePath(path.Join("/dav/spaces/", match.Entity.Ref.ResourceId.StorageId+"!"+match.Entity.Ref.ResourceId.OpaqueId, match.Entity.Ref.Path)), + Propstat: []propfind.PropstatXML{}, + } + + propstatOK := propfind.PropstatXML{ + Status: "HTTP/1.1 200 OK", + Prop: []prop.PropertyXML{}, + } + + // RDNVW + // 0 + // demo + // demo + // + // https://demo.owncloud.com/f/7 + // application/pdf + // + // + // done: + // 7 + // 6668668 + // 6668668 + // "0cdcdd1bb13a8fed3e54d3b2325dc97c" + // Mon, 25 Apr 2022 06:48:26 GMT + + propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:fileid", match.Entity.Id.StorageId+"!"+match.Entity.Id.OpaqueId)) + propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getetag", match.Entity.Etag)) + propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getlastmodified", match.Entity.LastModifiedTime.AsTime().Format(time.RFC3339))) + propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getcontenttype", match.Entity.MimeType)) + + size := strconv.FormatUint(match.Entity.Size, 10) + propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:size", size)) + propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getcontentlength", size)) + + // TODO find name for score property + score := strconv.FormatFloat(float64(match.Score), 'f', -1, 64) + propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:score", score)) + + if len(propstatOK.Prop) > 0 { + response.Propstat = append(response.Propstat, propstatOK) + } + + return &response, nil +} + +type report struct { + SearchFiles *reportSearchFiles + // FilterFiles TODO add this for tag based search + FilterFiles *reportFilterFiles `xml:"filter-files"` +} +type reportSearchFiles struct { + XMLName xml.Name `xml:"search-files"` + Lang string `xml:"xml:lang,attr,omitempty"` + Prop Props `xml:"DAV: prop"` + Search reportSearchFilesSearch `xml:"search"` +} +type reportSearchFilesSearch struct { + Pattern string `xml:"pattern"` + Limit int `xml:"limit"` + Offset int `xml:"offset"` +} + +type reportFilterFiles struct { + XMLName xml.Name `xml:"filter-files"` + Lang string `xml:"xml:lang,attr,omitempty"` + Prop Props `xml:"DAV: prop"` + Rules reportFilterFilesRules `xml:"filter-rules"` +} + +type reportFilterFilesRules struct { + Favorite bool `xml:"favorite"` + SystemTag int `xml:"systemtag"` +} + +// Props represents properties related to a resource +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind) +type Props []xml.Name + +// XML holds the xml representation of a propfind +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind +type XML struct { + XMLName xml.Name `xml:"DAV: propfind"` + Allprop *struct{} `xml:"DAV: allprop"` + Propname *struct{} `xml:"DAV: propname"` + Prop Props `xml:"DAV: prop"` + Include Props `xml:"DAV: include"` +} + +func readReport(r io.Reader) (rep *report, err error) { + decoder := xml.NewDecoder(r) + rep = &report{} + for { + t, err := decoder.Token() + if err == io.EOF { + // io.EOF is a successful end + return rep, nil + } + if err != nil { + return nil, err + } + + if v, ok := t.(xml.StartElement); ok { + if v.Name.Local == elementNameSearchFiles { + var repSF reportSearchFiles + err = decoder.DecodeElement(&repSF, &v) + if err != nil { + return nil, err + } + rep.SearchFiles = &repSF + /* + } else if v.Name.Local == elementNameFilterFiles { + var repFF reportFilterFiles + err = decoder.DecodeElement(&repFF, &v) + if err != nil { + return nil, http.StatusBadRequest, err + } + rep.FilterFiles = &repFF + */ + } + } + } +} diff --git a/extensions/webdav/pkg/service/v0/service.go b/extensions/webdav/pkg/service/v0/service.go index 747bafbace5..ad6faff6ded 100644 --- a/extensions/webdav/pkg/service/v0/service.go +++ b/extensions/webdav/pkg/service/v0/service.go @@ -23,6 +23,7 @@ import ( "github.com/owncloud/ocis/extensions/webdav/pkg/config" "github.com/owncloud/ocis/extensions/webdav/pkg/dav/requests" thumbnailsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/thumbnails/v0" + searchsvc "github.com/owncloud/ocis/protogen/gen/ocis/services/search/v0" thumbnailssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/thumbnails/v0" ) @@ -51,6 +52,7 @@ func NewService(opts ...Option) (Service, error) { conf := options.Config m := chi.NewMux() + chi.RegisterMethod("REPORT") m.Use(options.Middleware...) gwc, err := pool.GetGatewayServiceClient(conf.RevaGateway) @@ -62,6 +64,7 @@ func NewService(opts ...Option) (Service, error) { config: conf, log: options.Logger, mux: m, + searchClient: searchsvc.NewSearchProviderService("com.owncloud.api.search", grpc.DefaultClient), thumbnailsClient: thumbnailssvc.NewThumbnailService("com.owncloud.api.thumbnails", grpc.DefaultClient), revaClient: gwc, } @@ -71,16 +74,19 @@ func NewService(opts ...Option) (Service, error) { r.Get("/remote.php/dav/files/{id}/*", svc.Thumbnail) r.Get("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnail) r.Head("/remote.php/dav/public-files/{token}/*", svc.PublicThumbnailHead) + + r.MethodFunc("REPORT", "/remote.php/dav/files/{id}/*", svc.Search) }) return svc, nil } -// Webdav defines implements the business logic for Service. +// Webdav implements the business logic for Service. type Webdav struct { config *config.Config log log.Logger mux *chi.Mux + searchClient searchsvc.SearchProviderService thumbnailsClient thumbnailssvc.ThumbnailService revaClient gatewayv1beta1.GatewayAPIClient } diff --git a/go.mod b/go.mod index 90185fb87ae..346f6127cea 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,10 @@ require ( github.com/GeertJohan/yubigo v0.0.0-20190917122436-175bc097e60e github.com/ReneKroon/ttlcache/v2 v2.11.0 github.com/blevesearch/bleve/v2 v2.3.2 + github.com/blevesearch/bleve_index_api v1.0.1 github.com/coreos/go-oidc/v3 v3.1.0 github.com/cs3org/go-cs3apis v0.0.0-20220412090512-93c5918b4bde - github.com/cs3org/reva/v2 v2.0.0-20220502075009-8bcec2e4663e + github.com/cs3org/reva/v2 v2.0.0-20220502122639-bfbf8690a043 github.com/disintegration/imaging v1.6.2 github.com/glauth/glauth/v2 v2.0.0-20211021011345-ef3151c28733 github.com/go-chi/chi/v5 v5.0.7 @@ -104,7 +105,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-simplejson v0.5.0 // indirect github.com/bits-and-blooms/bitset v1.2.1 // indirect - github.com/blevesearch/bleve_index_api v1.0.1 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/mmap-go v1.0.3 // indirect diff --git a/go.sum b/go.sum index 2ee58f6e4f1..68f316b32f2 100644 --- a/go.sum +++ b/go.sum @@ -318,8 +318,8 @@ github.com/cs3org/go-cs3apis v0.0.0-20220412090512-93c5918b4bde h1:WrD9O8ZaWvsm0 github.com/cs3org/go-cs3apis v0.0.0-20220412090512-93c5918b4bde/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cs3org/reva v1.18.0 h1:MbPS5ZAa8RzKcTxAVeSDdISB3XXqLIxqB03BTN5ReBY= github.com/cs3org/reva v1.18.0/go.mod h1:e5VDUDu4vVWIeVkZcW//n6UZzhGGMa+Tz/whCiX3N6o= -github.com/cs3org/reva/v2 v2.0.0-20220502075009-8bcec2e4663e h1:ym80MMvfFLHMxt6aiU67kTe/pzRBaSOUNdPkmeKYejk= -github.com/cs3org/reva/v2 v2.0.0-20220502075009-8bcec2e4663e/go.mod h1:2e/4HcIy54Mic3V7Ow0bz4n5dkZU0dHIZSWomFe5vng= +github.com/cs3org/reva/v2 v2.0.0-20220502122639-bfbf8690a043 h1:wAvf45pBDnWIN4kpyWpD9uRl9y147ioAXJkfztrMSCM= +github.com/cs3org/reva/v2 v2.0.0-20220502122639-bfbf8690a043/go.mod h1:2e/4HcIy54Mic3V7Ow0bz4n5dkZU0dHIZSWomFe5vng= github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI= github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= diff --git a/ocis-pkg/config/config.go b/ocis-pkg/config/config.go index 6a20b51122d..8f6b1e0d834 100644 --- a/ocis-pkg/config/config.go +++ b/ocis-pkg/config/config.go @@ -22,6 +22,7 @@ import ( ocdav "github.com/owncloud/ocis/extensions/ocdav/pkg/config" ocs "github.com/owncloud/ocis/extensions/ocs/pkg/config" proxy "github.com/owncloud/ocis/extensions/proxy/pkg/config" + search "github.com/owncloud/ocis/extensions/search/pkg/config" settings "github.com/owncloud/ocis/extensions/settings/pkg/config" sharing "github.com/owncloud/ocis/extensions/sharing/pkg/config" storagemetadata "github.com/owncloud/ocis/extensions/storage-metadata/pkg/config" @@ -100,4 +101,5 @@ type Config struct { Store *store.Config `yaml:"store"` Thumbnails *thumbnails.Config `yaml:"thumbnails"` WebDAV *webdav.Config `yaml:"webdav"` + Search *search.Config `yaml:"search"` } diff --git a/ocis-pkg/config/defaultconfig.go b/ocis-pkg/config/defaultconfig.go index bd48781dda2..8798918bfce 100644 --- a/ocis-pkg/config/defaultconfig.go +++ b/ocis-pkg/config/defaultconfig.go @@ -20,6 +20,7 @@ import ( ocdav "github.com/owncloud/ocis/extensions/ocdav/pkg/config/defaults" ocs "github.com/owncloud/ocis/extensions/ocs/pkg/config/defaults" proxy "github.com/owncloud/ocis/extensions/proxy/pkg/config/defaults" + search "github.com/owncloud/ocis/extensions/search/pkg/config/defaults" settings "github.com/owncloud/ocis/extensions/settings/pkg/config/defaults" sharing "github.com/owncloud/ocis/extensions/sharing/pkg/config/defaults" storagemetadata "github.com/owncloud/ocis/extensions/storage-metadata/pkg/config/defaults" @@ -58,6 +59,7 @@ func DefaultConfig() *Config { OCDav: ocdav.DefaultConfig(), OCS: ocs.DefaultConfig(), Proxy: proxy.DefaultConfig(), + Search: search.FullDefaultConfig(), Settings: settings.DefaultConfig(), Sharing: sharing.DefaultConfig(), StorageMetadata: storagemetadata.DefaultConfig(), diff --git a/ocis/pkg/command/search.go b/ocis/pkg/command/search.go new file mode 100644 index 00000000000..61ef308842d --- /dev/null +++ b/ocis/pkg/command/search.go @@ -0,0 +1,26 @@ +package command + +import ( + "github.com/owncloud/ocis/extensions/search/pkg/command" + "github.com/owncloud/ocis/ocis-pkg/config" + "github.com/owncloud/ocis/ocis-pkg/config/parser" + "github.com/owncloud/ocis/ocis/pkg/register" + "github.com/urfave/cli/v2" +) + +// SearchCommand is the entry point for the search command. +func SearchCommand(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: cfg.Search.Service.Name, + Usage: subcommandDescription(cfg.Search.Service.Name), + Category: "extensions", + Before: func(ctx *cli.Context) error { + return parser.ParseConfig(cfg) + }, + Subcommands: command.GetCommands(cfg.Search), + } +} + +func init() { + register.AddCommand(SearchCommand) +} diff --git a/ocis/pkg/runtime/service/service.go b/ocis/pkg/runtime/service/service.go index 19a1cf02e50..99f947155b3 100644 --- a/ocis/pkg/runtime/service/service.go +++ b/ocis/pkg/runtime/service/service.go @@ -37,6 +37,7 @@ import ( ocdav "github.com/owncloud/ocis/extensions/ocdav/pkg/command" ocs "github.com/owncloud/ocis/extensions/ocs/pkg/command" proxy "github.com/owncloud/ocis/extensions/proxy/pkg/command" + search "github.com/owncloud/ocis/extensions/search/pkg/command" settings "github.com/owncloud/ocis/extensions/settings/pkg/command" sharing "github.com/owncloud/ocis/extensions/sharing/pkg/command" storagemetadata "github.com/owncloud/ocis/extensions/storage-metadata/pkg/command" @@ -131,6 +132,7 @@ func NewService(options ...Option) (*Service, error) { s.ServicesRegistry[opts.Config.StoragePublicLink.Service.Name] = storagepublic.NewStoragePublicLink s.ServicesRegistry[opts.Config.AppProvider.Service.Name] = appprovider.NewAppProvider s.ServicesRegistry[opts.Config.Notifications.Service.Name] = notifications.NewSutureService + s.ServicesRegistry[opts.Config.Search.Service.Name] = search.NewSutureService // populate delayed services s.Delayed[opts.Config.Sharing.Service.Name] = sharing.NewSharing diff --git a/protogen/gen/ocis/messages/accounts/v0/accounts.pb.go b/protogen/gen/ocis/messages/accounts/v0/accounts.pb.go index ced523b4aed..3937c7abad9 100644 --- a/protogen/gen/ocis/messages/accounts/v0/accounts.pb.go +++ b/protogen/gen/ocis/messages/accounts/v0/accounts.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc-gen-go v1.28.0 +// protoc (unknown) // source: ocis/messages/accounts/v0/accounts.proto package v0 diff --git a/protogen/gen/ocis/messages/search/v0/search.pb.go b/protogen/gen/ocis/messages/search/v0/search.pb.go new file mode 100644 index 00000000000..1f3acae8c2d --- /dev/null +++ b/protogen/gen/ocis/messages/search/v0/search.pb.go @@ -0,0 +1,471 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.0 +// protoc (unknown) +// source: ocis/messages/search/v0/search.proto + +package v0 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ResourceID struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StorageId string `protobuf:"bytes,1,opt,name=storage_id,json=storageId,proto3" json:"storage_id,omitempty"` + OpaqueId string `protobuf:"bytes,2,opt,name=opaque_id,json=opaqueId,proto3" json:"opaque_id,omitempty"` +} + +func (x *ResourceID) Reset() { + *x = ResourceID{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ResourceID) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceID) ProtoMessage() {} + +func (x *ResourceID) ProtoReflect() protoreflect.Message { + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceID.ProtoReflect.Descriptor instead. +func (*ResourceID) Descriptor() ([]byte, []int) { + return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{0} +} + +func (x *ResourceID) GetStorageId() string { + if x != nil { + return x.StorageId + } + return "" +} + +func (x *ResourceID) GetOpaqueId() string { + if x != nil { + return x.OpaqueId + } + return "" +} + +type Reference struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ResourceId *ResourceID `protobuf:"bytes,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` +} + +func (x *Reference) Reset() { + *x = Reference{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Reference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Reference) ProtoMessage() {} + +func (x *Reference) ProtoReflect() protoreflect.Message { + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Reference.ProtoReflect.Descriptor instead. +func (*Reference) Descriptor() ([]byte, []int) { + return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{1} +} + +func (x *Reference) GetResourceId() *ResourceID { + if x != nil { + return x.ResourceId + } + return nil +} + +func (x *Reference) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type Entity struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ref *Reference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + Id *ResourceID `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Etag string `protobuf:"bytes,4,opt,name=etag,proto3" json:"etag,omitempty"` + Size uint64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"` + LastModifiedTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=last_modified_time,json=lastModifiedTime,proto3" json:"last_modified_time,omitempty"` + MimeType string `protobuf:"bytes,7,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` + Permissions string `protobuf:"bytes,8,opt,name=permissions,proto3" json:"permissions,omitempty"` + Type uint64 `protobuf:"varint,9,opt,name=type,proto3" json:"type,omitempty"` + Deleted bool `protobuf:"varint,10,opt,name=deleted,proto3" json:"deleted,omitempty"` +} + +func (x *Entity) Reset() { + *x = Entity{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Entity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Entity) ProtoMessage() {} + +func (x *Entity) ProtoReflect() protoreflect.Message { + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Entity.ProtoReflect.Descriptor instead. +func (*Entity) Descriptor() ([]byte, []int) { + return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{2} +} + +func (x *Entity) GetRef() *Reference { + if x != nil { + return x.Ref + } + return nil +} + +func (x *Entity) GetId() *ResourceID { + if x != nil { + return x.Id + } + return nil +} + +func (x *Entity) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Entity) GetEtag() string { + if x != nil { + return x.Etag + } + return "" +} + +func (x *Entity) GetSize() uint64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *Entity) GetLastModifiedTime() *timestamppb.Timestamp { + if x != nil { + return x.LastModifiedTime + } + return nil +} + +func (x *Entity) GetMimeType() string { + if x != nil { + return x.MimeType + } + return "" +} + +func (x *Entity) GetPermissions() string { + if x != nil { + return x.Permissions + } + return "" +} + +func (x *Entity) GetType() uint64 { + if x != nil { + return x.Type + } + return 0 +} + +func (x *Entity) GetDeleted() bool { + if x != nil { + return x.Deleted + } + return false +} + +type Match struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // the matched entity + Entity *Entity `protobuf:"bytes,1,opt,name=entity,proto3" json:"entity,omitempty"` + // the match score + Score float32 `protobuf:"fixed32,2,opt,name=score,proto3" json:"score,omitempty"` +} + +func (x *Match) Reset() { + *x = Match{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Match) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Match) ProtoMessage() {} + +func (x *Match) ProtoReflect() protoreflect.Message { + mi := &file_ocis_messages_search_v0_search_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Match.ProtoReflect.Descriptor instead. +func (*Match) Descriptor() ([]byte, []int) { + return file_ocis_messages_search_v0_search_proto_rawDescGZIP(), []int{3} +} + +func (x *Match) GetEntity() *Entity { + if x != nil { + return x.Entity + } + return nil +} + +func (x *Match) GetScore() float32 { + if x != nil { + return x.Score + } + return 0 +} + +var File_ocis_messages_search_v0_search_proto protoreflect.FileDescriptor + +var file_ocis_messages_search_v0_search_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2f, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x17, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x1a, + 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0x48, 0x0a, 0x0a, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x12, 0x1d, + 0x0a, 0x0a, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x49, 0x64, 0x12, 0x1b, 0x0a, + 0x09, 0x6f, 0x70, 0x61, 0x71, 0x75, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x6f, 0x70, 0x61, 0x71, 0x75, 0x65, 0x49, 0x64, 0x22, 0x65, 0x0a, 0x09, 0x52, 0x65, + 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, + 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, + 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x44, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, + 0x68, 0x22, 0xe6, 0x02, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x03, + 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x63, 0x69, 0x73, + 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x03, 0x72, + 0x65, 0x66, 0x12, 0x33, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, + 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x44, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x65, + 0x74, 0x61, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x65, 0x74, 0x61, 0x67, 0x12, + 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, + 0x69, 0x7a, 0x65, 0x12, 0x48, 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x69, + 0x66, 0x69, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, 0x61, 0x73, + 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, + 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x56, 0x0a, 0x05, 0x4d, 0x61, + 0x74, 0x63, 0x68, 0x12, 0x37, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x45, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, + 0x72, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63, 0x69, 0x73, + 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_ocis_messages_search_v0_search_proto_rawDescOnce sync.Once + file_ocis_messages_search_v0_search_proto_rawDescData = file_ocis_messages_search_v0_search_proto_rawDesc +) + +func file_ocis_messages_search_v0_search_proto_rawDescGZIP() []byte { + file_ocis_messages_search_v0_search_proto_rawDescOnce.Do(func() { + file_ocis_messages_search_v0_search_proto_rawDescData = protoimpl.X.CompressGZIP(file_ocis_messages_search_v0_search_proto_rawDescData) + }) + return file_ocis_messages_search_v0_search_proto_rawDescData +} + +var file_ocis_messages_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_ocis_messages_search_v0_search_proto_goTypes = []interface{}{ + (*ResourceID)(nil), // 0: ocis.messages.search.v0.ResourceID + (*Reference)(nil), // 1: ocis.messages.search.v0.Reference + (*Entity)(nil), // 2: ocis.messages.search.v0.Entity + (*Match)(nil), // 3: ocis.messages.search.v0.Match + (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp +} +var file_ocis_messages_search_v0_search_proto_depIdxs = []int32{ + 0, // 0: ocis.messages.search.v0.Reference.resource_id:type_name -> ocis.messages.search.v0.ResourceID + 1, // 1: ocis.messages.search.v0.Entity.ref:type_name -> ocis.messages.search.v0.Reference + 0, // 2: ocis.messages.search.v0.Entity.id:type_name -> ocis.messages.search.v0.ResourceID + 4, // 3: ocis.messages.search.v0.Entity.last_modified_time:type_name -> google.protobuf.Timestamp + 2, // 4: ocis.messages.search.v0.Match.entity:type_name -> ocis.messages.search.v0.Entity + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_ocis_messages_search_v0_search_proto_init() } +func file_ocis_messages_search_v0_search_proto_init() { + if File_ocis_messages_search_v0_search_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_ocis_messages_search_v0_search_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ResourceID); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ocis_messages_search_v0_search_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Reference); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ocis_messages_search_v0_search_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Entity); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ocis_messages_search_v0_search_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Match); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_ocis_messages_search_v0_search_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_ocis_messages_search_v0_search_proto_goTypes, + DependencyIndexes: file_ocis_messages_search_v0_search_proto_depIdxs, + MessageInfos: file_ocis_messages_search_v0_search_proto_msgTypes, + }.Build() + File_ocis_messages_search_v0_search_proto = out.File + file_ocis_messages_search_v0_search_proto_rawDesc = nil + file_ocis_messages_search_v0_search_proto_goTypes = nil + file_ocis_messages_search_v0_search_proto_depIdxs = nil +} diff --git a/protogen/gen/ocis/messages/search/v0/search.pb.micro.go b/protogen/gen/ocis/messages/search/v0/search.pb.micro.go new file mode 100644 index 00000000000..37fd9b52b91 --- /dev/null +++ b/protogen/gen/ocis/messages/search/v0/search.pb.micro.go @@ -0,0 +1,16 @@ +// Code generated by protoc-gen-micro. DO NOT EDIT. +// source: ocis/messages/search/v0/search.proto + +package v0 + +import ( + fmt "fmt" + proto "google.golang.org/protobuf/proto" + _ "google.golang.org/protobuf/types/known/timestamppb" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf diff --git a/protogen/gen/ocis/messages/search/v0/search.pb.web.go b/protogen/gen/ocis/messages/search/v0/search.pb.web.go new file mode 100644 index 00000000000..7055e834977 --- /dev/null +++ b/protogen/gen/ocis/messages/search/v0/search.pb.web.go @@ -0,0 +1,155 @@ +// Code generated by protoc-gen-microweb. DO NOT EDIT. +// source: v0.proto + +package v0 + +import ( + "bytes" + "encoding/json" + + "github.com/golang/protobuf/jsonpb" +) + +// ResourceIDJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of ResourceID. This struct is safe to replace or modify but +// should not be done so concurrently. +var ResourceIDJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *ResourceID) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := ResourceIDJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*ResourceID)(nil) + +// ResourceIDJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of ResourceID. This struct is safe to replace or modify but +// should not be done so concurrently. +var ResourceIDJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *ResourceID) UnmarshalJSON(b []byte) error { + return ResourceIDJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*ResourceID)(nil) + +// ReferenceJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of Reference. This struct is safe to replace or modify but +// should not be done so concurrently. +var ReferenceJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *Reference) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := ReferenceJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*Reference)(nil) + +// ReferenceJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of Reference. This struct is safe to replace or modify but +// should not be done so concurrently. +var ReferenceJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *Reference) UnmarshalJSON(b []byte) error { + return ReferenceJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*Reference)(nil) + +// EntityJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of Entity. This struct is safe to replace or modify but +// should not be done so concurrently. +var EntityJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *Entity) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := EntityJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*Entity)(nil) + +// EntityJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of Entity. This struct is safe to replace or modify but +// should not be done so concurrently. +var EntityJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *Entity) UnmarshalJSON(b []byte) error { + return EntityJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*Entity)(nil) + +// MatchJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of Match. This struct is safe to replace or modify but +// should not be done so concurrently. +var MatchJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *Match) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := MatchJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*Match)(nil) + +// MatchJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of Match. This struct is safe to replace or modify but +// should not be done so concurrently. +var MatchJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *Match) UnmarshalJSON(b []byte) error { + return MatchJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*Match)(nil) diff --git a/protogen/gen/ocis/messages/search/v0/search.swagger.json b/protogen/gen/ocis/messages/search/v0/search.swagger.json new file mode 100644 index 00000000000..dd7fdc35883 --- /dev/null +++ b/protogen/gen/ocis/messages/search/v0/search.swagger.json @@ -0,0 +1,43 @@ +{ + "swagger": "2.0", + "info": { + "title": "ocis/messages/search/v0/search.proto", + "version": "version not set" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": {}, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + } + } +} diff --git a/protogen/gen/ocis/messages/settings/v0/settings.pb.go b/protogen/gen/ocis/messages/settings/v0/settings.pb.go index c4522665c36..c03cacf632e 100644 --- a/protogen/gen/ocis/messages/settings/v0/settings.pb.go +++ b/protogen/gen/ocis/messages/settings/v0/settings.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc-gen-go v1.28.0 +// protoc (unknown) // source: ocis/messages/settings/v0/settings.proto package v0 diff --git a/protogen/gen/ocis/messages/store/v0/store.pb.go b/protogen/gen/ocis/messages/store/v0/store.pb.go index 1471dda61c1..0783db6ec36 100644 --- a/protogen/gen/ocis/messages/store/v0/store.pb.go +++ b/protogen/gen/ocis/messages/store/v0/store.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc-gen-go v1.28.0 +// protoc (unknown) // source: ocis/messages/store/v0/store.proto package v0 diff --git a/protogen/gen/ocis/messages/thumbnails/v0/thumbnails.pb.go b/protogen/gen/ocis/messages/thumbnails/v0/thumbnails.pb.go index 25721838c88..d6198d3d7d5 100644 --- a/protogen/gen/ocis/messages/thumbnails/v0/thumbnails.pb.go +++ b/protogen/gen/ocis/messages/thumbnails/v0/thumbnails.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc-gen-go v1.28.0 +// protoc (unknown) // source: ocis/messages/thumbnails/v0/thumbnails.proto package v0 diff --git a/protogen/gen/ocis/services/accounts/v0/accounts.pb.go b/protogen/gen/ocis/services/accounts/v0/accounts.pb.go index bc4f5c54d7a..e5ff1ca4ca3 100644 --- a/protogen/gen/ocis/services/accounts/v0/accounts.pb.go +++ b/protogen/gen/ocis/services/accounts/v0/accounts.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc-gen-go v1.28.0 +// protoc (unknown) // source: ocis/services/accounts/v0/accounts.proto package v0 diff --git a/protogen/gen/ocis/services/search/v0/search.pb.go b/protogen/gen/ocis/services/search/v0/search.pb.go new file mode 100644 index 00000000000..236bfe95a01 --- /dev/null +++ b/protogen/gen/ocis/services/search/v0/search.pb.go @@ -0,0 +1,621 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.0 +// protoc (unknown) +// source: ocis/services/search/v0/search.proto + +package v0 + +import ( + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" + v0 "github.com/owncloud/ocis/protogen/gen/ocis/messages/search/v0" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/fieldmaskpb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SearchRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Optional. The maximum number of entries to return in the response + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A pagination token returned from a previous call to `Get` + // that indicates from where search should continue + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` +} + +func (x *SearchRequest) Reset() { + *x = SearchRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchRequest) ProtoMessage() {} + +func (x *SearchRequest) ProtoReflect() protoreflect.Message { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchRequest.ProtoReflect.Descriptor instead. +func (*SearchRequest) Descriptor() ([]byte, []int) { + return file_ocis_services_search_v0_search_proto_rawDescGZIP(), []int{0} +} + +func (x *SearchRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *SearchRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *SearchRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +type SearchResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Matches []*v0.Match `protobuf:"bytes,1,rep,name=matches,proto3" json:"matches,omitempty"` + // Token to retrieve the next page of results, or empty if there are no + // more results in the list + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` +} + +func (x *SearchResponse) Reset() { + *x = SearchResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchResponse) ProtoMessage() {} + +func (x *SearchResponse) ProtoReflect() protoreflect.Message { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchResponse.ProtoReflect.Descriptor instead. +func (*SearchResponse) Descriptor() ([]byte, []int) { + return file_ocis_services_search_v0_search_proto_rawDescGZIP(), []int{1} +} + +func (x *SearchResponse) GetMatches() []*v0.Match { + if x != nil { + return x.Matches + } + return nil +} + +func (x *SearchResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +type SearchIndexRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Optional. The maximum number of entries to return in the response + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A pagination token returned from a previous call to `Get` + // that indicates from where search should continue + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` + Ref *v0.Reference `protobuf:"bytes,4,opt,name=ref,proto3" json:"ref,omitempty"` +} + +func (x *SearchIndexRequest) Reset() { + *x = SearchIndexRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchIndexRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchIndexRequest) ProtoMessage() {} + +func (x *SearchIndexRequest) ProtoReflect() protoreflect.Message { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchIndexRequest.ProtoReflect.Descriptor instead. +func (*SearchIndexRequest) Descriptor() ([]byte, []int) { + return file_ocis_services_search_v0_search_proto_rawDescGZIP(), []int{2} +} + +func (x *SearchIndexRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *SearchIndexRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *SearchIndexRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *SearchIndexRequest) GetRef() *v0.Reference { + if x != nil { + return x.Ref + } + return nil +} + +type SearchIndexResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Matches []*v0.Match `protobuf:"bytes,1,rep,name=matches,proto3" json:"matches,omitempty"` + // Token to retrieve the next page of results, or empty if there are no + // more results in the list + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` +} + +func (x *SearchIndexResponse) Reset() { + *x = SearchIndexResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchIndexResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchIndexResponse) ProtoMessage() {} + +func (x *SearchIndexResponse) ProtoReflect() protoreflect.Message { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchIndexResponse.ProtoReflect.Descriptor instead. +func (*SearchIndexResponse) Descriptor() ([]byte, []int) { + return file_ocis_services_search_v0_search_proto_rawDescGZIP(), []int{3} +} + +func (x *SearchIndexResponse) GetMatches() []*v0.Match { + if x != nil { + return x.Matches + } + return nil +} + +func (x *SearchIndexResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +type IndexSpaceRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SpaceId string `protobuf:"bytes,1,opt,name=space_id,json=spaceId,proto3" json:"space_id,omitempty"` + UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` +} + +func (x *IndexSpaceRequest) Reset() { + *x = IndexSpaceRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IndexSpaceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IndexSpaceRequest) ProtoMessage() {} + +func (x *IndexSpaceRequest) ProtoReflect() protoreflect.Message { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IndexSpaceRequest.ProtoReflect.Descriptor instead. +func (*IndexSpaceRequest) Descriptor() ([]byte, []int) { + return file_ocis_services_search_v0_search_proto_rawDescGZIP(), []int{4} +} + +func (x *IndexSpaceRequest) GetSpaceId() string { + if x != nil { + return x.SpaceId + } + return "" +} + +func (x *IndexSpaceRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +type IndexSpaceResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *IndexSpaceResponse) Reset() { + *x = IndexSpaceResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IndexSpaceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IndexSpaceResponse) ProtoMessage() {} + +func (x *IndexSpaceResponse) ProtoReflect() protoreflect.Message { + mi := &file_ocis_services_search_v0_search_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IndexSpaceResponse.ProtoReflect.Descriptor instead. +func (*IndexSpaceResponse) Descriptor() ([]byte, []int) { + return file_ocis_services_search_v0_search_proto_rawDescGZIP(), []int{5} +} + +var File_ocis_services_search_v0_search_proto protoreflect.FileDescriptor + +var file_ocis_services_search_v0_search_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x17, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x1a, + 0x24, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x73, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, + 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6d, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, + 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, + 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0a, 0x70, 0x61, + 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, + 0xe2, 0x41, 0x01, 0x01, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x72, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, + 0x76, 0x30, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, + 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, + 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xae, 0x01, 0x0a, 0x12, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x21, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, + 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x09, 0x70, + 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x3a, + 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x63, + 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x42, + 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x03, 0x72, 0x65, 0x66, 0x22, 0x77, 0x0a, 0x13, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x38, 0x0a, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x4d, 0x61, 0x74, + 0x63, 0x68, 0x52, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, + 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x22, 0x47, 0x0a, 0x11, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x14, 0x0a, 0x12, + 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x32, 0x9c, 0x02, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x7b, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, + 0x26, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, + 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, + 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x3a, + 0x01, 0x2a, 0x12, 0x8c, 0x01, 0x0a, 0x0a, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, + 0x65, 0x12, 0x2a, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x49, 0x6e, 0x64, 0x65, + 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, + 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x1f, 0x22, 0x1a, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x2f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2d, 0x73, 0x70, 0x61, 0x63, 0x65, 0x3a, 0x01, + 0x2a, 0x32, 0x9d, 0x01, 0x0a, 0x0d, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x8b, 0x01, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x2b, + 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6f, 0x63, + 0x69, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x6e, 0x64, 0x65, + 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x20, 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x2f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x3a, 0x01, + 0x2a, 0x42, 0xde, 0x02, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63, 0x69, 0x73, + 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, + 0x76, 0x30, 0x92, 0x41, 0x9c, 0x02, 0x12, 0xb4, 0x01, 0x0a, 0x1e, 0x6f, 0x77, 0x6e, 0x43, 0x6c, + 0x6f, 0x75, 0x64, 0x20, 0x49, 0x6e, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x65, 0x20, 0x53, 0x63, 0x61, + 0x6c, 0x65, 0x20, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x22, 0x47, 0x0a, 0x0d, 0x6f, 0x77, 0x6e, + 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x47, 0x6d, 0x62, 0x48, 0x12, 0x20, 0x68, 0x74, 0x74, 0x70, + 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, + 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x1a, 0x14, 0x73, 0x75, + 0x70, 0x70, 0x6f, 0x72, 0x74, 0x40, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x63, + 0x6f, 0x6d, 0x2a, 0x42, 0x0a, 0x0a, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d, 0x32, 0x2e, 0x30, + 0x12, 0x34, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, + 0x69, 0x73, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x2f, 0x4c, + 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x05, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x2a, 0x02, 0x01, + 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, + 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x72, 0x3b, 0x0a, 0x10, 0x44, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, + 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x12, 0x27, 0x68, 0x74, 0x74, 0x70, 0x73, + 0x3a, 0x2f, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x64, 0x65, 0x76, 0x2f, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_ocis_services_search_v0_search_proto_rawDescOnce sync.Once + file_ocis_services_search_v0_search_proto_rawDescData = file_ocis_services_search_v0_search_proto_rawDesc +) + +func file_ocis_services_search_v0_search_proto_rawDescGZIP() []byte { + file_ocis_services_search_v0_search_proto_rawDescOnce.Do(func() { + file_ocis_services_search_v0_search_proto_rawDescData = protoimpl.X.CompressGZIP(file_ocis_services_search_v0_search_proto_rawDescData) + }) + return file_ocis_services_search_v0_search_proto_rawDescData +} + +var file_ocis_services_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_ocis_services_search_v0_search_proto_goTypes = []interface{}{ + (*SearchRequest)(nil), // 0: ocis.services.search.v0.SearchRequest + (*SearchResponse)(nil), // 1: ocis.services.search.v0.SearchResponse + (*SearchIndexRequest)(nil), // 2: ocis.services.search.v0.SearchIndexRequest + (*SearchIndexResponse)(nil), // 3: ocis.services.search.v0.SearchIndexResponse + (*IndexSpaceRequest)(nil), // 4: ocis.services.search.v0.IndexSpaceRequest + (*IndexSpaceResponse)(nil), // 5: ocis.services.search.v0.IndexSpaceResponse + (*v0.Match)(nil), // 6: ocis.messages.search.v0.Match + (*v0.Reference)(nil), // 7: ocis.messages.search.v0.Reference +} +var file_ocis_services_search_v0_search_proto_depIdxs = []int32{ + 6, // 0: ocis.services.search.v0.SearchResponse.matches:type_name -> ocis.messages.search.v0.Match + 7, // 1: ocis.services.search.v0.SearchIndexRequest.ref:type_name -> ocis.messages.search.v0.Reference + 6, // 2: ocis.services.search.v0.SearchIndexResponse.matches:type_name -> ocis.messages.search.v0.Match + 0, // 3: ocis.services.search.v0.SearchProvider.Search:input_type -> ocis.services.search.v0.SearchRequest + 4, // 4: ocis.services.search.v0.SearchProvider.IndexSpace:input_type -> ocis.services.search.v0.IndexSpaceRequest + 2, // 5: ocis.services.search.v0.IndexProvider.Search:input_type -> ocis.services.search.v0.SearchIndexRequest + 1, // 6: ocis.services.search.v0.SearchProvider.Search:output_type -> ocis.services.search.v0.SearchResponse + 5, // 7: ocis.services.search.v0.SearchProvider.IndexSpace:output_type -> ocis.services.search.v0.IndexSpaceResponse + 3, // 8: ocis.services.search.v0.IndexProvider.Search:output_type -> ocis.services.search.v0.SearchIndexResponse + 6, // [6:9] is the sub-list for method output_type + 3, // [3:6] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_ocis_services_search_v0_search_proto_init() } +func file_ocis_services_search_v0_search_proto_init() { + if File_ocis_services_search_v0_search_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_ocis_services_search_v0_search_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SearchRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ocis_services_search_v0_search_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SearchResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ocis_services_search_v0_search_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SearchIndexRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ocis_services_search_v0_search_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SearchIndexResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ocis_services_search_v0_search_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IndexSpaceRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ocis_services_search_v0_search_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IndexSpaceResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_ocis_services_search_v0_search_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_ocis_services_search_v0_search_proto_goTypes, + DependencyIndexes: file_ocis_services_search_v0_search_proto_depIdxs, + MessageInfos: file_ocis_services_search_v0_search_proto_msgTypes, + }.Build() + File_ocis_services_search_v0_search_proto = out.File + file_ocis_services_search_v0_search_proto_rawDesc = nil + file_ocis_services_search_v0_search_proto_goTypes = nil + file_ocis_services_search_v0_search_proto_depIdxs = nil +} diff --git a/protogen/gen/ocis/services/search/v0/search.pb.micro.go b/protogen/gen/ocis/services/search/v0/search.pb.micro.go new file mode 100644 index 00000000000..06e5ea9aedb --- /dev/null +++ b/protogen/gen/ocis/services/search/v0/search.pb.micro.go @@ -0,0 +1,211 @@ +// Code generated by protoc-gen-micro. DO NOT EDIT. +// source: ocis/services/search/v0/search.proto + +package v0 + +import ( + fmt "fmt" + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" + _ "github.com/owncloud/ocis/protogen/gen/ocis/messages/search/v0" + _ "google.golang.org/genproto/googleapis/api/annotations" + proto "google.golang.org/protobuf/proto" + _ "google.golang.org/protobuf/types/known/fieldmaskpb" + math "math" +) + +import ( + context "context" + api "go-micro.dev/v4/api" + client "go-micro.dev/v4/client" + server "go-micro.dev/v4/server" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// Reference imports to suppress errors if they are not otherwise used. +var _ api.Endpoint +var _ context.Context +var _ client.Option +var _ server.Option + +// Api Endpoints for SearchProvider service + +func NewSearchProviderEndpoints() []*api.Endpoint { + return []*api.Endpoint{ + { + Name: "SearchProvider.Search", + Path: []string{"/api/v0/search/search"}, + Method: []string{"POST"}, + Body: "*", + Handler: "rpc", + }, + { + Name: "SearchProvider.IndexSpace", + Path: []string{"/api/v0/search/index-space"}, + Method: []string{"POST"}, + Body: "*", + Handler: "rpc", + }, + } +} + +// Client API for SearchProvider service + +type SearchProviderService interface { + Search(ctx context.Context, in *SearchRequest, opts ...client.CallOption) (*SearchResponse, error) + IndexSpace(ctx context.Context, in *IndexSpaceRequest, opts ...client.CallOption) (*IndexSpaceResponse, error) +} + +type searchProviderService struct { + c client.Client + name string +} + +func NewSearchProviderService(name string, c client.Client) SearchProviderService { + return &searchProviderService{ + c: c, + name: name, + } +} + +func (c *searchProviderService) Search(ctx context.Context, in *SearchRequest, opts ...client.CallOption) (*SearchResponse, error) { + req := c.c.NewRequest(c.name, "SearchProvider.Search", in) + out := new(SearchResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *searchProviderService) IndexSpace(ctx context.Context, in *IndexSpaceRequest, opts ...client.CallOption) (*IndexSpaceResponse, error) { + req := c.c.NewRequest(c.name, "SearchProvider.IndexSpace", in) + out := new(IndexSpaceResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for SearchProvider service + +type SearchProviderHandler interface { + Search(context.Context, *SearchRequest, *SearchResponse) error + IndexSpace(context.Context, *IndexSpaceRequest, *IndexSpaceResponse) error +} + +func RegisterSearchProviderHandler(s server.Server, hdlr SearchProviderHandler, opts ...server.HandlerOption) error { + type searchProvider interface { + Search(ctx context.Context, in *SearchRequest, out *SearchResponse) error + IndexSpace(ctx context.Context, in *IndexSpaceRequest, out *IndexSpaceResponse) error + } + type SearchProvider struct { + searchProvider + } + h := &searchProviderHandler{hdlr} + opts = append(opts, api.WithEndpoint(&api.Endpoint{ + Name: "SearchProvider.Search", + Path: []string{"/api/v0/search/search"}, + Method: []string{"POST"}, + Body: "*", + Handler: "rpc", + })) + opts = append(opts, api.WithEndpoint(&api.Endpoint{ + Name: "SearchProvider.IndexSpace", + Path: []string{"/api/v0/search/index-space"}, + Method: []string{"POST"}, + Body: "*", + Handler: "rpc", + })) + return s.Handle(s.NewHandler(&SearchProvider{h}, opts...)) +} + +type searchProviderHandler struct { + SearchProviderHandler +} + +func (h *searchProviderHandler) Search(ctx context.Context, in *SearchRequest, out *SearchResponse) error { + return h.SearchProviderHandler.Search(ctx, in, out) +} + +func (h *searchProviderHandler) IndexSpace(ctx context.Context, in *IndexSpaceRequest, out *IndexSpaceResponse) error { + return h.SearchProviderHandler.IndexSpace(ctx, in, out) +} + +// Api Endpoints for IndexProvider service + +func NewIndexProviderEndpoints() []*api.Endpoint { + return []*api.Endpoint{ + { + Name: "IndexProvider.Search", + Path: []string{"/api/v0/search/index/search"}, + Method: []string{"POST"}, + Body: "*", + Handler: "rpc", + }, + } +} + +// Client API for IndexProvider service + +type IndexProviderService interface { + Search(ctx context.Context, in *SearchIndexRequest, opts ...client.CallOption) (*SearchIndexResponse, error) +} + +type indexProviderService struct { + c client.Client + name string +} + +func NewIndexProviderService(name string, c client.Client) IndexProviderService { + return &indexProviderService{ + c: c, + name: name, + } +} + +func (c *indexProviderService) Search(ctx context.Context, in *SearchIndexRequest, opts ...client.CallOption) (*SearchIndexResponse, error) { + req := c.c.NewRequest(c.name, "IndexProvider.Search", in) + out := new(SearchIndexResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for IndexProvider service + +type IndexProviderHandler interface { + Search(context.Context, *SearchIndexRequest, *SearchIndexResponse) error +} + +func RegisterIndexProviderHandler(s server.Server, hdlr IndexProviderHandler, opts ...server.HandlerOption) error { + type indexProvider interface { + Search(ctx context.Context, in *SearchIndexRequest, out *SearchIndexResponse) error + } + type IndexProvider struct { + indexProvider + } + h := &indexProviderHandler{hdlr} + opts = append(opts, api.WithEndpoint(&api.Endpoint{ + Name: "IndexProvider.Search", + Path: []string{"/api/v0/search/index/search"}, + Method: []string{"POST"}, + Body: "*", + Handler: "rpc", + })) + return s.Handle(s.NewHandler(&IndexProvider{h}, opts...)) +} + +type indexProviderHandler struct { + IndexProviderHandler +} + +func (h *indexProviderHandler) Search(ctx context.Context, in *SearchIndexRequest, out *SearchIndexResponse) error { + return h.IndexProviderHandler.Search(ctx, in, out) +} diff --git a/protogen/gen/ocis/services/search/v0/search.pb.web.go b/protogen/gen/ocis/services/search/v0/search.pb.web.go new file mode 100644 index 00000000000..427033152b2 --- /dev/null +++ b/protogen/gen/ocis/services/search/v0/search.pb.web.go @@ -0,0 +1,333 @@ +// Code generated by protoc-gen-microweb. DO NOT EDIT. +// source: v0.proto + +package v0 + +import ( + "bytes" + "encoding/json" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/golang/protobuf/jsonpb" +) + +type webSearchProviderHandler struct { + r chi.Router + h SearchProviderHandler +} + +func (h *webSearchProviderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.r.ServeHTTP(w, r) +} + +func (h *webSearchProviderHandler) Search(w http.ResponseWriter, r *http.Request) { + req := &SearchRequest{} + resp := &SearchResponse{} + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusPreconditionFailed) + return + } + + if err := h.h.Search( + r.Context(), + req, + resp, + ); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + render.Status(r, http.StatusCreated) + render.JSON(w, r, resp) +} + +func (h *webSearchProviderHandler) IndexSpace(w http.ResponseWriter, r *http.Request) { + req := &IndexSpaceRequest{} + resp := &IndexSpaceResponse{} + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusPreconditionFailed) + return + } + + if err := h.h.IndexSpace( + r.Context(), + req, + resp, + ); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + render.Status(r, http.StatusCreated) + render.JSON(w, r, resp) +} + +func RegisterSearchProviderWeb(r chi.Router, i SearchProviderHandler, middlewares ...func(http.Handler) http.Handler) { + handler := &webSearchProviderHandler{ + r: r, + h: i, + } + + r.MethodFunc("POST", "/api/v0/search/search", handler.Search) + r.MethodFunc("POST", "/api/v0/search/index-space", handler.IndexSpace) +} + +type webIndexProviderHandler struct { + r chi.Router + h IndexProviderHandler +} + +func (h *webIndexProviderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.r.ServeHTTP(w, r) +} + +func (h *webIndexProviderHandler) Search(w http.ResponseWriter, r *http.Request) { + req := &SearchIndexRequest{} + resp := &SearchIndexResponse{} + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusPreconditionFailed) + return + } + + if err := h.h.Search( + r.Context(), + req, + resp, + ); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + render.Status(r, http.StatusCreated) + render.JSON(w, r, resp) +} + +func RegisterIndexProviderWeb(r chi.Router, i IndexProviderHandler, middlewares ...func(http.Handler) http.Handler) { + handler := &webIndexProviderHandler{ + r: r, + h: i, + } + + r.MethodFunc("POST", "/api/v0/search/index/search", handler.Search) +} + +// SearchRequestJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of SearchRequest. This struct is safe to replace or modify but +// should not be done so concurrently. +var SearchRequestJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *SearchRequest) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := SearchRequestJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*SearchRequest)(nil) + +// SearchRequestJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of SearchRequest. This struct is safe to replace or modify but +// should not be done so concurrently. +var SearchRequestJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *SearchRequest) UnmarshalJSON(b []byte) error { + return SearchRequestJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*SearchRequest)(nil) + +// SearchResponseJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of SearchResponse. This struct is safe to replace or modify but +// should not be done so concurrently. +var SearchResponseJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *SearchResponse) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := SearchResponseJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*SearchResponse)(nil) + +// SearchResponseJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of SearchResponse. This struct is safe to replace or modify but +// should not be done so concurrently. +var SearchResponseJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *SearchResponse) UnmarshalJSON(b []byte) error { + return SearchResponseJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*SearchResponse)(nil) + +// SearchIndexRequestJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of SearchIndexRequest. This struct is safe to replace or modify but +// should not be done so concurrently. +var SearchIndexRequestJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *SearchIndexRequest) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := SearchIndexRequestJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*SearchIndexRequest)(nil) + +// SearchIndexRequestJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of SearchIndexRequest. This struct is safe to replace or modify but +// should not be done so concurrently. +var SearchIndexRequestJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *SearchIndexRequest) UnmarshalJSON(b []byte) error { + return SearchIndexRequestJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*SearchIndexRequest)(nil) + +// SearchIndexResponseJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of SearchIndexResponse. This struct is safe to replace or modify but +// should not be done so concurrently. +var SearchIndexResponseJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *SearchIndexResponse) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := SearchIndexResponseJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*SearchIndexResponse)(nil) + +// SearchIndexResponseJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of SearchIndexResponse. This struct is safe to replace or modify but +// should not be done so concurrently. +var SearchIndexResponseJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *SearchIndexResponse) UnmarshalJSON(b []byte) error { + return SearchIndexResponseJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*SearchIndexResponse)(nil) + +// IndexSpaceRequestJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of IndexSpaceRequest. This struct is safe to replace or modify but +// should not be done so concurrently. +var IndexSpaceRequestJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *IndexSpaceRequest) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := IndexSpaceRequestJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*IndexSpaceRequest)(nil) + +// IndexSpaceRequestJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of IndexSpaceRequest. This struct is safe to replace or modify but +// should not be done so concurrently. +var IndexSpaceRequestJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *IndexSpaceRequest) UnmarshalJSON(b []byte) error { + return IndexSpaceRequestJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*IndexSpaceRequest)(nil) + +// IndexSpaceResponseJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of IndexSpaceResponse. This struct is safe to replace or modify but +// should not be done so concurrently. +var IndexSpaceResponseJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *IndexSpaceResponse) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := IndexSpaceResponseJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*IndexSpaceResponse)(nil) + +// IndexSpaceResponseJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of IndexSpaceResponse. This struct is safe to replace or modify but +// should not be done so concurrently. +var IndexSpaceResponseJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *IndexSpaceResponse) UnmarshalJSON(b []byte) error { + return IndexSpaceResponseJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*IndexSpaceResponse)(nil) diff --git a/protogen/gen/ocis/services/search/v0/search.swagger.json b/protogen/gen/ocis/services/search/v0/search.swagger.json new file mode 100644 index 00000000000..55b025f1216 --- /dev/null +++ b/protogen/gen/ocis/services/search/v0/search.swagger.json @@ -0,0 +1,320 @@ +{ + "swagger": "2.0", + "info": { + "title": "ownCloud Infinite Scale search", + "version": "1.0.0", + "contact": { + "name": "ownCloud GmbH", + "url": "https://github.com/owncloud/ocis", + "email": "support@owncloud.com" + }, + "license": { + "name": "Apache-2.0", + "url": "https://github.com/owncloud/ocis/blob/master/LICENSE" + } + }, + "tags": [ + { + "name": "SearchProvider" + }, + { + "name": "IndexProvider" + } + ], + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/api/v0/search/index-space": { + "post": { + "operationId": "SearchProvider_IndexSpace", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v0IndexSpaceResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v0IndexSpaceRequest" + } + } + ], + "tags": [ + "SearchProvider" + ] + } + }, + "/api/v0/search/index/search": { + "post": { + "operationId": "IndexProvider_Search", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v0SearchIndexResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v0SearchIndexRequest" + } + } + ], + "tags": [ + "IndexProvider" + ] + } + }, + "/api/v0/search/search": { + "post": { + "operationId": "SearchProvider_Search", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v0SearchResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v0SearchRequest" + } + } + ], + "tags": [ + "SearchProvider" + ] + } + } + }, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v0Entity": { + "type": "object", + "properties": { + "ref": { + "$ref": "#/definitions/v0Reference" + }, + "id": { + "$ref": "#/definitions/v0ResourceID" + }, + "name": { + "type": "string" + }, + "etag": { + "type": "string" + }, + "size": { + "type": "string", + "format": "uint64" + }, + "lastModifiedTime": { + "type": "string", + "format": "date-time" + }, + "mimeType": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "type": { + "type": "string", + "format": "uint64" + }, + "deleted": { + "type": "boolean" + } + } + }, + "v0IndexSpaceRequest": { + "type": "object", + "properties": { + "spaceId": { + "type": "string" + }, + "userId": { + "type": "string" + } + } + }, + "v0IndexSpaceResponse": { + "type": "object" + }, + "v0Match": { + "type": "object", + "properties": { + "entity": { + "$ref": "#/definitions/v0Entity", + "title": "the matched entity" + }, + "score": { + "type": "number", + "format": "float", + "title": "the match score" + } + } + }, + "v0Reference": { + "type": "object", + "properties": { + "resourceId": { + "$ref": "#/definitions/v0ResourceID" + }, + "path": { + "type": "string" + } + } + }, + "v0ResourceID": { + "type": "object", + "properties": { + "storageId": { + "type": "string" + }, + "opaqueId": { + "type": "string" + } + } + }, + "v0SearchIndexRequest": { + "type": "object", + "properties": { + "pageSize": { + "type": "integer", + "format": "int32", + "title": "Optional. The maximum number of entries to return in the response" + }, + "pageToken": { + "type": "string", + "title": "Optional. A pagination token returned from a previous call to `Get`\nthat indicates from where search should continue" + }, + "query": { + "type": "string" + }, + "ref": { + "$ref": "#/definitions/v0Reference" + } + } + }, + "v0SearchIndexResponse": { + "type": "object", + "properties": { + "matches": { + "type": "array", + "items": { + "$ref": "#/definitions/v0Match" + } + }, + "nextPageToken": { + "type": "string", + "title": "Token to retrieve the next page of results, or empty if there are no\nmore results in the list" + } + } + }, + "v0SearchRequest": { + "type": "object", + "properties": { + "pageSize": { + "type": "integer", + "format": "int32", + "title": "Optional. The maximum number of entries to return in the response" + }, + "pageToken": { + "type": "string", + "title": "Optional. A pagination token returned from a previous call to `Get`\nthat indicates from where search should continue" + }, + "query": { + "type": "string" + } + } + }, + "v0SearchResponse": { + "type": "object", + "properties": { + "matches": { + "type": "array", + "items": { + "$ref": "#/definitions/v0Match" + } + }, + "nextPageToken": { + "type": "string", + "title": "Token to retrieve the next page of results, or empty if there are no\nmore results in the list" + } + } + } + }, + "externalDocs": { + "description": "Developer Manual", + "url": "https://owncloud.dev/extensions/search/" + } +} diff --git a/protogen/gen/ocis/services/settings/v0/settings.pb.go b/protogen/gen/ocis/services/settings/v0/settings.pb.go index 5eeeb956652..52afe123ca1 100644 --- a/protogen/gen/ocis/services/settings/v0/settings.pb.go +++ b/protogen/gen/ocis/services/settings/v0/settings.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc-gen-go v1.28.0 +// protoc (unknown) // source: ocis/services/settings/v0/settings.proto package v0 diff --git a/protogen/gen/ocis/services/store/v0/store.pb.go b/protogen/gen/ocis/services/store/v0/store.pb.go index 5dd2fc43648..7d5b055470d 100644 --- a/protogen/gen/ocis/services/store/v0/store.pb.go +++ b/protogen/gen/ocis/services/store/v0/store.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc-gen-go v1.28.0 +// protoc (unknown) // source: ocis/services/store/v0/store.proto package v0 diff --git a/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go b/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go index cf949fb4563..03daa47a76b 100644 --- a/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go +++ b/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc-gen-go v1.28.0 +// protoc (unknown) // source: ocis/services/thumbnails/v0/thumbnails.proto package v0 diff --git a/protogen/proto/ocis/messages/search/v0/search.proto b/protogen/proto/ocis/messages/search/v0/search.proto new file mode 100644 index 00000000000..b3729e47e5d --- /dev/null +++ b/protogen/proto/ocis/messages/search/v0/search.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package ocis.messages.search.v0; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/owncloud/ocis/protogen/gen/ocis/messages/search/v0"; + +message ResourceID { + string storage_id = 1; + string opaque_id = 2; +} + +message Reference { + ResourceID resource_id = 1; + string path = 2; +} + +message Entity { + Reference ref = 1; + ResourceID id = 2; + string name = 3; + string etag = 4; + uint64 size = 5; + google.protobuf.Timestamp last_modified_time = 6; + string mime_type = 7; + string permissions = 8; + uint64 type = 9; + bool deleted = 10; +} + +message Match { + // the matched entity + Entity entity = 1; + // the match score + float score = 2; +} \ No newline at end of file diff --git a/protogen/proto/ocis/services/search/v0/search.proto b/protogen/proto/ocis/services/search/v0/search.proto new file mode 100644 index 00000000000..7d78dae35a4 --- /dev/null +++ b/protogen/proto/ocis/services/search/v0/search.proto @@ -0,0 +1,107 @@ +syntax = "proto3"; + +package ocis.services.search.v0; + +option go_package = "github.com/owncloud/ocis/protogen/gen/ocis/service/search/v0"; + +import "ocis/messages/search/v0/search.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/field_mask.proto"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "ownCloud Infinite Scale search"; + version: "1.0.0"; + contact: { + name: "ownCloud GmbH"; + url: "https://github.com/owncloud/ocis"; + email: "support@owncloud.com"; + }; + license: { + name: "Apache-2.0"; + url: "https://github.com/owncloud/ocis/blob/master/LICENSE"; + }; + }; + schemes: HTTP; + schemes: HTTPS; + consumes: "application/json"; + produces: "application/json"; + external_docs: { + description: "Developer Manual"; + url: "https://owncloud.dev/extensions/search/"; + }; +}; + +service SearchProvider { + rpc Search(SearchRequest) returns (SearchResponse) { + option (google.api.http) = { + post: "/api/v0/search/search", + body: "*" + }; + }; + rpc IndexSpace(IndexSpaceRequest) returns (IndexSpaceResponse) { + option (google.api.http) = { + post: "/api/v0/search/index-space", + body: "*" + }; + } +} + +service IndexProvider { + rpc Search(SearchIndexRequest) returns (SearchIndexResponse) { + option (google.api.http) = { + post: "/api/v0/search/index/search", + body: "*" + }; + }; + // rpc Remove(RemoveRequest) returns (RemoveResponse) {}; +} + +message SearchRequest { + // Optional. The maximum number of entries to return in the response + int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A pagination token returned from a previous call to `Get` + // that indicates from where search should continue + string page_token = 2 [(google.api.field_behavior) = OPTIONAL]; + + string query = 3; +} + +message SearchResponse { + repeated ocis.messages.search.v0.Match matches = 1; + + // Token to retrieve the next page of results, or empty if there are no + // more results in the list + string next_page_token = 2; +} + +message SearchIndexRequest { + // Optional. The maximum number of entries to return in the response + int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A pagination token returned from a previous call to `Get` + // that indicates from where search should continue + string page_token = 2 [(google.api.field_behavior) = OPTIONAL]; + + string query = 3; + ocis.messages.search.v0.Reference ref = 4 [(google.api.field_behavior) = OPTIONAL]; +} + +message SearchIndexResponse { + repeated ocis.messages.search.v0.Match matches = 1; + + // Token to retrieve the next page of results, or empty if there are no + // more results in the list + string next_page_token = 2; +} + +message IndexSpaceRequest { + string space_id = 1; + string user_id = 2; +} + +message IndexSpaceResponse { +} \ No newline at end of file diff --git a/tests/acceptance/expected-failures-graphAPI-on-OCIS-storage.md b/tests/acceptance/expected-failures-graphAPI-on-OCIS-storage.md index f513a93f0a1..24c99648129 100644 --- a/tests/acceptance/expected-failures-graphAPI-on-OCIS-storage.md +++ b/tests/acceptance/expected-failures-graphAPI-on-OCIS-storage.md @@ -1016,6 +1016,15 @@ _ocdav: api compatibility, return correct status code_ - [apiWebdavOperations/search.feature:265](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavOperations/search.feature#L265) - [apiWebdavOperations/search.feature:270](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavOperations/search.feature#L270) +#### [Support for favorites](https://github.com/owncloud/ocis/issues/1228) + +- [apiFavorites/favorites.feature:115](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L115) +- [apiFavorites/favorites.feature:116](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L116) +- [apiFavorites/favorites.feature:141](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L141) +- [apiFavorites/favorites.feature:142](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L142) +- [apiFavorites/favorites.feature:267](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L267) +- [apiFavorites/favorites.feature:268](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L268) + And other missing implementation of favorites - [apiFavorites/favorites.feature:162](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiFavorites/favorites.feature#L162)