mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-10 12:48:29 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bafa4bd18 |
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
|
||||
occfg "github.com/opencloud-eu/opencloud/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/pkg/shared"
|
||||
"github.com/opencloud-eu/opencloud/services/activitylog/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/services/activitylog/pkg/config/defaults"
|
||||
|
||||
@@ -34,5 +35,8 @@ func ParseConfig(cfg *config.Config) error {
|
||||
|
||||
// Validate validates the config
|
||||
func Validate(cfg *config.Config) error {
|
||||
if cfg.Events.Disabled && cfg.HTTP.Disabled {
|
||||
return shared.AllComponentsDisabledError(cfg.Service.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -63,10 +63,6 @@ Store specific notes:
|
||||
- When using `nats-js-kv` it is recommended to set `OC_CACHE_STORE_NODES` to the same value as `OC_EVENTS_ENDPOINT`. That way the cache uses the same nats instance as the event bus.
|
||||
- When using the `nats-js-kv` store, it is possible to set `OC_CACHE_DISABLE_PERSISTENCE` to instruct nats to not persist cache data on disc.
|
||||
|
||||
### Auto-Accept Shares
|
||||
|
||||
When setting the `SHARING_AUTO_ACCEPT_SHARES` to `true` (sharing service), all incoming shares will be accepted automatically. Users can overwrite this setting individually in their profile. The deprecated `FRONTEND_AUTO_ACCEPT_SHARES` is still supported for backwards compatibility.
|
||||
|
||||
## Passwords
|
||||
|
||||
### The Password Policy
|
||||
|
||||
@@ -40,11 +40,7 @@ func ParseConfig(cfg *config.Config) error {
|
||||
|
||||
func Validate(cfg *config.Config) error {
|
||||
if cfg.HTTP.Disabled && cfg.Events.DisabledConsumer {
|
||||
// might be debatable, but this situation should be treated as an error,
|
||||
// as the process wouldn't be able to serve either API and would thus be
|
||||
// completely useless -- in that case, just don't start this service
|
||||
// in the first place (especially since it's optional)
|
||||
return shared.AllComponentsDisabledError("graph")
|
||||
return shared.AllComponentsDisabledError(cfg.Service.Name)
|
||||
}
|
||||
|
||||
if cfg.TokenManager.JWTSecret == "" {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
|
||||
occfg "github.com/opencloud-eu/opencloud/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/pkg/shared"
|
||||
"github.com/opencloud-eu/opencloud/services/policies/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/services/policies/pkg/config/defaults"
|
||||
|
||||
@@ -33,12 +34,8 @@ func ParseConfig(cfg *config.Config) error {
|
||||
}
|
||||
|
||||
func Validate(cfg *config.Config) error {
|
||||
if cfg.GRPC.Disabled && cfg.Events.Disabled {
|
||||
// might be debatable, but this situation should be treated as an error,
|
||||
// as the process wouldn't be able to serve either API and would thus be
|
||||
// completely useless -- in that case, just don't start this service
|
||||
// in the first place (especially since it's optional)
|
||||
return errors.New("both gRPC and events APIs are disabled by configuration; at least one must be enabled")
|
||||
if cfg.Events.Disabled && cfg.GRPC.Disabled {
|
||||
return shared.AllComponentsDisabledError(cfg.Service.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -13,7 +13,7 @@ Fill the new index by indexing all spaces again:
|
||||
|
||||
```shell
|
||||
# the service keeps running while it happens
|
||||
opencloud search index --all-spaces --insecure
|
||||
opencloud search index --all-spaces
|
||||
```
|
||||
|
||||
Once the new index is filled, every index but the one with the highest
|
||||
@@ -31,7 +31,7 @@ The new index is a directory next to the old `bleve` one, both in
|
||||
bleve index cannot be copied, index all spaces again:
|
||||
|
||||
```shell
|
||||
opencloud search index --all-spaces --insecure
|
||||
opencloud search index --all-spaces
|
||||
```
|
||||
|
||||
Once the new index is filled, every directory but the one with the highest
|
||||
|
||||
@@ -124,14 +124,14 @@ opencloud search index --space $SPACE_ID
|
||||
It can also be used to re-index all spaces:
|
||||
|
||||
```shell
|
||||
opencloud search index --all-spaces --insecure
|
||||
opencloud search index --all-spaces
|
||||
```
|
||||
|
||||
Please note that a reindex only picks up new or changed files. Files that have already been indexed are not scanned again, even if the configuration or the whole extractor has been changed. To force a full rescan (re-running the extractor on every file) you need to use the `force-rescan` flag:
|
||||
|
||||
|
||||
```shell
|
||||
opencloud search index --all-spaces --force-rescan --insecure
|
||||
opencloud search index --all-spaces --force-rescan
|
||||
```
|
||||
|
||||
## Metrics
|
||||
|
||||
@@ -75,10 +75,14 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques
|
||||
},
|
||||
)
|
||||
// Scope below the space root: restrict at query level so totals and
|
||||
// paging respect the path too. The folder term matches the folder and
|
||||
// its descendants (see PathAnalyzer).
|
||||
// paging respect the path too. Path is a case-preserving keyword
|
||||
// (paths act as references, /Foo and /foo are distinct), so the exact
|
||||
// folder or the folder prefix matches all of, and only, the scope.
|
||||
if requestedPath := utils.MakeRelativePath(sir.Ref.Path); requestedPath != "." {
|
||||
q.Conjuncts = append(q.Conjuncts, &query.TermQuery{FieldVal: "Path", Term: requestedPath})
|
||||
q.Conjuncts = append(q.Conjuncts, query.NewDisjunctionQuery([]query.Query{
|
||||
&query.TermQuery{FieldVal: "Path", Term: requestedPath},
|
||||
&query.PrefixQuery{FieldVal: "Path", Prefix: requestedPath + "/"},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package bleve
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
bleveSearch "github.com/blevesearch/bleve/v2/search"
|
||||
storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
|
||||
@@ -9,6 +11,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
|
||||
)
|
||||
|
||||
var queryEscape = regexp.MustCompile(`([` + regexp.QuoteMeta(`+=&|><!(){}[]^\"~*?:\/`) + `\-\s])`)
|
||||
|
||||
func getFieldValue[T any](m map[string]any, key string) (out T) {
|
||||
val, ok := m[key]
|
||||
if !ok {
|
||||
@@ -80,3 +84,7 @@ func hitToFacet[T any](fields map[string]any, prefix string) *T {
|
||||
func matchToResource(match *bleveSearch.DocumentMatch) *search.Resource {
|
||||
return mapping.Deserialize[search.Resource](match.Fields)
|
||||
}
|
||||
|
||||
func escapeQuery(s string) string {
|
||||
return queryEscape.ReplaceAllString(s, "\\$1")
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package bleve
|
||||
|
||||
import (
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
|
||||
)
|
||||
|
||||
var _ = Describe("searchResourcesByPath", func() {
|
||||
var idx bleve.Index
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
idx, _, err = NewIndex(GinkgoT().TempDir(), log.NopLogger())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
DeferCleanup(func() { Expect(idx.Close()).To(Succeed()) })
|
||||
})
|
||||
|
||||
ids := func(rootID, lookupPath string) []string {
|
||||
res, err := searchResourcesByPath(rootID, lookupPath, idx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
out := make([]string, 0, len(res))
|
||||
for _, r := range res {
|
||||
out = append(out, r.ID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
It("returns all of, and only, the descendants of the folder in its space", func() {
|
||||
const rootA, rootB = "s$a!root", "s$b!root"
|
||||
batch := idx.NewBatch()
|
||||
for _, r := range []search.Resource{
|
||||
{ID: "s$a!big", RootID: rootA, Path: "./big", Type: 2},
|
||||
{ID: "s$a!f1", RootID: rootA, Path: "./big/f1.txt", Type: 1},
|
||||
{ID: "s$a!f2", RootID: rootA, Path: "./big/sub/f2.txt", Type: 1},
|
||||
{ID: "s$a!big2", RootID: rootA, Path: "./big2/x.txt", Type: 1},
|
||||
{ID: "s$b!clone", RootID: rootB, Path: "./big/f1.txt", Type: 1},
|
||||
{ID: "s$a!odd", RootID: rootA, Path: `./odd name*[1]/file:with spaces?.txt`, Type: 1},
|
||||
} {
|
||||
Expect(batch.Index(r.ID, r)).To(Succeed())
|
||||
}
|
||||
Expect(idx.Batch(batch)).To(Succeed())
|
||||
|
||||
Expect(ids(rootA, "./big")).To(ConsistOf("s$a!f1", "s$a!f2"))
|
||||
Expect(ids(rootA, "./odd name*[1]")).To(ConsistOf("s$a!odd"))
|
||||
Expect(ids(rootA, ".")).To(ConsistOf("s$a!big", "s$a!f1", "s$a!f2", "s$a!big2", "s$a!odd"))
|
||||
})
|
||||
})
|
||||
@@ -1,82 +0,0 @@
|
||||
package hierarchy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
// emits every prefix up to a level: "./a/b" -> ".", "./a", "./a/b" with
|
||||
// delimiter "/", one level per byte without. tag_depth prepends "<depth>/".
|
||||
const Name = "hierarchy"
|
||||
|
||||
type Tokenizer struct {
|
||||
delimiter []byte
|
||||
tagDepth bool
|
||||
}
|
||||
|
||||
func (t *Tokenizer) Tokenize(input []byte) analysis.TokenStream {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
var out analysis.TokenStream
|
||||
emit := func(depth, end int) {
|
||||
term := input[:end]
|
||||
if t.tagDepth {
|
||||
term = strconv.AppendInt(make([]byte, 0, end+4), int64(depth), 10)
|
||||
term = append(term, '/')
|
||||
term = append(term, input[:end]...)
|
||||
}
|
||||
out = append(out, &analysis.Token{
|
||||
Term: term,
|
||||
Position: depth,
|
||||
Start: 0,
|
||||
End: end,
|
||||
Type: analysis.AlphaNumeric,
|
||||
})
|
||||
}
|
||||
|
||||
if len(t.delimiter) == 0 {
|
||||
for i := range input {
|
||||
emit(i+1, i+1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
depth := 0
|
||||
for start := 0; start <= len(input); {
|
||||
i := bytes.Index(input[start:], t.delimiter)
|
||||
if i < 0 {
|
||||
if start < len(input) {
|
||||
depth++
|
||||
emit(depth, len(input))
|
||||
}
|
||||
break
|
||||
}
|
||||
if i > 0 {
|
||||
depth++
|
||||
emit(depth, start+i)
|
||||
}
|
||||
start += i + len(t.delimiter)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func Constructor(config map[string]interface{}, _ *registry.Cache) (analysis.Tokenizer, error) {
|
||||
t := &Tokenizer{}
|
||||
if d, ok := config["delimiter"].(string); ok {
|
||||
t.delimiter = []byte(d)
|
||||
}
|
||||
if v, ok := config["tag_depth"].(bool); ok {
|
||||
t.tagDepth = v
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
if err := registry.RegisterTokenizer(Name, Constructor); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package hierarchy_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestHierarchy(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "hierarchy tokenizer")
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package hierarchy_test
|
||||
|
||||
import (
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/services/search/pkg/bleve/hierarchy"
|
||||
)
|
||||
|
||||
func terms(ts analysis.TokenStream) []string {
|
||||
out := make([]string, 0, len(ts))
|
||||
for _, t := range ts {
|
||||
out = append(out, string(t.Term))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tokenize(config map[string]any, input string) []string {
|
||||
tok, err := hierarchy.Constructor(config, registry.NewCache())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return terms(tok.Tokenize([]byte(input)))
|
||||
}
|
||||
|
||||
var _ = Describe("hierarchy tokenizer", func() {
|
||||
path := map[string]any{"delimiter": "/"}
|
||||
geohash := map[string]any{"tag_depth": true}
|
||||
|
||||
DescribeTable("emits every prefix up to a level boundary",
|
||||
func(config map[string]any, input string, want []string) {
|
||||
Expect(tokenize(config, input)).To(Equal(want))
|
||||
},
|
||||
Entry("relative path", path, "./a/b.txt", []string{".", "./a", "./a/b.txt"}),
|
||||
Entry("space root", path, ".", []string{"."}),
|
||||
Entry("trailing delimiter is not a level", path, "./a/", []string{".", "./a"}),
|
||||
Entry("delimiter only", path, "/", []string{}),
|
||||
Entry("leading delimiter", path, "/abs/x", []string{"/abs", "/abs/x"}),
|
||||
Entry("double delimiter", path, "./a//b", []string{".", "./a", "./a//b"}),
|
||||
Entry("spaces and special characters stay literal", path, "./odd name*[1]/f:x?.txt",
|
||||
[]string{".", "./odd name*[1]", "./odd name*[1]/f:x?.txt"}),
|
||||
Entry("empty input", path, "", []string{}),
|
||||
Entry("geohash, one level per byte, depth tagged", geohash, "u4pru",
|
||||
[]string{"1/u", "2/u4", "3/u4p", "4/u4pr", "5/u4pru"}),
|
||||
)
|
||||
|
||||
It("keeps byte offsets on the source value", func() {
|
||||
tok, err := hierarchy.Constructor(path, registry.NewCache())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
ts := tok.Tokenize([]byte("./a/b"))
|
||||
Expect(ts).To(HaveLen(3))
|
||||
Expect(ts[2].Start).To(Equal(0))
|
||||
Expect(ts[2].End).To(Equal(5))
|
||||
Expect(ts[2].Position).To(Equal(3))
|
||||
})
|
||||
})
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/services/search/pkg/bleve/hierarchy"
|
||||
searchmapping "github.com/opencloud-eu/opencloud/services/search/pkg/mapping"
|
||||
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
|
||||
)
|
||||
@@ -209,40 +208,6 @@ func NewMapping() (mapping.IndexMapping, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// path: every ancestor prefix is a term, so one term query matches a folder
|
||||
// and all of its descendants
|
||||
err = indexMapping.AddCustomTokenizer("path_hierarchy", map[string]any{
|
||||
"type": hierarchy.Name,
|
||||
"delimiter": "/",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = indexMapping.AddCustomAnalyzer(searchmapping.PathAnalyzer, map[string]any{
|
||||
"type": custom.Name,
|
||||
"tokenizer": "path_hierarchy",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// geohash: no field uses it yet. It is part of the v5 schema so that #3272
|
||||
// can add its geohash field additively: new fields reconcile at startup,
|
||||
// a changed analysis block does not (classifyStoredMapping), so the names
|
||||
// and the config below must not change.
|
||||
err = indexMapping.AddCustomTokenizer("geohash_hierarchy", map[string]any{
|
||||
"type": hierarchy.Name,
|
||||
"tag_depth": true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = indexMapping.AddCustomAnalyzer(searchmapping.GeohashAnalyzer, map[string]any{
|
||||
"type": custom.Name,
|
||||
"tokenizer": "geohash_hierarchy",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return indexMapping, nil
|
||||
}
|
||||
@@ -261,15 +226,11 @@ func searchResourceByID(id string, index bleve.Index) (*search.Resource, error)
|
||||
return matchToResource(res.Hits[0]), nil
|
||||
}
|
||||
|
||||
// searchResourcesByPath returns the descendants of the folder at lookupPath.
|
||||
// The folder term matches the folder and everything below it in one term
|
||||
// query (see PathAnalyzer); the folder itself is dropped from the result.
|
||||
func searchResourcesByPath(rootID string, lookupPath string, index bleve.Index) ([]*search.Resource, error) {
|
||||
rootQuery := bleve.NewTermQuery(rootID)
|
||||
rootQuery.SetField("RootID")
|
||||
pathQuery := bleve.NewTermQuery(lookupPath)
|
||||
pathQuery.SetField("Path")
|
||||
q := bleve.NewConjunctionQuery(rootQuery, pathQuery)
|
||||
q := bleve.NewConjunctionQuery(
|
||||
bleve.NewQueryStringQuery("RootID:"+rootID),
|
||||
bleve.NewQueryStringQuery("Path:"+escapeQuery(lookupPath+"/*")),
|
||||
)
|
||||
bleveReq := bleve.NewSearchRequest(q)
|
||||
bleveReq.Size = math.MaxInt
|
||||
bleveReq.Fields = []string{"*"}
|
||||
@@ -280,11 +241,7 @@ func searchResourcesByPath(rootID string, lookupPath string, index bleve.Index)
|
||||
|
||||
resources := make([]*search.Resource, 0, res.Hits.Len())
|
||||
for _, match := range res.Hits {
|
||||
resource := matchToResource(match)
|
||||
if resource.Path == lookupPath {
|
||||
continue
|
||||
}
|
||||
resources = append(resources, resource)
|
||||
resources = append(resources, matchToResource(match))
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
|
||||
+1
-19
@@ -160,7 +160,7 @@
|
||||
"fields": [
|
||||
{
|
||||
"type": "text",
|
||||
"analyzer": "path",
|
||||
"analyzer": "keyword",
|
||||
"store": true,
|
||||
"index": true,
|
||||
"include_term_vectors": true,
|
||||
@@ -1259,25 +1259,7 @@
|
||||
"type": "regexp"
|
||||
}
|
||||
},
|
||||
"tokenizers": {
|
||||
"geohash_hierarchy": {
|
||||
"tag_depth": true,
|
||||
"type": "hierarchy"
|
||||
},
|
||||
"path_hierarchy": {
|
||||
"delimiter": "/",
|
||||
"type": "hierarchy"
|
||||
}
|
||||
},
|
||||
"analyzers": {
|
||||
"geohash": {
|
||||
"tokenizer": "geohash_hierarchy",
|
||||
"type": "custom"
|
||||
},
|
||||
"path": {
|
||||
"tokenizer": "path_hierarchy",
|
||||
"type": "custom"
|
||||
},
|
||||
"words": {
|
||||
"char_filters": [
|
||||
"dot_to_space"
|
||||
|
||||
@@ -34,6 +34,10 @@ func ParseConfig(cfg *config.Config) error {
|
||||
}
|
||||
|
||||
func Validate(cfg *config.Config) error {
|
||||
if cfg.Events.Disabled && cfg.GRPC.Disabled {
|
||||
return shared.AllComponentsDisabledError(cfg.Service.Name)
|
||||
}
|
||||
|
||||
if cfg.TokenManager.JWTSecret == "" {
|
||||
return shared.MissingJWTTokenError(cfg.Service.Name)
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ func buildBleveDocMapping(t reflect.Type, overrides map[string]FieldOpts, prefix
|
||||
}
|
||||
|
||||
if fieldType == TypeKeyword || fieldType == TypePath {
|
||||
// bleve has no path tokenizer, so a path is a plain keyword here.
|
||||
base := bleveKeywordMapping(fieldType, opts)
|
||||
doc.AddFieldMappingsAt(fi.Name, base)
|
||||
if opts.caseInsensitive() {
|
||||
@@ -83,9 +84,8 @@ func buildBleveDocMapping(t reflect.Type, overrides map[string]FieldOpts, prefix
|
||||
return doc, err
|
||||
}
|
||||
|
||||
// bleveKeywordMapping is a case-preserving keyword field; path fields are
|
||||
// analyzed into their ancestor prefixes (see PathAnalyzer) and stay out of
|
||||
// _all by default.
|
||||
// bleveKeywordMapping is a case-preserving keyword field; path fields stay out
|
||||
// of _all by default.
|
||||
func bleveKeywordMapping(fieldType string, opts FieldOpts) *bleveMapping.FieldMapping {
|
||||
fm := bleve.NewKeywordFieldMapping()
|
||||
switch {
|
||||
@@ -94,9 +94,6 @@ func bleveKeywordMapping(fieldType string, opts FieldOpts) *bleveMapping.FieldMa
|
||||
case fieldType == TypePath:
|
||||
fm.IncludeInAll = false
|
||||
}
|
||||
if fieldType == TypePath {
|
||||
fm.Analyzer = PathAnalyzer
|
||||
}
|
||||
return fm
|
||||
}
|
||||
|
||||
|
||||
@@ -26,15 +26,6 @@ const WordsSuffix = "_words"
|
||||
// WordsAnalyzer names the analyzer both engines register for the words sibling.
|
||||
const WordsAnalyzer = "words"
|
||||
|
||||
// PathAnalyzer names the bleve analyzer for TypePath fields: every ancestor
|
||||
// prefix is a term, like path_hierarchy in OpenSearch.
|
||||
const PathAnalyzer = "path"
|
||||
|
||||
// GeohashAnalyzer names the bleve analyzer for a geohash: every prefix is a
|
||||
// depth-tagged term (1/u, 2/u4, ...), so a terms facet with TermPrefix
|
||||
// "<precision>/" is a geohash grid at that precision.
|
||||
const GeohashAnalyzer = "geohash"
|
||||
|
||||
// FieldOpts overrides the default type inference for a struct field. Keys in
|
||||
// the override map are json-tag names (e.g. "Name", "location", "audio.artist"),
|
||||
// not Go field names.
|
||||
|
||||
@@ -27,7 +27,7 @@ func Reconcile(index string, r SchemaReconciler, logger log.Logger) (Classificat
|
||||
case VerdictAdditive:
|
||||
persisted, err := r.ApplyAdditive()
|
||||
if persisted {
|
||||
logger.Warn().Strs("fields", classification.NewFields).Str("index", index).Msg("extended the search index mapping with new fields; documents indexed before the upgrade do not contain them and queries on these fields will miss those documents until they are re-indexed; to re-index everything run: opencloud search index --all-spaces --force-rescan --insecure")
|
||||
logger.Warn().Strs("fields", classification.NewFields).Str("index", index).Msg("extended the search index mapping with new fields; documents indexed before the upgrade do not contain them and queries on these fields will miss those documents until they are re-indexed; to re-index everything run: opencloud search index --all-spaces --force-rescan")
|
||||
}
|
||||
if err != nil {
|
||||
return classification, err
|
||||
@@ -40,5 +40,5 @@ func Reconcile(index string, r SchemaReconciler, logger log.Logger) (Classificat
|
||||
// LogNewIndexCreated logs that a fresh, empty index was created and how to
|
||||
// backfill it. The create path does not run through Reconcile.
|
||||
func LogNewIndexCreated(logger log.Logger, index string) {
|
||||
logger.Info().Str("index", index).Msg("created a new empty search index; if this OpenCloud instance already held files, they are not in it yet, index them by running: opencloud search index --all-spaces --force-rescan --insecure")
|
||||
logger.Info().Str("index", index).Msg("created a new empty search index; if this OpenCloud instance already held files, they are not in it yet, index them by running: opencloud search index --all-spaces --force-rescan")
|
||||
}
|
||||
@@ -123,13 +123,6 @@ func walk(offset int, nodes []ast.Node) (bleveQuery.Query, int, error) {
|
||||
|
||||
var q bleveQuery.Query = bleveQuery.NewQueryStringQuery(k + ":" + v)
|
||||
switch {
|
||||
case searchQuery.FieldIsPath(n.Key) && !isWildcard:
|
||||
// the folder term matches the folder itself and its descendants
|
||||
// (see PathAnalyzer); a query string would analyze the value into
|
||||
// its prefixes and match everything under the root
|
||||
tq := bleveQuery.NewTermQuery(val)
|
||||
tq.SetField(k)
|
||||
q = tq
|
||||
case n.Exact && !isWildcard:
|
||||
// = matches the whole value, on the lowercased sibling for
|
||||
// case-insensitive fields
|
||||
@@ -147,6 +140,17 @@ func walk(offset int, nodes []ast.Node) (bleveQuery.Query, int, error) {
|
||||
bq.SetMinShould(1)
|
||||
q = bq
|
||||
}
|
||||
if searchQuery.FieldIsPath(n.Key) {
|
||||
// bleve has no path hierarchy analyzer, unlike OpenSearch: match the
|
||||
// folder itself and its descendants (`\/*`). A BooleanQuery keeps
|
||||
// this atomic; a DisjunctionQuery would be redistributed by an
|
||||
// enclosing AND (mapBinary treats a left disjunction as an OR-chain).
|
||||
bq := bleve.NewBooleanQuery()
|
||||
bq.AddShould(q, bleveQuery.NewQueryStringQuery(k+":"+v+`\/*`))
|
||||
bq.SetMinShould(1)
|
||||
q = bq
|
||||
}
|
||||
|
||||
if prev == nil {
|
||||
prev = q
|
||||
} else {
|
||||
|
||||
@@ -51,17 +51,23 @@ func Test_compile(t *testing.T) {
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
// one term matches the folder itself and its descendants
|
||||
// path fields expand to match the folder itself and its descendants,
|
||||
// since bleve has no path hierarchy analyzer.
|
||||
name: `path:/Foo`,
|
||||
args: &ast.Ast{
|
||||
Nodes: []ast.Node{
|
||||
&ast.StringNode{Key: "path", Value: "/Foo"},
|
||||
},
|
||||
},
|
||||
// a BooleanQuery (should: exact OR descendants), not a DisjunctionQuery,
|
||||
// so an enclosing AND does not redistribute the folder-itself clause.
|
||||
want: func() query.Query {
|
||||
tq := query.NewTermQuery("/Foo")
|
||||
tq.SetField("Path")
|
||||
return query.NewConjunctionQuery([]query.Query{tq})
|
||||
bq := query.NewBooleanQuery(nil, []query.Query{
|
||||
query.NewQueryStringQuery(`Path:\/Foo`),
|
||||
query.NewQueryStringQuery(`Path:\/Foo\/*`),
|
||||
}, nil)
|
||||
bq.SetMinShould(1)
|
||||
return query.NewConjunctionQuery([]query.Query{bq})
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
// on a breaking mapping change: each version gets its own index (OpenSearch name
|
||||
// suffix, bleve path suffix), so the service builds a fresh index instead of
|
||||
// colliding with the old one. No migration; reindex to populate.
|
||||
const SchemaVersion = 5
|
||||
const SchemaVersion = 4
|
||||
|
||||
var scopeRegex = regexp.MustCompile(`scope:\s*([^" "\n\r]*)`)
|
||||
|
||||
|
||||
@@ -35,7 +35,12 @@ Share behavior can be configured via environment variables:
|
||||
- Auto-acceptance of shares
|
||||
- Share permissions and restrictions
|
||||
|
||||
See the `frontend` service README for more details on share-related configuration options.
|
||||
### Auto-Accept Shares
|
||||
|
||||
When setting the `SHARING_AUTO_ACCEPT_SHARES` to `true` (sharing service), all
|
||||
incoming shares will be accepted automatically. Users can overwrite this
|
||||
setting individually in their profile. The deprecated
|
||||
`FRONTEND_AUTO_ACCEPT_SHARES` is still supported for backwards compatibility.
|
||||
|
||||
## Scalability
|
||||
|
||||
|
||||
@@ -39,25 +39,16 @@ class WaitHelper {
|
||||
/**
|
||||
* Repeat $makeAttempt until $shouldStop returns true or the timeout elapses.
|
||||
*
|
||||
* @param callable $makeAttempt makes one attempt (e.g. sends a request) and returns its result
|
||||
* @param callable $shouldStop receives that result, returns true to stop polling
|
||||
* @param int|null $intervalMs pause between attempts in ms; defaults to self::INTERVAL_MS
|
||||
* @param int|null $timeoutSeconds overall time to keep polling; defaults to self::TIMEOUT_SECONDS
|
||||
* @param callable $makeAttempt makes one attempt (e.g. sends a request) and returns its result
|
||||
* @param callable $shouldStop receives that result, returns true to stop polling
|
||||
*
|
||||
* @return mixed the last result from $makeAttempt
|
||||
*/
|
||||
public static function waitUntil(
|
||||
callable $makeAttempt,
|
||||
callable $shouldStop,
|
||||
?int $intervalMs = null,
|
||||
?int $timeoutSeconds = null
|
||||
): mixed {
|
||||
$intervalMs ??= self::INTERVAL_MS;
|
||||
$timeoutSeconds ??= self::TIMEOUT_SECONDS;
|
||||
$deadline = \microtime(true) + $timeoutSeconds;
|
||||
public static function waitUntil(callable $makeAttempt, callable $shouldStop): mixed {
|
||||
$deadline = \microtime(true) + self::TIMEOUT_SECONDS;
|
||||
$result = $makeAttempt();
|
||||
while (!$shouldStop($result) && \microtime(true) < $deadline) {
|
||||
\usleep($intervalMs * 1000);
|
||||
\usleep(self::INTERVAL_MS * 1000);
|
||||
$result = $makeAttempt();
|
||||
}
|
||||
return $result;
|
||||
|
||||
@@ -150,7 +150,7 @@ class SearchContext implements Context {
|
||||
): void {
|
||||
// NOTE: because indexing of newly uploaded files or directories with OpenCloud is decoupled and occurs asynchronously
|
||||
// short wait is necessary before searching
|
||||
sleep(2);
|
||||
sleep(10);
|
||||
// remember the query so "should eventually contain" steps can re-search
|
||||
$this->lastSearchQuery = [
|
||||
"user" => $user,
|
||||
@@ -176,25 +176,6 @@ class SearchContext implements Context {
|
||||
string $path,
|
||||
string $user,
|
||||
TableNode $properties
|
||||
): void {
|
||||
$assert = fn () => $this->assertFileOrFolderInSearchResultContainsProperties($path, $user, $properties);
|
||||
$this->retrySearchUntilSatisfied($assert);
|
||||
$assert();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $user
|
||||
* @param TableNode $properties
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function assertFileOrFolderInSearchResultContainsProperties(
|
||||
string $path,
|
||||
string $user,
|
||||
TableNode $properties
|
||||
): void {
|
||||
$user = $this->featureContext->getActualUsername($user);
|
||||
$this->featureContext->verifyTableNodeColumns($properties, ['name', 'value']);
|
||||
@@ -253,7 +234,8 @@ class SearchContext implements Context {
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function assertSearchResultContainsEntriesWithHighlight(
|
||||
#[Then('/^the search result should contain these (?:files|entries) with highlight on keyword "([^"]*)"/')]
|
||||
public function theSearchResultShouldContainEntriesWithHighlight(
|
||||
TableNode $expectedFiles,
|
||||
string $expectedContent
|
||||
): void {
|
||||
@@ -302,15 +284,8 @@ class SearchContext implements Context {
|
||||
): void {
|
||||
// NOTE: since indexing of newly uploaded files or directories with OpenCloud is decoupled and occurs asynchronously,
|
||||
// a short wait is necessary before searching
|
||||
sleep(2);
|
||||
$this->lastSearchQuery = [
|
||||
"user" => $user,
|
||||
"pattern" => $pattern,
|
||||
"scopeType" => $scopeType,
|
||||
"scope" => $scope,
|
||||
"spaceName" => $spaceName,
|
||||
];
|
||||
$response = $this->searchFiles($user, $pattern, null, $scopeType, $scope, $spaceName);
|
||||
sleep(5);
|
||||
$response = $this-> searchFiles($user, $pattern, null, $scopeType, $scope, $spaceName);
|
||||
$this->featureContext->setResponse($response);
|
||||
}
|
||||
|
||||
@@ -325,7 +300,7 @@ class SearchContext implements Context {
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function retrySearchUntilSatisfied(callable $assert): void {
|
||||
private function retrySearchUntilSatisfied(callable $assert): void {
|
||||
Assert::assertNotEmpty(
|
||||
$this->lastSearchQuery,
|
||||
'No search to retry. Use a "searches for ... using the WebDAV API" step first.'
|
||||
@@ -335,67 +310,33 @@ class SearchContext implements Context {
|
||||
fn () => $this->searchFiles(
|
||||
$query["user"],
|
||||
$query["pattern"],
|
||||
$query["limit"] ?? null,
|
||||
$query["scopeType"] ?? null,
|
||||
$query["scope"] ?? null,
|
||||
$query["spaceName"] ?? null,
|
||||
$query["properties"] ?? null
|
||||
$query["limit"],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$query["properties"]
|
||||
),
|
||||
function ($response) use ($assert) {
|
||||
$this->featureContext->setResponse($response);
|
||||
try {
|
||||
$assert();
|
||||
return true;
|
||||
} catch (\Throwable) {
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
2000,
|
||||
20
|
||||
}
|
||||
);
|
||||
$this->featureContext->setResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param int $numFiles
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[Then('the search result should contain :numFiles files/entries')]
|
||||
public function theSearchResultShouldContainNumEntries(int $numFiles): void {
|
||||
$assert = fn () => $this->featureContext->checkIFResponseContainsNumberEntries($numFiles);
|
||||
$this->retrySearchUntilSatisfied($assert);
|
||||
$assert();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $user
|
||||
* @param int $expectedNumber
|
||||
* @param TableNode $expectedFiles
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[Then('the search result of user :user should contain any :expectedNumber of these files/entries:')]
|
||||
public function theSearchResultShouldContainAnyOfTheseEntries(
|
||||
string $user,
|
||||
int $expectedNumber,
|
||||
TableNode $expectedFiles
|
||||
): void {
|
||||
$assert = fn () => $this->featureContext->checkIfSearchResultContainsFiles($user, $expectedNumber, $expectedFiles);
|
||||
$this->retrySearchUntilSatisfied($assert);
|
||||
$assert();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user
|
||||
* @param TableNode $expectedFiles
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[Then('/^the search result of user "([^"]*)" should contain only these (?:files|entries):$/')]
|
||||
public function theSearchResultShouldContainOnlyEntries(string $user, TableNode $expectedFiles): void {
|
||||
#[Then('/^the search result of user "([^"]*)" should eventually contain only these (?:files|entries):$/')]
|
||||
public function theSearchResultShouldEventuallyContainOnlyEntries(string $user, TableNode $expectedFiles): void {
|
||||
$assert = fn () => $this->featureContext->thePropfindResultShouldContainOnlyEntries($user, $expectedFiles);
|
||||
$this->retrySearchUntilSatisfied($assert);
|
||||
$assert();
|
||||
@@ -407,31 +348,9 @@ class SearchContext implements Context {
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[Then('/^the search result of user "([^"]*)" should contain these (?:files|entries):$/')]
|
||||
public function theSearchResultShouldContainEntries(string $user, TableNode $expectedFiles): void {
|
||||
$this->assertSearchResultContainsEntries($user, "", $expectedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user
|
||||
* @param TableNode $expectedFiles
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[Then('/^the search result of user "([^"]*)" should not contain these (?:files|entries):$/')]
|
||||
public function theSearchResultShouldNotContainEntries(string $user, TableNode $expectedFiles): void {
|
||||
$this->assertSearchResultContainsEntries($user, "not", $expectedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user
|
||||
* @param string $shouldOrNot (not|)
|
||||
* @param TableNode $expectedFiles
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function assertSearchResultContainsEntries(string $user, string $shouldOrNot, TableNode $expectedFiles): void {
|
||||
$assert = fn () => $this->featureContext->thePropfindResultShouldContainEntries($user, $shouldOrNot, $expectedFiles);
|
||||
#[Then('/^the search result of user "([^"]*)" should eventually contain these (?:files|entries):$/')]
|
||||
public function theSearchResultShouldEventuallyContainEntries(string $user, TableNode $expectedFiles): void {
|
||||
$assert = fn () => $this->featureContext->thePropfindResultShouldContainEntries($user, '', $expectedFiles);
|
||||
$this->retrySearchUntilSatisfied($assert);
|
||||
$assert();
|
||||
}
|
||||
@@ -442,12 +361,9 @@ class SearchContext implements Context {
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[Then('/^the search result should contain these (?:files|entries) with highlight on keyword "([^"]*)"$/')]
|
||||
public function theSearchResultShouldContainEntriesWithHighlight(
|
||||
TableNode $expectedFiles,
|
||||
string $expectedContent
|
||||
): void {
|
||||
$assert = fn () => $this->assertSearchResultContainsEntriesWithHighlight($expectedFiles, $expectedContent);
|
||||
#[Then('/^the search result should eventually contain these (?:files|entries) with highlight on keyword "([^"]*)"$/')]
|
||||
public function theSearchResultShouldEventuallyContainEntriesWithHighlight(TableNode $expectedFiles, string $expectedContent): void {
|
||||
$assert = fn () => $this->theSearchResultShouldContainEntriesWithHighlight($expectedFiles, $expectedContent);
|
||||
$this->retrySearchUntilSatisfied($assert);
|
||||
$assert();
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ class SpacesContext implements Context {
|
||||
private ChecksumContext $checksumContext;
|
||||
private FilesVersionsContext $filesVersionsContext;
|
||||
private ArchiverContext $archiverContext;
|
||||
private SearchContext $searchContext;
|
||||
|
||||
/**
|
||||
* key is space name and value is the username that created the space
|
||||
@@ -513,7 +512,6 @@ class SpacesContext implements Context {
|
||||
$this->checksumContext = BehatHelper::getContext($scope, $environment, 'ChecksumContext');
|
||||
$this->filesVersionsContext = BehatHelper::getContext($scope, $environment, 'FilesVersionsContext');
|
||||
$this->archiverContext = BehatHelper::getContext($scope, $environment, 'ArchiverContext');
|
||||
$this->searchContext = BehatHelper::getContext($scope, $environment, 'SearchContext');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3927,47 +3925,41 @@ class SpacesContext implements Context {
|
||||
*/
|
||||
#[Then('for user :user the search result should contain space :spaceName')]
|
||||
public function searchResultShouldContainSpace(string $user, string $spaceName): void {
|
||||
// searching a space by name goes through the asynchronous file index, so
|
||||
// re-run the search until the space shows up (or the timeout elapses).
|
||||
$assert = function () use ($user, $spaceName): void {
|
||||
$responseArray = json_decode(
|
||||
json_encode(
|
||||
HttpRequestHelper::getResponseXml($this->featureContext->getResponse())->xpath("//d:response/d:href")
|
||||
),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
Assert::assertNotEmpty($responseArray, "search result is empty");
|
||||
$responseArray = json_decode(
|
||||
json_encode(
|
||||
HttpRequestHelper::getResponseXml($this->featureContext->getResponse())->xpath("//d:response/d:href")
|
||||
),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
Assert::assertNotEmpty($responseArray, "search result is empty");
|
||||
|
||||
// for mountpoint, id looks a little different than for project space
|
||||
if (str_contains($spaceName, 'mountpoint')) {
|
||||
$splitSpaceName = explode("/", $spaceName);
|
||||
$space = $this->getSpaceByName($user, $splitSpaceName[1]);
|
||||
$splitSpaceId = explode("$", $space['id']);
|
||||
$spaceId = str_replace('!', '%21', $splitSpaceId[1]);
|
||||
} else {
|
||||
$space = $this->getSpaceByName($user, $spaceName);
|
||||
$spaceId = $space['id'];
|
||||
}
|
||||
$suffixPath = $user;
|
||||
$davPathVersion = $this->featureContext->getDavPathVersion();
|
||||
if ($davPathVersion === WebDavHelper::DAV_VERSION_SPACES) {
|
||||
$suffixPath = $spaceId;
|
||||
}
|
||||
// for mountpoint, id looks a little different than for project space
|
||||
if (str_contains($spaceName, 'mountpoint')) {
|
||||
$splitSpaceName = explode("/", $spaceName);
|
||||
$space = $this->getSpaceByName($user, $splitSpaceName[1]);
|
||||
$splitSpaceId = explode("$", $space['id']);
|
||||
$spaceId = str_replace('!', '%21', $splitSpaceId[1]);
|
||||
} else {
|
||||
$space = $this->getSpaceByName($user, $spaceName);
|
||||
$spaceId = $space['id'];
|
||||
}
|
||||
$suffixPath = $user;
|
||||
$davPathVersion = $this->featureContext->getDavPathVersion();
|
||||
if ($davPathVersion === WebDavHelper::DAV_VERSION_SPACES) {
|
||||
$suffixPath = $spaceId;
|
||||
}
|
||||
|
||||
$topWebDavPath = "/" . WebDavHelper::getDavPath($davPathVersion, $suffixPath);
|
||||
$topWebDavPath = "/" . WebDavHelper::getDavPath($davPathVersion, $suffixPath);
|
||||
|
||||
$spaceFound = false;
|
||||
foreach ($responseArray as $value) {
|
||||
if ($topWebDavPath === $value[0]) {
|
||||
$spaceFound = true;
|
||||
}
|
||||
$spaceFound = false;
|
||||
foreach ($responseArray as $value) {
|
||||
if ($topWebDavPath === $value[0]) {
|
||||
$spaceFound = true;
|
||||
}
|
||||
Assert::assertTrue($spaceFound, "response does not contain the space '$spaceName'");
|
||||
};
|
||||
$this->searchContext->retrySearchUntilSatisfied($assert);
|
||||
$assert();
|
||||
}
|
||||
Assert::assertTrue($spaceFound, "response does not contain the space '$spaceName'");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,6 @@ use Behat\Gherkin\Node\TableNode;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TestHelpers\GraphHelper;
|
||||
use TestHelpers\WaitHelper;
|
||||
use TestHelpers\BehatHelper;
|
||||
use Behat\Step\Given;
|
||||
use Behat\Step\Then;
|
||||
@@ -39,7 +38,6 @@ require_once 'bootstrap.php';
|
||||
class TagContext implements Context {
|
||||
private FeatureContext $featureContext;
|
||||
private SpacesContext $spacesContext;
|
||||
private array $lastTagsQuery = [];
|
||||
|
||||
/**
|
||||
* This will run before EVERY scenario.
|
||||
@@ -177,79 +175,19 @@ class TagContext implements Context {
|
||||
*/
|
||||
#[When('user :user lists all available tag(s) via the Graph API')]
|
||||
public function theUserGetsAllAvailableTags(string $user): void {
|
||||
// Note: after creating or deleting tags, in some cases tags do not appear or disappear immediately
|
||||
sleep(2);
|
||||
$this->lastTagsQuery = ["user" => $user];
|
||||
$this->featureContext->setResponse($this->fetchTags($user));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws Exception
|
||||
*/
|
||||
private function fetchTags(string $user): ResponseInterface {
|
||||
return GraphHelper::getTags(
|
||||
$this->featureContext->getBaseUrl(),
|
||||
$user,
|
||||
$this->featureContext->getPasswordForUser($user),
|
||||
$this->featureContext->getStepLineRef()
|
||||
// Note: after creating or deleting tags, in some cases tags do not appear or disappear immediately,
|
||||
// So wait is necessary before listing tags
|
||||
sleep(5);
|
||||
$this->featureContext->setResponse(
|
||||
GraphHelper::getTags(
|
||||
$this->featureContext->getBaseUrl(),
|
||||
$user,
|
||||
$this->featureContext->getPasswordForUser($user),
|
||||
$this->featureContext->getStepLineRef()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* re-run the last tag listing until the assertion passes or the WaitHelper
|
||||
*
|
||||
* @param callable $assert
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function retryTagsUntilSatisfied(callable $assert): void {
|
||||
Assert::assertNotEmpty(
|
||||
$this->lastTagsQuery,
|
||||
'No tag listing to retry. Use a "lists all available tags via the Graph API" step first.'
|
||||
);
|
||||
$query = $this->lastTagsQuery;
|
||||
$response = WaitHelper::waitUntil(
|
||||
fn () => $this->fetchTags($query["user"]),
|
||||
function ($response) use ($assert) {
|
||||
$this->featureContext->setResponse($response);
|
||||
try {
|
||||
$assert();
|
||||
return true;
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
$this->featureContext->setResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param TableNode $table
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Then('/^the response should contain following tags:$/')]
|
||||
public function theResponseShouldContainFollowingTags(TableNode $table): void {
|
||||
$this->assertResponseContainsFollowingTags("", $table);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param TableNode $table
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Then('/^the response should not contain following tags:$/')]
|
||||
public function theResponseShouldNotContainFollowingTags(TableNode $table): void {
|
||||
$this->assertResponseContainsFollowingTags("not", $table);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $shouldOrNot (not|)
|
||||
@@ -258,29 +196,27 @@ class TagContext implements Context {
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function assertResponseContainsFollowingTags(string $shouldOrNot, TableNode $table): void {
|
||||
$assert = function () use ($shouldOrNot, $table): void {
|
||||
#[Then('/^the response should (not|)\\s?contain following tag(s):$/')]
|
||||
public function theFollowingTagsShouldExistForUser(string $shouldOrNot, TableNode $table): void {
|
||||
$rows = $table->getRows();
|
||||
foreach ($rows as $row) {
|
||||
$responseArray = $this->featureContext->getJsonDecodedResponse(
|
||||
$this->featureContext->getResponse()
|
||||
)['value'];
|
||||
foreach ($table->getRows() as $row) {
|
||||
if ($shouldOrNot === "not") {
|
||||
Assert::assertFalse(
|
||||
\in_array($row[0], $responseArray),
|
||||
"the response should not contain the tag $row[0].\nResponse\n"
|
||||
. print_r($responseArray, true)
|
||||
);
|
||||
} else {
|
||||
Assert::assertTrue(
|
||||
\in_array($row[0], $responseArray),
|
||||
"the response does not contain the tag $row[0].\nResponse\n"
|
||||
. print_r($responseArray, true)
|
||||
);
|
||||
}
|
||||
if ($shouldOrNot === "not") {
|
||||
Assert::assertFalse(
|
||||
\in_array($row[0], $responseArray),
|
||||
"the response should not contain the tag $row[0].\nResponse\n"
|
||||
. print_r($responseArray, true)
|
||||
);
|
||||
} else {
|
||||
Assert::assertTrue(
|
||||
\in_array($row[0], $responseArray),
|
||||
"the response does not contain the tag $row[0].\nResponse\n"
|
||||
. print_r($responseArray, true)
|
||||
);
|
||||
}
|
||||
};
|
||||
$this->retryTagsUntilSatisfied($assert);
|
||||
$assert();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4014,7 +4014,7 @@ trait WebDav {
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Then('/^the propfind result of user "([^"]*)" should (not|)\\s?contain these (?:files|entries):$/')]
|
||||
#[Then('/^the (?:propfind|search) result of user "([^"]*)" should (not|)\\s?contain these (?:files|entries):$/')]
|
||||
public function thePropfindResultShouldContainEntries(
|
||||
string $user,
|
||||
string $shouldOrNot,
|
||||
@@ -4036,7 +4036,7 @@ trait WebDav {
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Then('/^the propfind result of user "([^"]*)" should contain only these (?:files|entries):$/')]
|
||||
#[Then('/^the (?:propfind|search) result of user "([^"]*)" should contain only these (?:files|entries):$/')]
|
||||
public function thePropfindResultShouldContainOnlyEntries(
|
||||
string $user,
|
||||
TableNode $expectedFiles
|
||||
@@ -4063,7 +4063,7 @@ trait WebDav {
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[Then('the propfind result should contain :numFiles files/entries')]
|
||||
#[Then('the propfind/search result should contain :numFiles files/entries')]
|
||||
public function propfindResultShouldContainNumEntries(int $numFiles): void {
|
||||
$this->checkIFResponseContainsNumberEntries($numFiles);
|
||||
}
|
||||
@@ -4110,7 +4110,7 @@ trait WebDav {
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Then('the propfind result of user :user should contain any :expectedNumber of these files/entries:')]
|
||||
#[Then('the propfind/search result of user :user should contain any :expectedNumber of these files/entries:')]
|
||||
public function theSearchResultOfUserShouldContainAnyOfTheseEntries(
|
||||
string $user,
|
||||
int $expectedNumber,
|
||||
|
||||
@@ -186,6 +186,15 @@
|
||||
|
||||
- [apiServiceAvailability/serviceAvailabilityCheck.feature:123](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiServiceAvailability/serviceAvailabilityCheck.feature#L123)
|
||||
|
||||
#### [Missing properties in REPORT response](https://github.com/owncloud/ocis/issues/9780), [d:getetag property has empty value in REPORT response](https://github.com/owncloud/ocis/issues/9783)
|
||||
|
||||
- [apiSearch1/search.feature:437](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L437)
|
||||
- [apiSearch1/search.feature:438](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L438)
|
||||
- [apiSearch1/search.feature:439](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L439)
|
||||
- [apiSearch1/search.feature:465](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L465)
|
||||
- [apiSearch1/search.feature:466](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L466)
|
||||
- [apiSearch1/search.feature:467](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L467)
|
||||
|
||||
## Scenarios from core API tests that are expected to fail with decomposed storage
|
||||
|
||||
### File
|
||||
|
||||
@@ -186,6 +186,15 @@
|
||||
|
||||
- [apiServiceAvailability/serviceAvailabilityCheck.feature:123](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiServiceAvailability/serviceAvailabilityCheck.feature#L123)
|
||||
|
||||
#### [Missing properties in REPORT response](https://github.com/owncloud/ocis/issues/9780), [d:getetag property has empty value in REPORT response](https://github.com/owncloud/ocis/issues/9783)
|
||||
|
||||
- [apiSearch1/search.feature:437](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L437)
|
||||
- [apiSearch1/search.feature:438](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L438)
|
||||
- [apiSearch1/search.feature:439](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L439)
|
||||
- [apiSearch1/search.feature:465](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L465)
|
||||
- [apiSearch1/search.feature:466](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L466)
|
||||
- [apiSearch1/search.feature:467](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/search.feature#L467)
|
||||
|
||||
## Scenarios from core API tests that are expected to fail with posix storage
|
||||
|
||||
### File
|
||||
|
||||
@@ -409,7 +409,7 @@ Feature: Search
|
||||
| new |
|
||||
| spaces |
|
||||
|
||||
@skip @issue-3501 @issue-9780
|
||||
@issue-4712 @issue-9780 @issue-9781 @issue-9783 @issue-10329
|
||||
Scenario Outline: report extra properties in search entries for a file
|
||||
Given using <dav-path-version> DAV path
|
||||
When user "Alice" searches for "*insideTheFo*" using the WebDAV API requesting these properties:
|
||||
@@ -438,7 +438,7 @@ Feature: Search
|
||||
| new |
|
||||
| spaces |
|
||||
|
||||
@skip @issue-3501 @issue-9780
|
||||
@issue-4712 @issue-9780 @issue-9781 @issue-9783 @issue-10329
|
||||
Scenario Outline: report extra properties in search entries for a folder
|
||||
Given using <dav-path-version> DAV path
|
||||
When user "Alice" searches for "*folderMain*" using the WebDAV API requesting these properties:
|
||||
|
||||
@@ -16,7 +16,7 @@ Feature: content search
|
||||
And user "Alice" has uploaded file with content "namaste from nepal" to "hello.txt"
|
||||
When user "Alice" searches for "Content:hello" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| keywordAtStart.txt |
|
||||
| keywordAtMiddle.txt |
|
||||
| keywordAtLast.txt |
|
||||
@@ -34,15 +34,15 @@ Feature: content search
|
||||
And user "Alice" has uploaded file with content "alan@example.org want to say hello" to "findByEmail.docs"
|
||||
When user "Alice" searches for "Content:k6" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| wordWithNumber.md |
|
||||
When user "Alice" searches for "Content:https://opencloud.eu/" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| findByWebSite.txt |
|
||||
When user "Alice" searches for "Content:alan@" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| findByEmail.docs |
|
||||
Examples:
|
||||
| dav-path-version |
|
||||
@@ -71,11 +71,11 @@ Feature: content search
|
||||
And user "Alice" has uploaded file with content "He has expirience, we must to have, I have to find ...." to "fileWithStopWords.txt"
|
||||
When user "Alice" searches for 'Content:"he has"' using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| fileWithStopWords.txt |
|
||||
When user "Alice" searches for 'Content:"I have"' using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| fileWithStopWords.txt |
|
||||
Examples:
|
||||
| dav-path-version |
|
||||
@@ -101,7 +101,7 @@ Feature: content search
|
||||
And user "Brian" has a share "uploadFolder" synced
|
||||
When user "Brian" searches for "Content:hello" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Brian" should contain only these files:
|
||||
And the search result of user "Brian" should eventually contain only these files:
|
||||
| keywordAtStart.txt |
|
||||
| keywordAtMiddle.txt |
|
||||
| keywordAtLast.txt |
|
||||
@@ -121,7 +121,7 @@ Feature: content search
|
||||
And user "Alice" has deleted file "keywordAtLast.txt"
|
||||
When user "Alice" searches for "Content:hello" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| keywordAtStart.txt |
|
||||
| keywordAtMiddle.txt |
|
||||
Examples:
|
||||
@@ -139,7 +139,7 @@ Feature: content search
|
||||
And user "Alice" has restored the file with original path "keywordAtStart.txt"
|
||||
When user "Alice" searches for "Content:hello" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| keywordAtStart.txt |
|
||||
Examples:
|
||||
| dav-path-version |
|
||||
@@ -154,7 +154,7 @@ Feature: content search
|
||||
And user "Alice" has restored version index "1" of file "test.txt"
|
||||
When user "Alice" searches for "Content:hello" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| test.txt |
|
||||
Examples:
|
||||
| dav-path-version |
|
||||
@@ -175,7 +175,7 @@ Feature: content search
|
||||
And using <dav-path-version> DAV path
|
||||
When user "Alice" searches for "Content:hello" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| keywordAtStart.txt |
|
||||
| keywordAtMiddle.txt |
|
||||
| keywordAtLast.txt |
|
||||
@@ -204,7 +204,7 @@ Feature: content search
|
||||
And using <dav-path-version> DAV path
|
||||
When user "Brian" searches for "Content:hello" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain only these files:
|
||||
And the search result of user "Alice" should eventually contain only these files:
|
||||
| keywordAtStart.txt |
|
||||
| keywordAtMiddle.txt |
|
||||
| keywordAtLast.txt |
|
||||
@@ -224,7 +224,7 @@ Feature: content search
|
||||
| technical task.txt | test |
|
||||
When user "Alice" searches for '<pattern>' using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result of user "Alice" should contain these entries:
|
||||
And the search result of user "Alice" should eventually contain these entries:
|
||||
| <search-result-1> |
|
||||
| <search-result-2> |
|
||||
And the search result should contain "<result-count>" entries
|
||||
@@ -251,7 +251,7 @@ Feature: content search
|
||||
And user "Alice" has uploaded a file inside space "project-space" with content "this is a simple odt file" to "test-odt-file.odt"
|
||||
When user "Alice" searches for "Content:simple" using the WebDAV API
|
||||
Then the HTTP status code should be "207"
|
||||
And the search result should contain these entries with highlight on keyword "simple"
|
||||
And the search result should eventually contain these entries with highlight on keyword "simple"
|
||||
| test-text-file.txt |
|
||||
| test-pdf-file.pdf |
|
||||
| test-cpp-file.cpp |
|
||||
|
||||
Reference in new issue
Block a user