Compare commits

..

1 Commits

Author SHA1 Message Date
Viktor Scharf
d073551767 ApiTest: set user profile photo 2025-05-28 16:18:10 +02:00
793 changed files with 40261 additions and 84203 deletions

View File

@@ -1,3 +1,3 @@
# The test runner source for UI tests
WEB_COMMITID=9d5615635b05b177946649962d61df78c8d9fc07
WEB_COMMITID=8c10b33af6ec3a949f95a57f197ee915d666f343
WEB_BRANCH=main

View File

@@ -1,90 +1,5 @@
# Changelog
## [3.0.0](https://github.com/opencloud-eu/opencloud/releases/tag/v3.0.0) - 2025-06-10
### ❤️ Thanks to all contributors! ❤️
@AlexAndBear, @ScharfViktor, @VuiMuich, @aduffeck, @butonic, @fschade, @kulmann, @micbar, @prashant-gurung899, @rhafer
### 💥 Breaking changes
- do not automatically expand drive root permissions [[#495](https://github.com/opencloud-eu/opencloud/pull/495)]
### ✨ Features
- Enhancement: Introduced support for PrivateLink in WebDAV search responses [[#983](https://github.com/opencloud-eu/opencloud/pull/983)]
- Add profile photo [[#864](https://github.com/opencloud-eu/opencloud/pull/864)]
- feat: hide close button in collabora [[#828](https://github.com/opencloud-eu/opencloud/pull/828)]
### 📈 Enhancement
- graph: Add $filter to only list (and/or count) member permissions [[#996](https://github.com/opencloud-eu/opencloud/pull/996)]
- [full-ci] chore: bump web to v3.0.0 [[#1026](https://github.com/opencloud-eu/opencloud/pull/1026)]
- [full-ci] chore: bump web to v3.0.0-alpha.1 [[#972](https://github.com/opencloud-eu/opencloud/pull/972)]
- feat: add shareType to sharees field on activities api [[#954](https://github.com/opencloud-eu/opencloud/pull/954)]
- graph: Add more $select options to ListPermissions endpoint [[#916](https://github.com/opencloud-eu/opencloud/pull/916)]
- feat: add webp format [[#869](https://github.com/opencloud-eu/opencloud/pull/869)]
### ✅ Tests
- apiTest. count permission in the list permissions endpoint [[#1010](https://github.com/opencloud-eu/opencloud/pull/1010)]
- apiTest. select option for root/permissions endpoint [[#942](https://github.com/opencloud-eu/opencloud/pull/942)]
- [full-ci] ApiTest. checking private link in report response [[#993](https://github.com/opencloud-eu/opencloud/pull/993)]
- [full-ci] Change `eicar_com.zip` virus file and update tests [[#992](https://github.com/opencloud-eu/opencloud/pull/992)]
### 🐛 Bug Fixes
- Fix broken urls in README.md of deployment example [[#1023](https://github.com/opencloud-eu/opencloud/pull/1023)]
- Make activitylog service scalable [[#941](https://github.com/opencloud-eu/opencloud/pull/941)]
- Fix purging revisions from decomposeds3 blobstores [[#958](https://github.com/opencloud-eu/opencloud/pull/958)]
- fix(graph-metadata): lazy cs3 metadata storage initialization [[#946](https://github.com/opencloud-eu/opencloud/pull/946)]
- always get the user email for admin user [[#898](https://github.com/opencloud-eu/opencloud/pull/898)]
### 📚 Documentation
- Updated boxes in readme [[#970](https://github.com/opencloud-eu/opencloud/pull/970)]
### 📦️ Dependencies
- [decomposed] bump-version-v3.0.0 [[#1030](https://github.com/opencloud-eu/opencloud/pull/1030)]
- [full-ci] chore:reva bump v.2.33.1 [[#1027](https://github.com/opencloud-eu/opencloud/pull/1027)]
- build(deps): bump i18next from 25.1.2 to 25.2.1 in /services/idp [[#1024](https://github.com/opencloud-eu/opencloud/pull/1024)]
- build(deps): bump golang.org/x/image from 0.27.0 to 0.28.0 [[#1012](https://github.com/opencloud-eu/opencloud/pull/1012)]
- build(deps): bump @types/node from 22.15.29 to 22.15.30 in /services/idp [[#1008](https://github.com/opencloud-eu/opencloud/pull/1008)]
- build(deps): bump github.com/open-policy-agent/opa from 1.5.0 to 1.5.1 [[#1000](https://github.com/opencloud-eu/opencloud/pull/1000)]
- build(deps): bump golang.org/x/sync from 0.14.0 to 0.15.0 [[#1006](https://github.com/opencloud-eu/opencloud/pull/1006)]
- build(deps-dev): bump eslint-plugin-react from 7.37.2 to 7.37.5 in /services/idp [[#1004](https://github.com/opencloud-eu/opencloud/pull/1004)]
- build(deps-dev): bump postcss-normalize from 13.0.0 to 13.0.1 in /services/idp [[#1003](https://github.com/opencloud-eu/opencloud/pull/1003)]
- build(deps): bump @testing-library/react from 11.2.7 to 12.1.5 in /services/idp [[#994](https://github.com/opencloud-eu/opencloud/pull/994)]
- build(deps): bump github.com/blevesearch/bleve/v2 from 2.5.1 to 2.5.2 [[#999](https://github.com/opencloud-eu/opencloud/pull/999)]
- build(deps): bump @fontsource/roboto from 5.1.0 to 5.2.5 in /services/idp [[#995](https://github.com/opencloud-eu/opencloud/pull/995)]
- build(deps): bump google.golang.org/grpc from 1.72.1 to 1.72.2 [[#991](https://github.com/opencloud-eu/opencloud/pull/991)]
- build(deps): bump github.com/nats-io/nats.go from 1.42.0 to 1.43.0 [[#990](https://github.com/opencloud-eu/opencloud/pull/990)]
- build(deps): bump @types/jest from 29.5.12 to 29.5.14 in /services/idp [[#987](https://github.com/opencloud-eu/opencloud/pull/987)]
- build(deps): bump github.com/leonelquinteros/gotext from 1.7.1 to 1.7.2 [[#981](https://github.com/opencloud-eu/opencloud/pull/981)]
- build(deps): bump @types/node from 22.15.19 to 22.15.29 in /services/idp [[#980](https://github.com/opencloud-eu/opencloud/pull/980)]
- build(deps): bump github.com/opencloud-eu/libre-graph-api-go from 1.0.6 to 1.0.7 [[#982](https://github.com/opencloud-eu/opencloud/pull/982)]
- build(deps-dev): bump sass-loader from 16.0.4 to 16.0.5 in /services/idp [[#979](https://github.com/opencloud-eu/opencloud/pull/979)]
- build(deps): bump web-vitals from 4.2.4 to 5.0.2 in /services/idp [[#978](https://github.com/opencloud-eu/opencloud/pull/978)]
- build(deps): bump github.com/open-policy-agent/opa from 1.4.2 to 1.5.0 [[#977](https://github.com/opencloud-eu/opencloud/pull/977)]
- build(deps-dev): bump cldr from 7.5.0 to 7.9.0 in /services/idp [[#975](https://github.com/opencloud-eu/opencloud/pull/975)]
- build(deps): bump github.com/olekukonko/tablewriter from 1.0.6 to 1.0.7 [[#974](https://github.com/opencloud-eu/opencloud/pull/974)]
- build(deps): bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc from 0.60.0 to 0.61.0 [[#915](https://github.com/opencloud-eu/opencloud/pull/915)]
- build(deps): bump go.opentelemetry.io/contrib/zpages from 0.60.0 to 0.61.0 [[#938](https://github.com/opencloud-eu/opencloud/pull/938)]
- build(deps): bump @testing-library/user-event from 14.5.2 to 14.6.1 in /services/idp [[#939](https://github.com/opencloud-eu/opencloud/pull/939)]
- build(deps): bump i18next-browser-languagedetector from 7.2.1 to 8.1.0 in /services/idp [[#937](https://github.com/opencloud-eu/opencloud/pull/937)]
- build(deps): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp from 0.60.0 to 0.61.0 [[#923](https://github.com/opencloud-eu/opencloud/pull/923)]
- build(deps): bump github.com/nats-io/nats-server/v2 from 2.11.3 to 2.11.4 [[#914](https://github.com/opencloud-eu/opencloud/pull/914)]
- build(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc from 1.35.0 to 1.36.0 [[#907](https://github.com/opencloud-eu/opencloud/pull/907)]
- build(deps): bump go.opentelemetry.io/otel/trace from 1.35.0 to 1.36.0 [[#906](https://github.com/opencloud-eu/opencloud/pull/906)]
- build(deps): bump github.com/blevesearch/bleve/v2 from 2.5.0 to 2.5.1 [[#900](https://github.com/opencloud-eu/opencloud/pull/900)]
- build(deps): bump axios from 1.7.7 to 1.8.2 in /services/idp [[#902](https://github.com/opencloud-eu/opencloud/pull/902)]
- build(deps): bump github.com/opencloud-eu/libre-graph-api-go from 1.0.5 to 1.0.6 [[#899](https://github.com/opencloud-eu/opencloud/pull/899)]
- build(deps): bump @types/node from 20.14.11 to 22.15.19 in /services/idp [[#886](https://github.com/opencloud-eu/opencloud/pull/886)]
- build(deps-dev): bump i18next-conv from 14.1.0 to 15.1.1 in /services/idp [[#887](https://github.com/opencloud-eu/opencloud/pull/887)]
- build(deps): bump golang.org/x/net from 0.39.0 to 0.40.0 [[#889](https://github.com/opencloud-eu/opencloud/pull/889)]
- build(deps): bump github.com/olekukonko/tablewriter from 0.0.5 to 1.0.6 [[#888](https://github.com/opencloud-eu/opencloud/pull/888)]
## [2.3.0](https://github.com/opencloud-eu/opencloud/releases/tag/v2.3.0) - 2025-05-19
### ❤️ Thanks to all contributors! ❤️

View File

@@ -8,7 +8,7 @@ This deployment example is documented in two locations for different audiences:
* In the [Admin Documentation](https://docs.opencloud.eu/docs/admin/intro)\
Providing two variants using detailed configuration step by step guides:\
[Docker Compose Setup](https://docs.opencloud.eu/docs/admin/getting-started/container/docker-compose) and [Docker Compose Local](https://docs.opencloud.eu/docs/admin/getting-started/container/docker-compose-local).\
[Docker Compose Setup](https://docs.opencloud.eu/docs/admin/getting-started/docker/docker-compose) and [Docker Compose Local](https://docs.opencloud.eu/docs/admin/getting-started/docker/docker-compose-local).\
Note that these examples use LetsEncrypt certificates and are intended for production use.
* In the [Developer Documentation](https://docs.opencloud.eu/docs/dev/intro)\

60
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/Nerzal/gocloak/v13 v13.9.0
github.com/bbalet/stopwords v1.0.0
github.com/beevik/etree v1.5.1
github.com/blevesearch/bleve/v2 v2.5.2
github.com/blevesearch/bleve/v2 v2.5.1
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/coreos/go-oidc/v3 v3.14.1
github.com/cs3org/go-cs3apis v0.0.0-20241105092511-3ad35d174fc1
@@ -49,25 +49,25 @@ require (
github.com/jinzhu/now v1.1.5
github.com/justinas/alice v1.2.0
github.com/kovidgoyal/imaging v1.6.4
github.com/leonelquinteros/gotext v1.7.2
github.com/leonelquinteros/gotext v1.7.1
github.com/libregraph/idm v0.5.0
github.com/libregraph/lico v0.66.0
github.com/mitchellh/mapstructure v1.5.0
github.com/mna/pigeon v1.3.0
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/nats-io/nats-server/v2 v2.11.4
github.com/nats-io/nats.go v1.43.0
github.com/nats-io/nats.go v1.42.0
github.com/oklog/run v1.1.0
github.com/olekukonko/tablewriter v1.0.7
github.com/olekukonko/tablewriter v1.0.6
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
github.com/open-policy-agent/opa v1.5.1
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250603072916-fa601fb14450
github.com/opencloud-eu/reva/v2 v2.33.1
github.com/open-policy-agent/opa v1.4.2
github.com/opencloud-eu/libre-graph-api-go v1.0.6
github.com/opencloud-eu/reva/v2 v2.33.1-0.20250520152851-d33c49bb52b9
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
github.com/pkg/xattr v0.4.11
github.com/pkg/xattr v0.4.10
github.com/prometheus/client_golang v1.22.0
github.com/r3labs/sse/v2 v2.10.0
github.com/riandyrn/otelchi v0.12.1
@@ -88,24 +88,24 @@ require (
github.com/xhit/go-simple-mail/v2 v2.16.0
go-micro.dev/v4 v4.11.0
go.etcd.io/bbolt v1.4.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0
go.opentelemetry.io/contrib/zpages v0.61.0
go.opentelemetry.io/contrib/zpages v0.60.0
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/exporters/jaeger v1.17.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
golang.org/x/crypto v0.39.0
golang.org/x/crypto v0.38.0
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/image v0.28.0
golang.org/x/image v0.27.0
golang.org/x/net v0.40.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.15.0
golang.org/x/sync v0.14.0
golang.org/x/term v0.32.0
golang.org/x/text v0.26.0
golang.org/x/text v0.25.0
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237
google.golang.org/grpc v1.72.2
google.golang.org/grpc v1.72.1
google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v2 v2.4.0
gotest.tools/v3 v3.5.2
@@ -148,7 +148,7 @@ require (
github.com/blevesearch/zapx/v13 v13.4.2 // indirect
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
github.com/blevesearch/zapx/v16 v16.2.4 // indirect
github.com/blevesearch/zapx/v16 v16.2.3 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/bombsimon/logrusr/v3 v3.1.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
@@ -156,7 +156,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cevaris/ordered_map v0.0.0-20190319150403-3adeae072e73 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cornelk/hashmap v1.0.8 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
@@ -177,7 +177,7 @@ require (
github.com/evanphx/json-patch/v5 v5.5.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gdexlab/go-render v1.0.1 // indirect
github.com/go-acme/lego/v4 v4.4.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
@@ -246,7 +246,7 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/mattn/go-sqlite3 v1.14.27 // indirect
github.com/maxymania/go-system v0.0.0-20170110133659-647cc364bf0b // indirect
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 // indirect
github.com/miekg/dns v1.1.57 // indirect
@@ -254,7 +254,7 @@ require (
github.com/minio/crc64nvme v1.0.1 // indirect
github.com/minio/highwayhash v1.0.3 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.92 // indirect
github.com/minio/minio-go/v7 v7.0.89 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -266,19 +266,18 @@ require (
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
github.com/olekukonko/ll v0.0.8 // indirect
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pablodz/inotifywaitgo v0.0.9 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/cachecontrol v0.2.0 // indirect
github.com/prometheus/alertmanager v0.28.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/prometheus/statsd_exporter v0.22.8 // indirect
@@ -304,10 +303,8 @@ require (
github.com/tchap/go-patricia/v2 v2.3.2 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/trustelem/zxcvbn v1.0.1 // indirect
github.com/vektah/gqlparser/v2 v2.5.26 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map v1.0.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
@@ -316,21 +313,22 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
go.etcd.io/etcd/api/v3 v3.6.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.0 // indirect
go.etcd.io/etcd/client/v3 v3.6.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.20 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.20 // indirect
go.etcd.io/etcd/client/v3 v3.5.20 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/mod v0.25.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect

122
go.sum
View File

@@ -109,8 +109,6 @@ github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6Ct
github.com/aliyun/alibaba-cloud-sdk-go v1.61.976/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4=
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op h1:+OSa/t11TFhqfrX0EOSqQBDJ0YlpmK0rDSiB19dg9M0=
@@ -134,6 +132,7 @@ github.com/bbalet/stopwords v1.0.0 h1:0TnGycCtY0zZi4ltKoOGRFIlZHv0WqpoIGUsObjztf
github.com/bbalet/stopwords v1.0.0/go.mod h1:sAWrQoDMfqARGIn4s6dp7OW7ISrshUD8IP2q3KoqPjc=
github.com/beevik/etree v1.5.1 h1:TC3zyxYp+81wAmbsi8SWUpZCurbxa6S8RITYRSkNRwo=
github.com/beevik/etree v1.5.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -147,8 +146,8 @@ github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blevesearch/bleve/v2 v2.5.2 h1:Ab0r0MODV2C5A6BEL87GqLBySqp/s9xFgceCju6BQk8=
github.com/blevesearch/bleve/v2 v2.5.2/go.mod h1:5Dj6dUQxZM6aqYT3eutTD/GpWKGFSsV8f7LDidFbwXo=
github.com/blevesearch/bleve/v2 v2.5.1 h1:cc/O++W2Hcjp1SU5ETHeE+QYWv2oV88ldYEPowdmg8M=
github.com/blevesearch/bleve/v2 v2.5.1/go.mod h1:9g/wnbWKm9AgXrU8Ecqi+IDdqjUHWymwkQRDg+5tafU=
github.com/blevesearch/bleve_index_api v1.2.8 h1:Y98Pu5/MdlkRyLM0qDHostYo7i+Vv1cDNhqTeR4Sy6Y=
github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/geo v0.2.3 h1:K9/vbGI9ehlXdxjxDRJtoAMt7zGAsMIzc6n8zWcwnhg=
@@ -181,8 +180,8 @@ github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT
github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.2.4 h1:tGgfvleXTAkwsD5mEzgM3zCS/7pgocTCnO1oyAUjlww=
github.com/blevesearch/zapx/v16 v16.2.4/go.mod h1:Rti/REtuuMmzwsI8/C/qIzRaEoSK/wiFYw5e5ctUKKs=
github.com/blevesearch/zapx/v16 v16.2.3 h1:7Y0r+a3diEvlazsncexq1qoFOcBd64xwMS7aDm4lo1s=
github.com/blevesearch/zapx/v16 v16.2.3/go.mod h1:wVJ+GtURAaRG9KQAMNYyklq0egV+XJlGcXNCE0OFjjA=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
@@ -227,9 +226,8 @@ github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkE
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@@ -320,8 +318,8 @@ github.com/fschade/icap-client v0.0.0-20240802074440-aade4a234387 h1:Y3wZgTr29sL
github.com/fschade/icap-client v0.0.0-20240802074440-aade4a234387/go.mod h1:HpntrRsQA6RKNXy2Nbr4kVj+NO3OYWpAQUVxeya+3sU=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gdexlab/go-render v1.0.1 h1:rxqB3vo5s4n1kF0ySmoNeSPRYkEsyHgln4jFIQY7v0U=
@@ -716,8 +714,8 @@ github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvf
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
github.com/leonelquinteros/gotext v1.7.1 h1:/JNPeE3lY5JeVYv2+KBpz39994W3W9fmZCGq3eO9Ri8=
github.com/leonelquinteros/gotext v1.7.1/go.mod h1:I0WoFDn9u2D3VbPnnDPT8mzZu0iSXG8iih+AH2fHHqg=
github.com/libregraph/idm v0.5.0 h1:tDMwKbAOZzdeDYMxVlY5PbSqRKO7dbAW9KT42A51WSk=
github.com/libregraph/idm v0.5.0/go.mod h1:BGMwIQ/6orJSPVzJ1x6kgG2JyG9GY05YFmbsnaD80k0=
github.com/libregraph/lico v0.66.0 h1:7T6fD1YF0Ep9n0g4KN6dvWHTlDC3awrQpgsP5GdYCF4=
@@ -763,8 +761,8 @@ github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -784,8 +782,8 @@ github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.92 h1:jpBFWyRS3p8P/9tsRc+NuvqoFi7qAmTCFPoRFmobbVw=
github.com/minio/minio-go/v7 v7.0.92/go.mod h1:vTIc8DNcnAZIhyFsk8EB90AbPjj3j68aWIEQCiPj7d0=
github.com/minio/minio-go/v7 v7.0.89 h1:hx4xV5wwTUfyv8LarhJAwNecnXpoTsj9v3f3q/ZkiJU=
github.com/minio/minio-go/v7 v7.0.89/go.mod h1:2rFnGAp02p7Dddo1Fq4S2wYOfpF0MUTSeLTRC90I204=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
@@ -824,8 +822,8 @@ github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI=
github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.11.4 h1:oQhvy6He6ER926sGqIKBKuYHH4BGnUQCNb0Y5Qa+M54=
github.com/nats-io/nats-server/v2 v2.11.4/go.mod h1:jFnKKwbNeq6IfLHq+OMnl7vrFRihQ/MkhRbiWfjLdjU=
github.com/nats-io/nats.go v1.43.0 h1:uRFZ2FEoRvP64+UUhaTokyS18XBCR/xM2vQZKO4i8ug=
github.com/nats-io/nats.go v1.43.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -846,11 +844,11 @@ github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DV
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985 h1:V2wKiwjwAfRJRtUP6pC7wt4opeF14enO0du2dRV6Llo=
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
github.com/olekukonko/tablewriter v1.0.6 h1:/T45mIHc5hcEvibgzBzvMy7ruT+RjgoQRvkHbnl6OWA=
github.com/olekukonko/tablewriter v1.0.6/go.mod h1:SJ0MV1aHb/89fLcsBMXMp30Xg3g5eGoOUu0RptEk4AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@@ -863,14 +861,14 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/open-policy-agent/opa v1.5.1 h1:LTxxBJusMVjfs67W4FoRcnMfXADIGFMzpqnfk6D08Cg=
github.com/open-policy-agent/opa v1.5.1/go.mod h1:bYbS7u+uhTI+cxHQIpzvr5hxX0hV7urWtY+38ZtjMgk=
github.com/open-policy-agent/opa v1.4.2 h1:ag4upP7zMsa4WE2p1pwAFeG4Pn3mNwfAx9DLhhJfbjU=
github.com/open-policy-agent/opa v1.4.2/go.mod h1:DNzZPKqKh4U0n0ANxcCVlw8lCSv2c+h5G/3QvSYdWZ8=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a h1:Sakl76blJAaM6NxylVkgSzktjo2dS504iDotEFJsh3M=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250603072916-fa601fb14450 h1:QWn9G2f1R/EbyZSbkjtd9jqNq9X0NIphmmD4KYLNZtA=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250603072916-fa601fb14450/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.33.1 h1:dm3AxLx7MCHPI9h23iYr/Uw/AQHmQN+G4XlbZQnm35Y=
github.com/opencloud-eu/reva/v2 v2.33.1/go.mod h1:0Eu27TSdmj1+0PGn8MjFNh84nW16kGzt2Cxdz9oUncM=
github.com/opencloud-eu/libre-graph-api-go v1.0.6 h1:bUQq0tfaLboZZmPuI6C1rr/wFIVOIM9IsE1WqI5QsDA=
github.com/opencloud-eu/libre-graph-api-go v1.0.6/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.33.1-0.20250520152851-d33c49bb52b9 h1:7y8gTqVQSXLyAqeUFesbI58OkgGcS5fmfq2f3e95XOI=
github.com/opencloud-eu/reva/v2 v2.33.1-0.20250520152851-d33c49bb52b9/go.mod h1:8S3B+GPFdGMcNL/pkSHI4K2/E0ICvR7qxllE7Ooydm8=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
@@ -892,8 +890,6 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
@@ -905,8 +901,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
github.com/pkg/xattr v0.4.11 h1:DA7usy0rTMNMGvm06b5LhZUwiPj708D89S8DkXpMB1E=
github.com/pkg/xattr v0.4.11/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA=
github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -937,8 +933,8 @@ github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.0.0-20170706130215-fb369f752a7f/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
@@ -1091,8 +1087,6 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
@@ -1113,8 +1107,6 @@ github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/vektah/gqlparser/v2 v2.5.26 h1:REqqFkO8+SOEgZHR/eHScjjVjGS8Nk3RMO/juiTobN4=
github.com/vektah/gqlparser/v2 v2.5.26/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
@@ -1156,12 +1148,12 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.etcd.io/etcd/api/v3 v3.6.0 h1:vdbkcUBGLf1vfopoGE/uS3Nv0KPyIpUV/HM6w9yx2kM=
go.etcd.io/etcd/api/v3 v3.6.0/go.mod h1:Wt5yZqEmxgTNJGHob7mTVBJDZNXiHPtXTcPab37iFOw=
go.etcd.io/etcd/client/pkg/v3 v3.6.0 h1:nchnPqpuxvv3UuGGHaz0DQKYi5EIW5wOYsgUNRc365k=
go.etcd.io/etcd/client/pkg/v3 v3.6.0/go.mod h1:Jv5SFWMnGvIBn8o3OaBq/PnT0jjsX8iNokAUessNjoA=
go.etcd.io/etcd/client/v3 v3.6.0 h1:/yjKzD+HW5v/3DVj9tpwFxzNbu8hjcKID183ug9duWk=
go.etcd.io/etcd/client/v3 v3.6.0/go.mod h1:Jzk/Knqe06pkOZPHXsQ0+vNDvMQrgIqJ0W8DwPdMJMg=
go.etcd.io/etcd/api/v3 v3.5.20 h1:aKfz3nPZECWoZJXMSH9y6h2adXjtOHaHTGEVCuCmaz0=
go.etcd.io/etcd/api/v3 v3.5.20/go.mod h1:QqKGViq4KTgOG43dr/uH0vmGWIaoJY3ggFi6ZH0TH/U=
go.etcd.io/etcd/client/pkg/v3 v3.5.20 h1:sZIAtra+xCo56gdf6BR62to/hiie5Bwl7hQIqMzVTEM=
go.etcd.io/etcd/client/pkg/v3 v3.5.20/go.mod h1:qaOi1k4ZA9lVLejXNvyPABrVEe7VymMF2433yyRQ7O0=
go.etcd.io/etcd/client/v3 v3.5.20 h1:jMT2MwQEhyvhQg49Cec+1ZHJzfUf6ZgcmV0GjPv0tIQ=
go.etcd.io/etcd/client/v3 v3.5.20/go.mod h1:J5lbzYRMUR20YolS5UjlqqMcu3/wdEvG5VNBhzyo3m0=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -1174,12 +1166,12 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/contrib/zpages v0.61.0 h1:tYvUj377Dn3k1wf1le/f8YWSNQ8k0byS3jK8PiIXu9Y=
go.opentelemetry.io/contrib/zpages v0.61.0/go.mod h1:MFNPHMJOGA1P6m5501ANjOJDp4A9BUQja1Y53CDL8LQ=
go.opentelemetry.io/contrib/zpages v0.60.0 h1:wOM9ie1Hz4H88L9KE6GrGbKJhfm+8F1NfW/Y3q9Xt+8=
go.opentelemetry.io/contrib/zpages v0.60.0/go.mod h1:xqfToSRGh2MYUsfyErNz8jnNDPlnpZqWM/y6Z2Cx7xw=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
@@ -1203,6 +1195,8 @@ go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAj
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
@@ -1215,8 +1209,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1238,8 +1232,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1255,8 +1249,8 @@ golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScy
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1281,8 +1275,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1365,8 +1359,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1477,8 +1471,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1541,8 +1535,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1623,8 +1617,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e h1:m7aQHHqd0q89mRwhwS9Bx2rjyl/hsFAeta+uGrHsQaU=
google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e/go.mod h1:gID3PKrg7pWKntu9Ss6zTLJ0ttC0X9IHgREOCZwbCVU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=

View File

@@ -19,7 +19,6 @@ var (
// 9113a718-8285-4b32-9042-f930f1a58ac2.REV.2024-05-22T07:32:53.89969726Z.mpk
// 9113a718-8285-4b32-9042-f930f1a58ac2.REV.2024-05-22T07:32:53.89969726Z.mlock
_versionRegex = regexp.MustCompile(`\.REV\.[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z*`)
_spaceIDRegex = regexp.MustCompile(`/spaces/(.+)/nodes/`)
)
// DelBlobstore is the interface for a blobstore that can delete blobs.
@@ -146,10 +145,7 @@ func PurgeRevisions(nodes <-chan string, bs DelBlobstore, dryRun, verbose bool)
continue
}
var (
spaceID, blobID string
)
var blobID string
e := filepath.Ext(d)
switch e {
case ".mpk":
@@ -158,12 +154,6 @@ func PurgeRevisions(nodes <-chan string, bs DelBlobstore, dryRun, verbose bool)
fmt.Printf("error getting blobID from %s: %v\n", d, err)
continue
}
matches := _spaceIDRegex.FindStringSubmatch(d)
if len(matches) != 2 {
fmt.Printf("error extracting spaceID from %s\n", d)
continue
}
spaceID = strings.ReplaceAll(matches[1], "/", "")
countBlobs++
case ".mlock":
@@ -175,7 +165,7 @@ func PurgeRevisions(nodes <-chan string, bs DelBlobstore, dryRun, verbose bool)
if !dryRun {
if blobID != "" {
// TODO: needs spaceID for decomposeds3
if err := bs.Delete(&node.Node{BaseNode: node.BaseNode{SpaceID: spaceID}, BlobID: blobID}); err != nil {
if err := bs.Delete(&node.Node{BlobID: blobID}); err != nil {
fmt.Printf("error deleting blob %s: %v\n", blobID, err)
continue
}

View File

@@ -1,166 +0,0 @@
package metadata
import (
"context"
"errors"
"sync"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-playground/validator/v10"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/opencloud-eu/opencloud/pkg/storage"
)
// Lazy is a lazy storage implementation that initializes the underlying storage only when needed.
type Lazy struct {
next func() (metadata.Storage, error)
initName string `validate:"required"`
initCTX context.Context `validate:"required"`
}
func NewLazyStorage(next metadata.Storage) (*Lazy, error) {
s := &Lazy{}
s.next = sync.OnceValues[metadata.Storage, error](func() (metadata.Storage, error) {
if err := validator.New(validator.WithPrivateFieldValidation()).Struct(s); err != nil {
return nil, errors.Join(storage.ErrStorageInitialization, storage.ErrStorageValidation, err)
}
if err := next.Init(s.initCTX, s.initName); err != nil {
return nil, errors.Join(storage.ErrStorageInitialization, err)
}
return next, nil
})
return s, nil
}
// Backend wraps the backend of the next storage
func (s *Lazy) Backend() string {
next, err := s.next()
if err != nil {
return ""
}
return next.Backend()
}
// Init prepares the required data for the underlying lazy storage initialization
func (s *Lazy) Init(ctx context.Context, name string) (err error) {
s.initCTX = ctx
s.initName = name
return nil
}
// Upload wraps the upload method of the next storage
func (s *Lazy) Upload(ctx context.Context, req metadata.UploadRequest) (*metadata.UploadResponse, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.Upload(ctx, req)
}
// Download wraps the download method of the next storage
func (s *Lazy) Download(ctx context.Context, req metadata.DownloadRequest) (*metadata.DownloadResponse, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.Download(ctx, req)
}
// SimpleUpload wraps the simple upload method of the next storage
func (s *Lazy) SimpleUpload(ctx context.Context, uploadpath string, content []byte) error {
next, err := s.next()
if err != nil {
return err
}
return next.SimpleUpload(ctx, uploadpath, content)
}
// SimpleDownload wraps the simple download method of the next storage
func (s *Lazy) SimpleDownload(ctx context.Context, path string) ([]byte, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.SimpleDownload(ctx, path)
}
// Delete wraps the delete method of the next storage
func (s *Lazy) Delete(ctx context.Context, path string) error {
next, err := s.next()
if err != nil {
return err
}
return next.Delete(ctx, path)
}
// Stat wraps the stat method of the next storage
func (s *Lazy) Stat(ctx context.Context, path string) (*provider.ResourceInfo, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.Stat(ctx, path)
}
// ReadDir wraps the read directory method of the next storage
func (s *Lazy) ReadDir(ctx context.Context, path string) ([]string, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.ReadDir(ctx, path)
}
// ListDir wraps the list directory method of the next storage
func (s *Lazy) ListDir(ctx context.Context, path string) ([]*provider.ResourceInfo, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.ListDir(ctx, path)
}
// CreateSymlink wraps the create symlink method of the next storage
func (s *Lazy) CreateSymlink(ctx context.Context, oldname, newname string) error {
next, err := s.next()
if err != nil {
return err
}
return next.CreateSymlink(ctx, oldname, newname)
}
// ResolveSymlink wraps the resolve symlink method of the next storage
func (s *Lazy) ResolveSymlink(ctx context.Context, name string) (string, error) {
next, err := s.next()
if err != nil {
return "", err
}
return next.ResolveSymlink(ctx, name)
}
// MakeDirIfNotExist wraps the make directory if not exist method of the next storage
func (s *Lazy) MakeDirIfNotExist(ctx context.Context, name string) error {
next, err := s.next()
if err != nil {
return err
}
return next.MakeDirIfNotExist(ctx, name)
}

View File

@@ -1,13 +0,0 @@
package storage
import (
"errors"
)
var (
// ErrStorageInitialization is returned when the storage initialization fails
ErrStorageInitialization = errors.New("failed to initialize storage")
// ErrStorageValidation is returned when the storage configuration is invalid
ErrStorageValidation = errors.New("failed to validate storage configuration")
)

View File

@@ -16,7 +16,7 @@ var (
// LatestTag is the latest released version plus the dev meta version.
// Will be overwritten by the release pipeline
// Needs a manual change for every tagged release
LatestTag = "3.0.0+dev"
LatestTag = "2.3.0+dev"
// Date indicates the build date.
// This has been removed, it looks like you can only replace static strings with recent go versions

View File

@@ -34,7 +34,6 @@ type Config struct {
Context context.Context `yaml:"-"`
WriteBufferDuration time.Duration `yaml:"write_buffer_duration" env:"ACTIVITYLOG_WRITE_BUFFER_DURATION" desc:"The duration to wait before flushing the write buffer. This is used to reduce the number of writes to the store." introductionVersion:"%%NEXT%%"`
MaxActivities int `yaml:"max_activities" env:"ACTIVITYLOG_MAX_ACTIVITIES" desc:"The maximum number of activities to keep in the store per resource. If the number of activities exceeds this value, the oldest activities will be removed." introductionVersion:"%%NEXT%%"`
}
// Events combines the configuration options for the event bus.

View File

@@ -53,7 +53,6 @@ func DefaultConfig() *config.Config {
},
},
WriteBufferDuration: 10 * time.Second,
MaxActivities: 6000,
}
}

View File

@@ -88,6 +88,7 @@ func Server(opts ...Option) (http.Service, error) {
svc.HistoryClient(options.HistoryClient),
svc.ValueClient(options.ValueClient),
svc.RegisteredEvents(options.RegisteredEvents),
svc.WriteBufferDuration(options.Config.WriteBufferDuration),
)
if err != nil {
return http.Service{}, err

View File

@@ -1,129 +0,0 @@
package service
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/nats-io/nats.go"
"github.com/vmihailenco/msgpack/v5"
)
const activitylogVersionKey = "activitylog.version"
const currentMigrationVersion = "1"
// RunMigrations checks the activitylog data version and runs migrations if necessary.
// It should be called during service startup, after the NATS KeyValue store is initialized.
func (a *ActivitylogService) runMigrations(ctx context.Context, kv nats.KeyValue) error {
entry, err := kv.Get(activitylogVersionKey)
if err == nats.ErrKeyNotFound {
a.log.Info().Msg("activitylog version key not found. Running migration to V1...")
return a.migrateToV1(ctx, kv)
} else if err != nil {
return fmt.Errorf("failed to get activitylog version from NATS KV store: %w", err)
}
version := string(entry.Value())
if version == currentMigrationVersion {
a.log.Debug().Str("currentVersion", version).Msg("No migration needed")
return nil
}
// If version is something else, it might indicate a future version or an unexpected state.
// Add logic here if more complex version handling is needed.
return fmt.Errorf("unexpected activitylog version: %s, expected %s or older", version, currentMigrationVersion)
}
// migrateToV1 performs the data migration to version 1.
// It iterates over all keys, expecting their values to be JSON arrays of strings.
// For each such key, it creates a new key in the format "originalKey.count.timestamp"
// and stores the original list of strings (re-marshalled to messagepack) as its value.
// Finally, it sets the activitylog.version key to "1".
func (a *ActivitylogService) migrateToV1(_ context.Context, kv nats.KeyValue) error {
lister, err := kv.ListKeys()
if err != nil {
return fmt.Errorf("migrateToV1: failed to list keys from NATS KV store: %w", err)
}
migratedCount := 0
skippedCount := 0
keyChan := lister.Keys()
defer lister.Stop()
// keyValueEnvelope is the data structure used by the go micro plugin which was used previously.
type keyValueEnvelope struct {
Key string `json:"key"`
Data []byte `json:"data"`
Metadata map[string]interface{} `json:"metadata"`
}
for key := range keyChan {
if key == activitylogVersionKey {
skippedCount++
continue // Skip the version key itself
}
// Get the original value
entry, err := kv.Get(key)
if err != nil {
a.log.Error().Err(err).Str("key", key).Msg("migrateToV1: Failed to get value for key. Skipping.")
skippedCount++
continue
}
valBytes := entry.Value()
val := keyValueEnvelope{}
// Unmarshal the value into the keyValueEnvelope structure
if err := json.Unmarshal(valBytes, &val); err != nil {
a.log.Error().Err(err).Str("key", key).Msg("migrateToV1: Value for key ss not a keyValueEnvelope. Skipping.")
skippedCount++
continue
}
// Unmarshal value into a list of strings
var activities []RawActivity
if err := msgpack.Unmarshal(val.Data, &activities); err != nil {
if err := json.Unmarshal(val.Data, &activities); err != nil {
// This key's value is not a JSON array of strings. Skip it.
a.log.Error().Err(err).Str("key", key).Msg("migrateToV1: Value for key is not a msgback or JSON array of strings. Skipping.")
skippedCount++
continue
}
}
// Construct the new key
newKey := natsKey(val.Key, len(activities))
newValue, err := msgpack.Marshal(activities)
if err != nil {
a.log.Error().Err(err).Str("key", key).Msg("migrateToV1: Failed to marshal activities. Skipping.")
skippedCount++
continue
}
// Write the value (the list of strings, marshalled as messagepack) under the new key
if _, err := kv.Put(newKey, newValue); err != nil {
a.log.Error().Err(err).Str("newKey", newKey).Str("key", key).Msg("migrateToV1: Failed to put new key. Skipping.")
skippedCount++
continue
}
// delete old key, it's no longer needed
if err := kv.Delete(key); err != nil {
log.Printf("migrateToV1: Failed to delete old key '%s' after migration: %v. Skipping deletion.", key, err)
skippedCount++
continue
}
migratedCount++
}
// Set the activitylog version to "1" after migration
if _, err := kv.PutString(activitylogVersionKey, currentMigrationVersion); err != nil {
return fmt.Errorf("migrateToV1: failed to set activitylog version key to '%s' in NATS KV store: %w", currentMigrationVersion, err)
}
a.log.Info().Int("migrated", migratedCount).Int("skipped", skippedCount).Msg("Migration to V1 complete")
return nil
}

View File

@@ -31,7 +31,6 @@ type Options struct {
HistoryClient ehsvc.EventHistoryService
ValueClient settingssvc.ValueService
WriteBufferDuration time.Duration
MaxActivities int
}
// Logger configures a logger for the activitylog service
@@ -103,3 +102,10 @@ func ValueClient(vs settingssvc.ValueService) Option {
o.ValueClient = vs
}
}
// WriteBufferDuration sets the write buffer duration
func WriteBufferDuration(d time.Duration) Option {
return func(o *Options) {
o.WriteBufferDuration = d
}
}

View File

@@ -12,9 +12,9 @@ import (
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/pkg/l10n"
)
@@ -61,13 +61,6 @@ type Actor struct {
DisplayName string `json:"displayName"`
}
// Sharee represents a share reciever (group or user)
type Sharee struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
ShareType string `json:"shareType"`
}
// ActivityOption allows setting variables for an activity
type ActivityOption func(context.Context, gateway.GatewayAPIClient, map[string]interface{}) error
@@ -211,23 +204,20 @@ func WithSharee(uid *user.UserId, gid *group.GroupId) ActivityOption {
case uid != nil:
u, err := utils.GetUser(uid, gwc)
if err != nil {
vars["sharee"] = Sharee{
vars["sharee"] = Actor{
DisplayName: "DeletedUser",
ShareType: "user",
}
return err
}
vars["sharee"] = Sharee{
vars["sharee"] = Actor{
ID: uid.GetOpaqueId(),
DisplayName: u.GetUsername(),
ShareType: "user",
}
case gid != nil:
vars["sharee"] = Sharee{
vars["sharee"] = Actor{
ID: gid.GetOpaqueId(),
DisplayName: "DeletedGroup",
ShareType: "group",
}
r, err := gwc.GetGroup(ctx, &group.GetGroupRequest{GroupId: gid})
if err != nil {
@@ -238,10 +228,9 @@ func WithSharee(uid *user.UserId, gid *group.GroupId) ActivityOption {
return fmt.Errorf("error getting group: %s", r.GetStatus().GetMessage())
}
vars["sharee"] = Sharee{
vars["sharee"] = Actor{
ID: gid.GetOpaqueId(),
DisplayName: r.GetGroup().GetDisplayName(),
ShareType: "group",
}
}

View File

@@ -2,14 +2,11 @@ package service
import (
"context"
"encoding/base32"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"time"
@@ -17,13 +14,12 @@ import (
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/jellydator/ttlcache/v2"
"github.com/nats-io/nats.go"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"github.com/vmihailenco/msgpack/v5"
microstore "go-micro.dev/v4/store"
"go.opentelemetry.io/otel/trace"
"github.com/opencloud-eu/opencloud/pkg/log"
@@ -33,7 +29,7 @@ import (
)
// Nats runs into max payload exceeded errors at around 7k activities. Let's keep a buffer.
var _maxActivitiesDefault = 6000
var _maxActivities = 6000
// RawActivity represents an activity as it is stored in the activitylog store
type RawActivity struct {
@@ -47,6 +43,7 @@ type ActivitylogService struct {
cfg *config.Config
log log.Logger
events <-chan events.Event
store microstore.Store
gws pool.Selectable[gateway.GatewayAPIClient]
mux *chi.Mux
evHistory ehsvc.EventHistoryService
@@ -56,9 +53,6 @@ type ActivitylogService struct {
tracer trace.Tracer
debouncer *Debouncer
parentIdCache *ttlcache.Cache
natskv nats.KeyValue
maxActivities int
registeredEvents map[string]events.Unmarshaller
}
@@ -77,12 +71,6 @@ type queueItem struct {
timer *time.Timer
}
type batchInfo struct {
key string
count int
timestamp time.Time
}
// NewDebouncer returns a new Debouncer instance
func NewDebouncer(d time.Duration, f func(id string, ra []RawActivity) error) *Debouncer {
return &Debouncer{
@@ -140,9 +128,7 @@ func (d *Debouncer) Debounce(id string, ra RawActivity) {
// New creates a new ActivitylogService
func New(opts ...Option) (*ActivitylogService, error) {
o := &Options{
MaxActivities: _maxActivitiesDefault,
}
o := &Options{}
for _, opt := range opts {
opt(o)
}
@@ -151,6 +137,10 @@ func New(opts ...Option) (*ActivitylogService, error) {
return nil, errors.New("stream is required")
}
if o.Store == nil {
return nil, errors.New("store is required")
}
ch, err := events.Consume(o.Stream, o.Config.Service.Name, o.RegisteredEvents...)
if err != nil {
return nil, err
@@ -162,41 +152,11 @@ func New(opts ...Option) (*ActivitylogService, error) {
return nil, err
}
// Connect to NATS servers
natsOptions := nats.Options{
Servers: o.Config.Store.Nodes,
}
conn, err := natsOptions.Connect()
if err != nil {
return nil, err
}
js, err := conn.JetStream()
if err != nil {
return nil, err
}
kv, err := js.KeyValue(o.Config.Store.Database)
if err != nil {
if !errors.Is(err, nats.ErrBucketNotFound) {
return nil, errors.Wrapf(err, "Failed to get bucket (%s)", o.Config.Store.Database)
}
kv, err = js.CreateKeyValue(&nats.KeyValueConfig{
Bucket: o.Config.Store.Database,
})
if err != nil {
return nil, errors.Wrapf(err, "Failed to create bucket (%s)", o.Config.Store.Database)
}
}
if err != nil {
return nil, err
}
s := &ActivitylogService{
log: o.Logger,
cfg: o.Config,
events: ch,
store: o.Store,
gws: o.GatewaySelector,
mux: o.Mux,
evHistory: o.HistoryClient,
@@ -206,16 +166,8 @@ func New(opts ...Option) (*ActivitylogService, error) {
tp: o.TraceProvider,
tracer: o.TraceProvider.Tracer("github.com/opencloud-eu/opencloud/services/activitylog/pkg/service"),
parentIdCache: cache,
maxActivities: o.Config.MaxActivities,
natskv: kv,
}
s.debouncer = NewDebouncer(o.Config.WriteBufferDuration, s.storeActivity)
// run migrations
err = s.runMigrations(context.Background(), kv)
if err != nil {
return nil, err
}
s.debouncer = NewDebouncer(o.WriteBufferDuration, s.storeActivity)
s.mux.Get("/graph/v1beta1/extensions/org.libregraph/activities", s.HandleGetItemActivities)
@@ -298,7 +250,7 @@ func (a *ActivitylogService) AddActivity(initRef *provider.Reference, parentId *
ctx, span = a.tracer.Start(ctx, "AddActivity")
defer span.End()
return a.addActivity(ctx, initRef, parentId, eventID, timestamp, func(ctx context.Context, ref *provider.Reference) (*provider.ResourceInfo, error) {
return a.addActivity(ctx, initRef, parentId, eventID, timestamp, func(ref *provider.Reference) (*provider.ResourceInfo, error) {
return utils.GetResource(ctx, ref, gwc)
})
}
@@ -333,10 +285,10 @@ func (a *ActivitylogService) AddActivityTrashed(resourceID *provider.ResourceId,
}
var span trace.Span
ctx, span = a.tracer.Start(ctx, "AddActivityTrashed")
ctx, span = a.tracer.Start(ctx, "AddActivity")
defer span.End()
return a.addActivity(ctx, ref, parentId, eventID, timestamp, func(ctx context.Context, ref *provider.Reference) (*provider.ResourceInfo, error) {
return a.addActivity(ctx, ref, parentId, eventID, timestamp, func(ref *provider.Reference) (*provider.ResourceInfo, error) {
return utils.GetResource(ctx, ref, gwc)
})
}
@@ -391,8 +343,10 @@ func (a *ActivitylogService) RemoveActivities(rid *provider.ResourceId, toDelete
return err
}
_, err = a.natskv.Put(storagespace.FormatResourceID(rid), b)
return err
return a.store.Write(&microstore.Record{
Key: storagespace.FormatResourceID(rid),
Value: b,
})
}
// RemoveResource removes the resource from the store
@@ -404,53 +358,45 @@ func (a *ActivitylogService) RemoveResource(rid *provider.ResourceId) error {
a.lock.Lock()
defer a.lock.Unlock()
return a.natskv.Delete(storagespace.FormatResourceID(rid))
return a.store.Delete(storagespace.FormatResourceID(rid))
}
func (a *ActivitylogService) activities(rid *provider.ResourceId) ([]RawActivity, error) {
resourceID := storagespace.FormatResourceID(rid)
glob := fmt.Sprintf("%s.>", base32.StdEncoding.EncodeToString([]byte(resourceID)))
watcher, err := a.natskv.Watch(glob, nats.IgnoreDeletes())
if err != nil {
return nil, err
records, err := a.store.Read(resourceID)
if err != nil && err != microstore.ErrNotFound {
return nil, fmt.Errorf("could not read activities: %w", err)
}
if len(records) == 0 {
return []RawActivity{}, nil
}
defer watcher.Stop()
var activities []RawActivity
for update := range watcher.Updates() {
if update == nil {
break
if err := msgpack.Unmarshal(records[0].Value, &activities); err != nil {
a.log.Debug().Err(err).Str("resourceID", resourceID).Msg("could not unmarshal messagepack, trying json")
if err := json.Unmarshal(records[0].Value, &activities); err != nil {
return nil, fmt.Errorf("could not unmarshal activities: %w", err)
}
var batchActivities []RawActivity
if err := msgpack.Unmarshal(update.Value(), &batchActivities); err != nil {
a.log.Debug().Err(err).Str("resourceID", resourceID).Msg("could not unmarshal messagepack, trying json")
}
activities = append(activities, batchActivities...)
}
return activities, nil
}
// note: getResource is abstracted to allow unit testing, in general this will just be utils.GetResource
func (a *ActivitylogService) addActivity(ctx context.Context, initRef *provider.Reference, parentId *provider.ResourceId, eventID string, timestamp time.Time, getResource func(context.Context, *provider.Reference) (*provider.ResourceInfo, error)) error {
func (a *ActivitylogService) addActivity(ctx context.Context, initRef *provider.Reference, parentId *provider.ResourceId, eventID string, timestamp time.Time, getResource func(*provider.Reference) (*provider.ResourceInfo, error)) error {
var (
err error
depth int
ref = initRef
)
ctx, span := a.tracer.Start(ctx, "addActivity")
defer span.End()
for {
var info *provider.ResourceInfo
id := ref.GetResourceId()
if ref.Path != "" {
// Path based reference, we need to resolve the resource id
ctx, span = a.tracer.Start(ctx, "addActivity.getResource")
info, err = getResource(ctx, ref)
span.End()
info, err = getResource(ref)
if err != nil {
return fmt.Errorf("could not get resource info: %w", err)
}
@@ -461,15 +407,17 @@ func (a *ActivitylogService) addActivity(ctx context.Context, initRef *provider.
}
key := storagespace.FormatResourceID(id)
_, span := a.tracer.Start(ctx, "queueStoreActivity")
a.debouncer.Debounce(key, RawActivity{
EventID: eventID,
Depth: depth,
Timestamp: timestamp,
})
span.End()
if id.OpaqueId == id.SpaceId {
// we are at the root of the space, no need to go further
break
return nil
}
// check if parent id is cached
@@ -478,8 +426,8 @@ func (a *ActivitylogService) addActivity(ctx context.Context, initRef *provider.
if parentId == nil {
if v, err := a.parentIdCache.Get(key); err != nil {
if info == nil {
ctx, span := a.tracer.Start(ctx, "addActivity.getResource parent")
info, err = getResource(ctx, ref)
_, span = a.tracer.Start(ctx, "getResource")
info, err = getResource(ref)
span.End()
if err != nil || info.GetParentId() == nil || info.GetParentId().GetOpaqueId() == "" {
return fmt.Errorf("could not get parent id: %w", err)
@@ -498,8 +446,6 @@ func (a *ActivitylogService) addActivity(ctx context.Context, initRef *provider.
ref = &provider.Reference{ResourceId: parentId}
parentId = nil // reset parent id so it's not reused in the next iteration
}
return nil
}
func (a *ActivitylogService) storeActivity(resourceID string, activities []RawActivity) error {
@@ -508,110 +454,43 @@ func (a *ActivitylogService) storeActivity(resourceID string, activities []RawAc
ctx, span := a.tracer.Start(context.Background(), "storeActivity")
defer span.End()
_, subspan := a.tracer.Start(ctx, "store.Read")
records, err := a.store.Read(resourceID)
if err != nil && err != microstore.ErrNotFound {
return err
}
subspan.End()
_, subspan := a.tracer.Start(ctx, "storeActivity.Marshal")
_, subspan = a.tracer.Start(ctx, "Unmarshal")
var existingActivities []RawActivity
if len(records) > 0 {
if err := msgpack.Unmarshal(records[0].Value, &existingActivities); err != nil {
a.log.Debug().Err(err).Str("resourceID", resourceID).Msg("could not unmarshal messagepack, trying json")
if err := json.Unmarshal(records[0].Value, &existingActivities); err != nil {
return err
}
}
}
subspan.End()
if l := len(existingActivities) + len(activities); l >= _maxActivities {
start := min(len(existingActivities), l-_maxActivities+1)
existingActivities = existingActivities[start:]
}
activities = append(existingActivities, activities...)
_, subspan = a.tracer.Start(ctx, "Unmarshal")
b, err := msgpack.Marshal(activities)
if err != nil {
return err
}
subspan.End()
_, subspan = a.tracer.Start(ctx, "storeActivity.natskv.Put")
key := natsKey(resourceID, len(activities))
_, err = a.natskv.Put(key, b)
if err != nil {
return err
}
subspan.End()
ctx, subspan = a.tracer.Start(ctx, "storeActivity.enforceMaxActivities")
a.enforceMaxActivities(ctx, resourceID)
subspan.End()
return nil
}
func (a *ActivitylogService) enforceMaxActivities(ctx context.Context, resourceID string) {
if a.maxActivities <= 0 {
return
}
key := fmt.Sprintf("%s.>", base32.StdEncoding.EncodeToString([]byte(resourceID)))
_, subspan := a.tracer.Start(ctx, "enforceMaxActivities.watch")
watcher, err := a.natskv.Watch(key, nats.IgnoreDeletes())
if err != nil {
a.log.Error().Err(err).Str("resourceID", resourceID).Msg("could not watch")
return
}
defer watcher.Stop()
var keys []string
for update := range watcher.Updates() {
if update == nil {
break
}
var batchActivities []RawActivity
if err := msgpack.Unmarshal(update.Value(), &batchActivities); err != nil {
a.log.Debug().Err(err).Str("resourceID", resourceID).Msg("could not unmarshal messagepack, trying json")
}
keys = append(keys, update.Key())
}
subspan.End()
_, subspan = a.tracer.Start(ctx, "enforceMaxActivities.compile")
// Parse keys into batches
batches := make([]batchInfo, 0)
var activitiesCount int
for _, k := range keys {
parts := strings.SplitN(k, ".", 3)
if len(parts) < 3 {
a.log.Warn().Str("key", k).Msg("skipping key, not enough parts")
continue
}
c, err := strconv.Atoi(parts[1])
if err != nil {
a.log.Warn().Str("key", k).Msg("skipping key, can not parse count")
continue
}
// parse timestamp
nano, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
a.log.Warn().Str("key", k).Msg("skipping key, can not parse timestamp")
continue
}
batches = append(batches, batchInfo{
key: k,
count: c,
timestamp: time.Unix(0, nano),
})
activitiesCount += c
}
// sort batches by timestamp
sort.Slice(batches, func(i, j int) bool {
return batches[i].timestamp.Before(batches[j].timestamp)
return a.store.Write(&microstore.Record{
Key: resourceID,
Value: b,
})
subspan.End()
_, subspan = a.tracer.Start(ctx, "enforceMaxActivities.delete")
// remove oldest keys until we are at max activities
for _, b := range batches {
if activitiesCount-b.count < a.maxActivities {
break
}
activitiesCount -= b.count
err = a.natskv.Delete(b.key)
if err != nil {
a.log.Error().Err(err).Str("key", b.key).Msg("could not delete key")
break
}
}
subspan.End()
}
func toRef(r *provider.ResourceId) *provider.Reference {
@@ -652,10 +531,3 @@ func (a *ActivitylogService) removeCachedParentID(ref *provider.Reference) {
a.log.Error().Interface("event", ref).Err(err).Msg("could not delete parent id cache")
}
}
func natsKey(resourceID string, activitiesCount int) string {
return fmt.Sprintf("%s.%d.%d",
base32.StdEncoding.EncodeToString([]byte(resourceID)),
activitiesCount,
time.Now().UnixNano())
}

View File

@@ -2,101 +2,30 @@ package service
import (
"context"
"net"
"os"
"path/filepath"
"time"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
nserver "github.com/nats-io/nats-server/v2/server"
"github.com/jellydator/ttlcache/v2"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opencloud-eu/opencloud/services/activitylog/pkg/config"
eventsmocks "github.com/opencloud-eu/reva/v2/pkg/events/mocks"
"github.com/test-go/testify/mock"
"github.com/opencloud-eu/reva/v2/pkg/store"
"go.opentelemetry.io/otel/trace/noop"
)
var (
server *nserver.Server
tmpdir string
)
func getFreeLocalhostPort() (int, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return -1, err
}
port := l.Addr().(*net.TCPAddr).Port
_ = l.Close() // Close the listener immediately to free the port
return port, nil
}
// Spawn a nats server and a JetStream instance for the duration of the test suite.
// The different tests need to make sure to use different databases to avoid conflicts.
var _ = SynchronizedBeforeSuite(func() {
port, err := getFreeLocalhostPort()
server, err = nserver.NewServer(&nserver.Options{
Port: port,
})
Expect(err).ToNot(HaveOccurred())
tmpdir, err = os.MkdirTemp("", "activitylog-test")
natsdir := filepath.Join(tmpdir, "nats-js")
jsConf := &nserver.JetStreamConfig{
StoreDir: natsdir,
}
// first start NATS
go server.Start()
time.Sleep(time.Second)
// second start JetStream
err = server.EnableJetStream(jsConf)
Expect(err).ToNot(HaveOccurred())
}, func() {})
var _ = SynchronizedAfterSuite(func() {
server.Shutdown()
_ = os.RemoveAll(tmpdir)
}, func() {})
var _ = Describe("ActivitylogService", func() {
var (
alog *ActivitylogService
getResource func(_ context.Context, ref *provider.Reference) (*provider.ResourceInfo, error)
writebufferduration = 100 * time.Millisecond
alog *ActivitylogService
getResource func(ref *provider.Reference) (*provider.ResourceInfo, error)
)
JustBeforeEach(func() {
var err error
stream := &eventsmocks.Stream{}
stream.EXPECT().Consume(mock.Anything, mock.Anything).Return(nil, nil)
alog, err = New(
Config(&config.Config{
Service: config.Service{
Name: "activitylog-test",
},
Store: config.Store{
Store: "nats-js-kv",
Nodes: []string{server.Addr().String()},
Database: "activitylog-test-" + uuid.New().String(),
},
MaxActivities: 4,
WriteBufferDuration: writebufferduration,
}),
Stream(stream),
TraceProvider(noop.NewTracerProvider()),
Mux(chi.NewMux()),
)
Expect(err).ToNot(HaveOccurred())
})
Context("with a noop debouncer", func() {
BeforeEach(func() {
writebufferduration = 0
alog = &ActivitylogService{
store: store.Create(),
tracer: noop.NewTracerProvider().Tracer("test"),
parentIdCache: ttlcache.NewCache(),
}
alog.debouncer = NewDebouncer(0, alog.storeActivity)
})
Describe("AddActivity", func() {
@@ -147,8 +76,8 @@ var _ = Describe("ActivitylogService", func() {
for _, tc := range testCases {
tc := tc // capture range variable
Context(tc.Name, func() {
JustBeforeEach(func() {
getResource = func(_ context.Context, ref *provider.Reference) (*provider.ResourceInfo, error) {
BeforeEach(func() {
getResource = func(ref *provider.Reference) (*provider.ResourceInfo, error) {
return tc.Tree[ref.GetResourceId().GetOpaqueId()], nil
}
@@ -178,92 +107,30 @@ var _ = Describe("ActivitylogService", func() {
"spaceid": resourceInfo("spaceid", "spaceid"),
}
)
BeforeEach(func() {
writebufferduration = 100 * time.Millisecond
alog = &ActivitylogService{
store: store.Create(),
tracer: noop.NewTracerProvider().Tracer("test"),
parentIdCache: ttlcache.NewCache(),
}
alog.debouncer = NewDebouncer(100*time.Millisecond, alog.storeActivity)
})
Describe("addActivity", func() {
var (
getResource = func(_ context.Context, ref *provider.Reference) (*provider.ResourceInfo, error) {
return tree[ref.GetResourceId().GetOpaqueId()], nil
}
)
It("should debounce activities", func() {
getResource = func(ref *provider.Reference) (*provider.ResourceInfo, error) {
return tree[ref.GetResourceId().GetOpaqueId()], nil
}
It("debounces activities", func() {
err := alog.addActivity(context.Background(), reference("base"), nil, "activity1", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
err = alog.addActivity(context.Background(), reference("base"), nil, "activity2", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
err := alog.addActivity(context.Background(), reference("base"), nil, "activity1", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
err = alog.addActivity(context.Background(), reference("base"), nil, "activity2", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
Eventually(func(g Gomega) {
activities, err := alog.Activities(resourceID("base"))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(activities).To(ConsistOf(activitites("activity1", 0, "activity2", 0)))
}).Should(Succeed())
})
It("adheres to the MaxActivities setting", func() {
err := alog.addActivity(context.Background(), reference("base"), nil, "activity1", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
Eventually(func(g Gomega) {
activities, err := alog.Activities(resourceID("base"))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(len(activities)).To(Equal(1))
}).Should(Succeed())
err = alog.addActivity(context.Background(), reference("base"), nil, "activity2", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
Eventually(func(g Gomega) {
activities, err := alog.Activities(resourceID("base"))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(len(activities)).To(Equal(2))
}).Should(Succeed())
err = alog.addActivity(context.Background(), reference("base"), nil, "activity3", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
err = alog.addActivity(context.Background(), reference("base"), nil, "activity4", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
err = alog.addActivity(context.Background(), reference("base"), nil, "activity5", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
Eventually(func(g Gomega) {
activities, err := alog.Activities(resourceID("base"))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(activities).To(ConsistOf(activitites("activity2", 0, "activity3", 0, "activity4", 0, "activity5", 0)))
}).Should(Succeed())
})
})
Describe("Activities", func() {
It("combines multiple batches", func() {
getResource = func(_ context.Context, ref *provider.Reference) (*provider.ResourceInfo, error) {
return tree[ref.GetResourceId().GetOpaqueId()], nil
}
err := alog.addActivity(context.Background(), reference("base"), nil, "activity1", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
err = alog.addActivity(context.Background(), reference("base"), nil, "activity2", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
Eventually(func(g Gomega) {
activities, err := alog.Activities(resourceID("base"))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(activities).To(ConsistOf(activitites("activity1", 0, "activity2", 0)))
}).Should(Succeed())
err = alog.addActivity(context.Background(), reference("base"), nil, "activity3", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
err = alog.addActivity(context.Background(), reference("base"), nil, "activity4", time.Time{}, getResource)
Expect(err).NotTo(HaveOccurred())
Eventually(func(g Gomega) {
activities, err := alog.Activities(resourceID("base"))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(activities).To(ConsistOf(activitites("activity1", 0, "activity2", 0, "activity3", 0, "activity4", 0)))
}).Should(Succeed())
})
Eventually(func(g Gomega) {
activities, err := alog.Activities(resourceID("base"))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(activities).To(ConsistOf(activitites("activity1", 0, "activity2", 0)))
}).Should(Succeed())
})
})
})

View File

@@ -129,8 +129,9 @@ The default language can be defined via the `OC_DEFAULT_LANGUAGE` environment va
Unified Roles are roles granted a user for sharing and can be enabled or disabled. A CLI command is provided to list existing roles and their state among other data.
::: info
{{< hint info >}}
Note that a disabled role does not lose previously assigned permissions. It only means that the role is not available for new assignments.
{{< /hint >}}
The following roles are **enabled** by default:

View File

@@ -9,8 +9,6 @@ import (
mock "github.com/stretchr/testify/mock"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
svc "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0"
)
// DriveItemPermissionsProvider is an autogenerated mock type for the DriveItemPermissionsProvider type
@@ -296,9 +294,9 @@ func (_c *DriveItemPermissionsProvider_Invite_Call) RunAndReturn(run func(contex
return _c
}
// ListPermissions provides a mock function with given fields: ctx, itemID, queryOptions
func (_m *DriveItemPermissionsProvider) ListPermissions(ctx context.Context, itemID *providerv1beta1.ResourceId, queryOptions svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
ret := _m.Called(ctx, itemID, queryOptions)
// ListPermissions provides a mock function with given fields: ctx, itemID, listFederatedRoles, selectedAttrs
func (_m *DriveItemPermissionsProvider) ListPermissions(ctx context.Context, itemID *providerv1beta1.ResourceId, listFederatedRoles bool, selectedAttrs map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
ret := _m.Called(ctx, itemID, listFederatedRoles, selectedAttrs)
if len(ret) == 0 {
panic("no return value specified for ListPermissions")
@@ -306,17 +304,17 @@ func (_m *DriveItemPermissionsProvider) ListPermissions(ctx context.Context, ite
var r0 libregraph.CollectionOfPermissionsWithAllowedValues
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)); ok {
return rf(ctx, itemID, queryOptions)
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, bool, map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error)); ok {
return rf(ctx, itemID, listFederatedRoles, selectedAttrs)
}
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) libregraph.CollectionOfPermissionsWithAllowedValues); ok {
r0 = rf(ctx, itemID, queryOptions)
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, bool, map[string]struct{}) libregraph.CollectionOfPermissionsWithAllowedValues); ok {
r0 = rf(ctx, itemID, listFederatedRoles, selectedAttrs)
} else {
r0 = ret.Get(0).(libregraph.CollectionOfPermissionsWithAllowedValues)
}
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) error); ok {
r1 = rf(ctx, itemID, queryOptions)
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, bool, map[string]struct{}) error); ok {
r1 = rf(ctx, itemID, listFederatedRoles, selectedAttrs)
} else {
r1 = ret.Error(1)
}
@@ -332,14 +330,15 @@ type DriveItemPermissionsProvider_ListPermissions_Call struct {
// ListPermissions is a helper method to define mock.On call
// - ctx context.Context
// - itemID *providerv1beta1.ResourceId
// - queryOptions svc.ListPermissionsQueryOptions
func (_e *DriveItemPermissionsProvider_Expecter) ListPermissions(ctx interface{}, itemID interface{}, queryOptions interface{}) *DriveItemPermissionsProvider_ListPermissions_Call {
return &DriveItemPermissionsProvider_ListPermissions_Call{Call: _e.mock.On("ListPermissions", ctx, itemID, queryOptions)}
// - listFederatedRoles bool
// - selectedAttrs map[string]struct{}
func (_e *DriveItemPermissionsProvider_Expecter) ListPermissions(ctx interface{}, itemID interface{}, listFederatedRoles interface{}, selectedAttrs interface{}) *DriveItemPermissionsProvider_ListPermissions_Call {
return &DriveItemPermissionsProvider_ListPermissions_Call{Call: _e.mock.On("ListPermissions", ctx, itemID, listFederatedRoles, selectedAttrs)}
}
func (_c *DriveItemPermissionsProvider_ListPermissions_Call) Run(run func(ctx context.Context, itemID *providerv1beta1.ResourceId, queryOptions svc.ListPermissionsQueryOptions)) *DriveItemPermissionsProvider_ListPermissions_Call {
func (_c *DriveItemPermissionsProvider_ListPermissions_Call) Run(run func(ctx context.Context, itemID *providerv1beta1.ResourceId, listFederatedRoles bool, selectedAttrs map[string]struct{})) *DriveItemPermissionsProvider_ListPermissions_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*providerv1beta1.ResourceId), args[2].(svc.ListPermissionsQueryOptions))
run(args[0].(context.Context), args[1].(*providerv1beta1.ResourceId), args[2].(bool), args[3].(map[string]struct{}))
})
return _c
}
@@ -349,14 +348,14 @@ func (_c *DriveItemPermissionsProvider_ListPermissions_Call) Return(_a0 libregra
return _c
}
func (_c *DriveItemPermissionsProvider_ListPermissions_Call) RunAndReturn(run func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)) *DriveItemPermissionsProvider_ListPermissions_Call {
func (_c *DriveItemPermissionsProvider_ListPermissions_Call) RunAndReturn(run func(context.Context, *providerv1beta1.ResourceId, bool, map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error)) *DriveItemPermissionsProvider_ListPermissions_Call {
_c.Call.Return(run)
return _c
}
// ListSpaceRootPermissions provides a mock function with given fields: ctx, driveID, queryOptions
func (_m *DriveItemPermissionsProvider) ListSpaceRootPermissions(ctx context.Context, driveID *providerv1beta1.ResourceId, queryOptions svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
ret := _m.Called(ctx, driveID, queryOptions)
// ListSpaceRootPermissions provides a mock function with given fields: ctx, driveID, selectedAttrs
func (_m *DriveItemPermissionsProvider) ListSpaceRootPermissions(ctx context.Context, driveID *providerv1beta1.ResourceId, selectedAttrs map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
ret := _m.Called(ctx, driveID, selectedAttrs)
if len(ret) == 0 {
panic("no return value specified for ListSpaceRootPermissions")
@@ -364,17 +363,17 @@ func (_m *DriveItemPermissionsProvider) ListSpaceRootPermissions(ctx context.Con
var r0 libregraph.CollectionOfPermissionsWithAllowedValues
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)); ok {
return rf(ctx, driveID, queryOptions)
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error)); ok {
return rf(ctx, driveID, selectedAttrs)
}
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) libregraph.CollectionOfPermissionsWithAllowedValues); ok {
r0 = rf(ctx, driveID, queryOptions)
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, map[string]struct{}) libregraph.CollectionOfPermissionsWithAllowedValues); ok {
r0 = rf(ctx, driveID, selectedAttrs)
} else {
r0 = ret.Get(0).(libregraph.CollectionOfPermissionsWithAllowedValues)
}
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) error); ok {
r1 = rf(ctx, driveID, queryOptions)
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, map[string]struct{}) error); ok {
r1 = rf(ctx, driveID, selectedAttrs)
} else {
r1 = ret.Error(1)
}
@@ -390,14 +389,14 @@ type DriveItemPermissionsProvider_ListSpaceRootPermissions_Call struct {
// ListSpaceRootPermissions is a helper method to define mock.On call
// - ctx context.Context
// - driveID *providerv1beta1.ResourceId
// - queryOptions svc.ListPermissionsQueryOptions
func (_e *DriveItemPermissionsProvider_Expecter) ListSpaceRootPermissions(ctx interface{}, driveID interface{}, queryOptions interface{}) *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call {
return &DriveItemPermissionsProvider_ListSpaceRootPermissions_Call{Call: _e.mock.On("ListSpaceRootPermissions", ctx, driveID, queryOptions)}
// - selectedAttrs map[string]struct{}
func (_e *DriveItemPermissionsProvider_Expecter) ListSpaceRootPermissions(ctx interface{}, driveID interface{}, selectedAttrs interface{}) *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call {
return &DriveItemPermissionsProvider_ListSpaceRootPermissions_Call{Call: _e.mock.On("ListSpaceRootPermissions", ctx, driveID, selectedAttrs)}
}
func (_c *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call) Run(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, queryOptions svc.ListPermissionsQueryOptions)) *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call {
func (_c *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call) Run(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, selectedAttrs map[string]struct{})) *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*providerv1beta1.ResourceId), args[2].(svc.ListPermissionsQueryOptions))
run(args[0].(context.Context), args[1].(*providerv1beta1.ResourceId), args[2].(map[string]struct{}))
})
return _c
}
@@ -407,7 +406,7 @@ func (_c *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call) Return(_a0
return _c
}
func (_c *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call) RunAndReturn(run func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)) *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call {
func (_c *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call) RunAndReturn(run func(context.Context, *providerv1beta1.ResourceId, map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error)) *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
"github.com/opencloud-eu/opencloud/services/graph/pkg/odata"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
@@ -82,7 +81,7 @@ func (i *CS3) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libr
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
}
search, err := odata.GetSearchValues(oreq.Query)
search, err := GetSearchValues(oreq.Query)
if err != nil {
return nil, err
}
@@ -133,7 +132,7 @@ func (i *CS3) GetGroups(ctx context.Context, oreq *godata.GoDataRequest) ([]*lib
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
}
search, err := odata.GetSearchValues(oreq.Query)
search, err := GetSearchValues(oreq.Query)
if err != nil {
return nil, err
}

View File

@@ -19,7 +19,6 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
"github.com/opencloud-eu/opencloud/services/graph/pkg/odata"
)
const (
@@ -564,7 +563,7 @@ func (i *LDAP) GetUser(ctx context.Context, nameOrID string, oreq *godata.GoData
}
}
exp, err := odata.GetExpandValues(oreq.Query)
exp, err := GetExpandValues(oreq.Query)
if err != nil {
return nil, err
}
@@ -594,12 +593,12 @@ func (i *LDAP) FilterUsers(ctx context.Context, oreq *godata.GoDataRequest, filt
return nil, err
}
search, err := odata.GetSearchValues(oreq.Query)
search, err := GetSearchValues(oreq.Query)
if err != nil {
return nil, err
}
exp, err := odata.GetExpandValues(oreq.Query)
exp, err := GetExpandValues(oreq.Query)
if err != nil {
return nil, err
}

View File

@@ -15,7 +15,6 @@ import (
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
"github.com/opencloud-eu/opencloud/services/graph/pkg/odata"
)
type groupAttributeMap struct {
@@ -60,17 +59,17 @@ func (i *LDAP) GetGroups(ctx context.Context, oreq *godata.GoDataRequest) ([]*li
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetGroups")
search, err := odata.GetSearchValues(oreq.Query)
search, err := GetSearchValues(oreq.Query)
if err != nil {
return nil, err
}
var expandMembers bool
exp, err := odata.GetExpandValues(oreq.Query)
exp, err := GetExpandValues(oreq.Query)
if err != nil {
return nil, err
}
sel, err := odata.GetSelectValues(oreq.Query)
sel, err := GetSelectValues(oreq.Query)
if err != nil {
return nil, err
}
@@ -147,7 +146,7 @@ func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string, req *godata.
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetGroupMembers")
exp, err := odata.GetExpandValues(req.Query)
exp, err := GetExpandValues(req.Query)
if err != nil {
return nil, err
}
@@ -157,7 +156,7 @@ func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string, req *godata.
return nil, err
}
searchTerm, err := odata.GetSearchValues(req.Query)
searchTerm, err := GetSearchValues(req.Query)
if err != nil {
return nil, err
}

View File

@@ -0,0 +1,63 @@
package identity
import (
"strings"
"github.com/CiscoM31/godata"
)
// GetExpandValues extracts the values of the $expand query parameter and
// returns them in a []string, rejects any $expand value that consists of more
// than just a single path segment
func GetExpandValues(req *godata.GoDataQuery) ([]string, error) {
if req == nil || req.Expand == nil {
return []string{}, nil
}
expand := make([]string, 0, len(req.Expand.ExpandItems))
for _, item := range req.Expand.ExpandItems {
if item.Filter != nil || item.At != nil || item.Search != nil ||
item.OrderBy != nil || item.Skip != nil || item.Top != nil ||
item.Select != nil || item.Compute != nil || item.Expand != nil ||
item.Levels != 0 {
return []string{}, godata.NotImplementedError("options for $expand not supported")
}
if len(item.Path) > 1 {
return []string{}, godata.NotImplementedError("multiple segments in $expand not supported")
}
expand = append(expand, item.Path[0].Value)
}
return expand, nil
}
// GetSelectValues extracts the values of the $select query parameter and
// returns them in a []string, rejects any $select value that consists of more
// than just a single path segment
func GetSelectValues(req *godata.GoDataQuery) ([]string, error) {
if req == nil || req.Select == nil {
return []string{}, nil
}
sel := make([]string, 0, len(req.Select.SelectItems))
for _, item := range req.Select.SelectItems {
if len(item.Segments) > 1 {
return []string{}, godata.NotImplementedError("multiple segments in $select not supported")
}
sel = append(sel, item.Segments[0].Value)
}
return sel, nil
}
// GetSearchValues extracts the value of the $search query parameter and returns
// it as a string. Rejects any search query that is more than just a simple string
func GetSearchValues(req *godata.GoDataQuery) (string, error) {
if req == nil || req.Search == nil {
return "", nil
}
// Only allow simple search queries for now
if len(req.Search.Tree.Children) != 0 {
return "", godata.NotImplementedError("complex search queries are not supported")
}
searchValue := strings.Trim(req.Search.Tree.Token.Value, "\"")
return searchValue, nil
}

View File

@@ -1,104 +0,0 @@
package odata
import (
"strings"
"github.com/CiscoM31/godata"
)
// GetExpandValues extracts the values of the $expand query parameter and
// returns them in a []string, rejecting any $expand value that consists of more
// than just a single path segment.
func GetExpandValues(req *godata.GoDataQuery) ([]string, error) {
if req == nil || req.Expand == nil {
return []string{}, nil
}
var expand []string
for _, item := range req.Expand.ExpandItems {
paths, err := collectExpandPaths(item, "")
if err != nil {
return nil, err
}
expand = append(expand, paths...)
}
return expand, nil
}
// collectExpandPaths recursively collects all valid expand paths from the given item.
func collectExpandPaths(item *godata.ExpandItem, prefix string) ([]string, error) {
if err := validateExpandItem(item); err != nil {
return nil, err
}
// Build the current path
currentPath := prefix
if len(item.Path) > 1 {
return nil, godata.NotImplementedError("multiple segments in $expand not supported")
}
if len(item.Path) == 1 {
if currentPath == "" {
currentPath = item.Path[0].Value
} else {
currentPath += "." + item.Path[0].Value
}
}
// Collect all paths, including nested ones
paths := []string{currentPath}
if item.Expand != nil {
for _, subItem := range item.Expand.ExpandItems {
subPaths, err := collectExpandPaths(subItem, currentPath)
if err != nil {
return nil, err
}
paths = append(paths, subPaths...)
}
}
return paths, nil
}
// validateExpandItem checks if an expand item contains unsupported options.
func validateExpandItem(item *godata.ExpandItem) error {
if item.Filter != nil || item.At != nil || item.Search != nil ||
item.OrderBy != nil || item.Skip != nil || item.Top != nil ||
item.Select != nil || item.Compute != nil || item.Levels != 0 {
return godata.NotImplementedError("options for $expand not supported")
}
return nil
}
// GetSelectValues extracts the values of the $select query parameter and
// returns them in a []string, rejects any $select value that consists of more
// than just a single path segment
func GetSelectValues(req *godata.GoDataQuery) ([]string, error) {
if req == nil || req.Select == nil {
return []string{}, nil
}
sel := make([]string, 0, len(req.Select.SelectItems))
for _, item := range req.Select.SelectItems {
if len(item.Segments) > 1 {
return []string{}, godata.NotImplementedError("multiple segments in $select not supported")
}
sel = append(sel, item.Segments[0].Value)
}
return sel, nil
}
// GetSearchValues extracts the value of the $search query parameter and returns
// it as a string. Rejects any search query that is more than just a simple string
func GetSearchValues(req *godata.GoDataQuery) (string, error) {
if req == nil || req.Search == nil {
return "", nil
}
// Only allow simple search queries for now
if len(req.Search.Tree.Children) != 0 {
return "", godata.NotImplementedError("complex search queries are not supported")
}
searchValue := strings.Trim(req.Search.Tree.Token.Value, "\"")
return searchValue, nil
}

View File

@@ -1,160 +0,0 @@
package odata
import (
"testing"
"github.com/CiscoM31/godata"
"github.com/stretchr/testify/assert"
)
func TestGetExpandValues(t *testing.T) {
t.Run("NilRequest", func(t *testing.T) {
result, err := GetExpandValues(nil)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("EmptyExpand", func(t *testing.T) {
req := &godata.GoDataQuery{}
result, err := GetExpandValues(req)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("SinglePathSegment", func(t *testing.T) {
req := &godata.GoDataQuery{
Expand: &godata.GoDataExpandQuery{
ExpandItems: []*godata.ExpandItem{
{Path: []*godata.Token{{Value: "orders"}}},
},
},
}
result, err := GetExpandValues(req)
assert.NoError(t, err)
assert.Equal(t, []string{"orders"}, result)
})
t.Run("MultiplePathSegments", func(t *testing.T) {
req := &godata.GoDataQuery{
Expand: &godata.GoDataExpandQuery{
ExpandItems: []*godata.ExpandItem{
{Path: []*godata.Token{{Value: "orders"}, {Value: "details"}}},
},
},
}
result, err := GetExpandValues(req)
assert.Error(t, err)
assert.Empty(t, result)
})
t.Run("NestedExpand", func(t *testing.T) {
req := &godata.GoDataQuery{
Expand: &godata.GoDataExpandQuery{
ExpandItems: []*godata.ExpandItem{
{
Path: []*godata.Token{{Value: "items"}},
Expand: &godata.GoDataExpandQuery{
ExpandItems: []*godata.ExpandItem{
{
Path: []*godata.Token{{Value: "subitem"}},
Expand: &godata.GoDataExpandQuery{
ExpandItems: []*godata.ExpandItem{
{Path: []*godata.Token{{Value: "subsubitems"}}},
},
},
},
},
},
},
},
},
}
result, err := GetExpandValues(req)
assert.NoError(t, err)
assert.Subset(t, result, []string{"items", "items.subitem", "items.subitem.subsubitems"}, "must contain all levels of expansion")
})
}
func TestGetSelectValues(t *testing.T) {
t.Run("NilRequest", func(t *testing.T) {
result, err := GetSelectValues(nil)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("EmptySelect", func(t *testing.T) {
req := &godata.GoDataQuery{}
result, err := GetSelectValues(req)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("SinglePathSegment", func(t *testing.T) {
req := &godata.GoDataQuery{
Select: &godata.GoDataSelectQuery{
SelectItems: []*godata.SelectItem{
{Segments: []*godata.Token{{Value: "name"}}},
},
},
}
result, err := GetSelectValues(req)
assert.NoError(t, err)
assert.Equal(t, []string{"name"}, result)
})
t.Run("MultiplePathSegments", func(t *testing.T) {
req := &godata.GoDataQuery{
Select: &godata.GoDataSelectQuery{
SelectItems: []*godata.SelectItem{
{Segments: []*godata.Token{{Value: "name"}, {Value: "first"}}},
},
},
}
result, err := GetSelectValues(req)
assert.Error(t, err)
assert.Empty(t, result)
})
}
func TestGetSearchValues(t *testing.T) {
t.Run("NilRequest", func(t *testing.T) {
result, err := GetSearchValues(nil)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("EmptySearch", func(t *testing.T) {
req := &godata.GoDataQuery{}
result, err := GetSearchValues(req)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("SimpleSearch", func(t *testing.T) {
req := &godata.GoDataQuery{
Search: &godata.GoDataSearchQuery{
Tree: &godata.ParseNode{
Token: &godata.Token{Value: "test"},
},
},
}
result, err := GetSearchValues(req)
assert.NoError(t, err)
assert.Equal(t, "test", result)
})
t.Run("ComplexSearch", func(t *testing.T) {
req := &godata.GoDataQuery{
Search: &godata.GoDataSearchQuery{
Tree: &godata.ParseNode{
Children: []*godata.ParseNode{
{Token: &godata.Token{Value: "test"}},
},
},
},
}
result, err := GetSearchValues(req)
assert.Error(t, err)
assert.Empty(t, result)
})
}

View File

@@ -1,8 +1,6 @@
package http
import (
"context"
"errors"
"fmt"
stdhttp "net/http"
@@ -10,7 +8,8 @@ import (
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
revaMetadata "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/pkg/errors"
"go-micro.dev/v4"
"go-micro.dev/v4/events"
@@ -21,7 +20,6 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/service/grpc"
"github.com/opencloud-eu/opencloud/pkg/service/http"
"github.com/opencloud-eu/opencloud/pkg/storage/metadata"
"github.com/opencloud-eu/opencloud/pkg/version"
ehsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/eventhistory/v0"
searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
@@ -61,7 +59,7 @@ func Server(opts ...Option) (http.Service, error) {
options.Logger.Error().
Err(err).
Msg("Error initializing events publisher")
return http.Service{}, fmt.Errorf("could not initialize events publisher: %w", err)
return http.Service{}, errors.Wrap(err, "could not initialize events publisher")
}
}
@@ -108,7 +106,7 @@ func Server(opts ...Option) (http.Service, error) {
pool.WithTracerProvider(options.TraceProvider),
)...)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize gateway selector: %w", err)
return http.Service{}, errors.Wrap(err, "could not initialize gateway selector")
}
} else {
middlewares = append(middlewares, graphMiddleware.Token(options.Config.HTTP.APIToken))
@@ -131,38 +129,21 @@ func Server(opts ...Option) (http.Service, error) {
hClient := ehsvc.NewEventHistoryService("eu.opencloud.api.eventhistory", grpcClient)
var userProfilePhotoService svc.UsersUserProfilePhotoProvider
{
photoStorage, err := revaMetadata.NewCS3Storage(
options.Config.Metadata.GatewayAddress,
options.Config.Metadata.StorageAddress,
options.Config.Metadata.SystemUserID,
options.Config.Metadata.SystemUserIDP,
options.Config.Metadata.SystemUserAPIKey,
)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize reva metadata storage: %w", err)
}
photoStorage, err = metadata.NewLazyStorage(photoStorage)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize lazy metadata storage: %w", err)
}
if err := photoStorage.Init(context.Background(), "f2bdd61a-da7c-49fc-8203-0558109d1b4f"); err != nil {
return http.Service{}, fmt.Errorf("could not initialize metadata storage: %w", err)
}
userProfilePhotoService, err = svc.NewUsersUserProfilePhotoService(photoStorage)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize user profile photo service: %w", err)
}
storage, err := metadata.NewCS3Storage(
options.Config.Metadata.GatewayAddress,
options.Config.Metadata.StorageAddress,
options.Config.Metadata.SystemUserID,
options.Config.Metadata.SystemUserIDP,
options.Config.Metadata.SystemUserAPIKey,
)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize metadata storage: %w", err)
}
var handle svc.Service
handle, err = svc.NewService(
svc.Context(options.Context),
svc.UserProfilePhotoService(userProfilePhotoService),
svc.MetadataStorage(storage),
svc.Logger(options.Logger),
svc.Config(options.Config),
svc.Middleware(middlewares...),
@@ -179,11 +160,11 @@ func Server(opts ...Option) (http.Service, error) {
)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize graph service: %w", err)
return http.Service{}, errors.New("could not initialize graph service")
}
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
return http.Service{}, fmt.Errorf("could not register graph service handler: %w", err)
return http.Service{}, err
}
return service, nil

View File

@@ -31,7 +31,6 @@ import (
"github.com/opencloud-eu/opencloud/pkg/l10n"
l10n_pkg "github.com/opencloud-eu/opencloud/services/graph/pkg/l10n"
"github.com/opencloud-eu/opencloud/services/graph/pkg/odata"
"github.com/opencloud-eu/opencloud/pkg/conversions"
"github.com/opencloud-eu/opencloud/pkg/log"
@@ -46,15 +45,14 @@ const (
invalidIdMsg = "invalid driveID or itemID"
parseDriveIDErrMsg = "could not parse driveID"
federatedRolesODataFilter = "@libre.graph.permissions.roles.allowedValues/rolePermissions/any(p:contains(p/condition, '@Subject.UserType==\"Federated\"'))"
noLinksODataFilter = "grantedToV2 ne ''"
)
// DriveItemPermissionsProvider contains the methods related to handling permissions on drive items
type DriveItemPermissionsProvider interface {
Invite(ctx context.Context, resourceId *storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error)
SpaceRootInvite(ctx context.Context, driveID *storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error)
ListPermissions(ctx context.Context, itemID *storageprovider.ResourceId, queryOptions ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)
ListSpaceRootPermissions(ctx context.Context, driveID *storageprovider.ResourceId, queryOptions ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)
ListPermissions(ctx context.Context, itemID *storageprovider.ResourceId, listFederatedRoles bool, selectedAttrs map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error)
ListSpaceRootPermissions(ctx context.Context, driveID *storageprovider.ResourceId, selectedAttrs map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error)
DeletePermission(ctx context.Context, itemID *storageprovider.ResourceId, permissionID string) error
DeleteSpaceRootPermission(ctx context.Context, driveID *storageprovider.ResourceId, permissionID string) error
UpdatePermission(ctx context.Context, itemID *storageprovider.ResourceId, permissionID string, newPermission libregraph.Permission) (libregraph.Permission, error)
@@ -80,14 +78,6 @@ const (
OCM
)
type ListPermissionsQueryOptions struct {
Count bool
NoValues bool
NoLinkPermissions bool
FilterFederatedRoles bool
SelectedAttrs []string
}
// NewDriveItemPermissionsService creates a new DriveItemPermissionsService
func NewDriveItemPermissionsService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], identityCache identity.IdentityCache, config *config.Config) (DriveItemPermissionsService, error) {
return DriveItemPermissionsService{
@@ -354,7 +344,7 @@ func (s DriveItemPermissionsService) SpaceRootInvite(ctx context.Context, driveI
}
// ListPermissions lists the permissions of a driveItem
func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID *storageprovider.ResourceId, queryOptions ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID *storageprovider.ResourceId, listFederatedRoles bool, selectedAttrs map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
collectionOfPermissions := libregraph.CollectionOfPermissionsWithAllowedValues{}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
@@ -377,17 +367,17 @@ func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID
collectionOfPermissions = libregraph.CollectionOfPermissionsWithAllowedValues{}
if len(queryOptions.SelectedAttrs) == 0 || slices.Contains(queryOptions.SelectedAttrs, "@libre.graph.permissions.actions.allowedValues") {
if _, ok := selectedAttrs["@libre.graph.permissions.actions.allowedValues"]; ok || len(selectedAttrs) == 0 {
collectionOfPermissions.LibreGraphPermissionsActionsAllowedValues = allowedActions
}
if len(queryOptions.SelectedAttrs) == 0 || slices.Contains(queryOptions.SelectedAttrs, "@libre.graph.permissions.roles.allowedValues") {
if _, ok := selectedAttrs["@libre.graph.permissions.roles.allowedValues"]; ok || len(selectedAttrs) == 0 {
collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues = conversions.ToValueSlice(
unifiedrole.GetRolesByPermissions(
unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(s.config.UnifiedRoles.AvailableRoles...)),
allowedActions,
condition,
queryOptions.FilterFederatedRoles,
listFederatedRoles,
false,
),
)
@@ -399,7 +389,7 @@ func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID
collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues[i] = definition
}
if len(queryOptions.SelectedAttrs) > 0 {
if len(selectedAttrs) > 0 {
// no need to fetch shares, we are only interested allowedActions and/or allowedRoles
return collectionOfPermissions, nil
}
@@ -412,11 +402,8 @@ func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID
}
driveItems[storagespace.FormatResourceID(statResponse.GetInfo().GetId())] = *item
var permissionsCount int
if IsSpaceRoot(statResponse.GetInfo().GetId()) {
var permissions []libregraph.Permission
permissions, permissionsCount, err = s.getSpaceRootPermissions(ctx, statResponse.GetInfo().GetSpace().GetId(), queryOptions.NoValues)
permissions, err := s.getSpaceRootPermissions(ctx, statResponse.GetInfo().GetSpace().GetId())
if err != nil {
return collectionOfPermissions, err
}
@@ -441,33 +428,23 @@ func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID
}
}
}
if !queryOptions.NoLinkPermissions {
// finally get public shares, which are possible for spaceroots and "normal" resources
driveItems, err = s.listPublicShares(ctx, []*link.ListPublicSharesRequest_Filter{
publicshare.ResourceIDFilter(itemID),
}, driveItems)
if err != nil {
return collectionOfPermissions, err
}
// finally get public shares, which are possible for spaceroots and "normal" resources
driveItems, err = s.listPublicShares(ctx, []*link.ListPublicSharesRequest_Filter{
publicshare.ResourceIDFilter(itemID),
}, driveItems)
if err != nil {
return collectionOfPermissions, err
}
for _, driveItem := range driveItems {
permissionsCount += len(driveItem.Permissions)
if !queryOptions.NoValues {
collectionOfPermissions.Value = append(collectionOfPermissions.Value, driveItem.Permissions...)
}
}
if queryOptions.Count {
collectionOfPermissions.SetOdataCount(int32(permissionsCount))
collectionOfPermissions.Value = append(collectionOfPermissions.Value, driveItem.Permissions...)
}
return collectionOfPermissions, nil
}
// ListSpaceRootPermissions handles ListPermissions request on project spaces
func (s DriveItemPermissionsService) ListSpaceRootPermissions(ctx context.Context, driveID *storageprovider.ResourceId, queryOptions ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
func (s DriveItemPermissionsService) ListSpaceRootPermissions(ctx context.Context, driveID *storageprovider.ResourceId, selectedAttrs map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
collectionOfPermissions := libregraph.CollectionOfPermissionsWithAllowedValues{}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
@@ -485,7 +462,7 @@ func (s DriveItemPermissionsService) ListSpaceRootPermissions(ctx context.Contex
}
rootResourceID := space.GetRoot()
return s.ListPermissions(ctx, rootResourceID, queryOptions) // federated roles are not supported for spaces
return s.ListPermissions(ctx, rootResourceID, false, selectedAttrs) // federated roles are not supported for spaces
}
// DeletePermission deletes a permission from a drive item
@@ -738,17 +715,23 @@ func (api DriveItemPermissionsApi) ListPermissions(w http.ResponseWriter, r *htt
return
}
var queryOptions ListPermissionsQueryOptions
queryOptions, err = api.getListPermissionsQueryOptions(odataReq)
var listFederatedRoles bool
if odataReq.Query.Filter != nil {
if odataReq.Query.Filter.RawValue == federatedRolesODataFilter {
listFederatedRoles = true
}
}
selectRoles, err := api.listPermissionsQuerySelectValues(odataReq.Query)
if err != nil {
api.logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("Error parsing ListPermissionRequest query options")
api.logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("Error parsing ListPermissionRequest: query error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
ctx := r.Context()
permissions, err := api.driveItemPermissionsService.ListPermissions(ctx, itemID, queryOptions)
permissions, err := api.driveItemPermissionsService.ListPermissions(ctx, itemID, listFederatedRoles, selectRoles)
if err != nil {
errorcode.RenderError(w, r, err)
return
@@ -789,16 +772,15 @@ func (api DriveItemPermissionsApi) ListSpaceRootPermissions(w http.ResponseWrite
return
}
var queryOptions ListPermissionsQueryOptions
queryOptions, err = api.getListPermissionsQueryOptions(odataReq)
selected, err := api.listPermissionsQuerySelectValues(odataReq.Query)
if err != nil {
api.logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("Error parsing ListPermissionRequest query options")
api.logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("Error parsing ListPermissionRequest: query error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
ctx := r.Context()
permissions, err := api.driveItemPermissionsService.ListSpaceRootPermissions(ctx, &driveID, queryOptions)
permissions, err := api.driveItemPermissionsService.ListSpaceRootPermissions(ctx, &driveID, selected)
if err != nil {
errorcode.RenderError(w, r, err)
@@ -823,42 +805,6 @@ func (api DriveItemPermissionsApi) ListSpaceRootPermissions(w http.ResponseWrite
render.JSON(w, r, permissions)
}
func (api DriveItemPermissionsApi) getListPermissionsQueryOptions(odataReq *godata.GoDataRequest) (ListPermissionsQueryOptions, error) {
queryOptions := ListPermissionsQueryOptions{}
if odataReq.Query.Filter != nil {
switch odataReq.Query.Filter.RawValue {
case federatedRolesODataFilter:
queryOptions.FilterFederatedRoles = true
case noLinksODataFilter:
queryOptions.NoLinkPermissions = true
default:
return ListPermissionsQueryOptions{}, errorcode.New(errorcode.InvalidRequest, "invalid filter value")
}
}
selectAttrs, err := odata.GetSelectValues(odataReq.Query)
if err != nil {
return ListPermissionsQueryOptions{}, err
}
queryOptions.SelectedAttrs = selectAttrs
if odataReq.Query.Count != nil {
queryOptions.Count = bool(*odataReq.Query.Count)
}
if odataReq.Query.Top != nil {
top := int(*odataReq.Query.Top)
switch {
case top != 0:
return ListPermissionsQueryOptions{}, err
case top == 0 && !queryOptions.Count:
return ListPermissionsQueryOptions{}, err
default:
queryOptions.NoValues = true
}
}
return queryOptions, nil
}
// DeletePermission handles DeletePermission requests
func (api DriveItemPermissionsApi) DeletePermission(w http.ResponseWriter, r *http.Request) {
_, itemID, err := GetDriveAndItemIDParam(r, &api.logger)
@@ -990,3 +936,19 @@ func (api DriveItemPermissionsApi) UpdateSpaceRootPermission(w http.ResponseWrit
render.Status(r, http.StatusOK)
render.JSON(w, r, &updatedPermission)
}
func (api DriveItemPermissionsApi) listPermissionsQuerySelectValues(odataQuery *godata.GoDataQuery) (map[string]struct{}, error) {
selectedAttrs := map[string]struct{}{}
if odataQuery.Select != nil {
for _, item := range odataQuery.Select.SelectItems {
// for now we only support a limitted set of $select attributes
if item.Segments[0].Value == "@libre.graph.permissions.roles.allowedValues" || item.Segments[0].Value == "@libre.graph.permissions.actions.allowedValues" {
selectedAttrs[item.Segments[0].Value] = struct{}{}
} else {
api.logger.Debug().Msg("Error parsing ListPermissionRequest: unsupported select item")
return selectedAttrs, errorcode.New(errorcode.InvalidRequest, "unsupported select item")
}
}
}
return selectedAttrs, nil
}

View File

@@ -385,7 +385,7 @@ var _ = Describe("DriveItemPermissionsService", func() {
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(listSharesResponse, nil)
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID, svc.ListPermissionsQueryOptions{})
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID, false, map[string]struct{}{})
Expect(err).ToNot(HaveOccurred())
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
Expect(len(permissions.LibreGraphPermissionsRolesAllowedValues)).ToNot(BeZero())
@@ -433,7 +433,7 @@ var _ = Describe("DriveItemPermissionsService", func() {
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(listSharesResponse, nil)
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID, svc.ListPermissionsQueryOptions{})
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID, false, map[string]struct{}{})
Expect(err).ToNot(HaveOccurred())
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
Expect(len(permissions.LibreGraphPermissionsRolesAllowedValues)).ToNot(BeZero())
@@ -472,7 +472,7 @@ var _ = Describe("DriveItemPermissionsService", func() {
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(listSharesResponse, nil)
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
permissions, err := service.ListPermissions(context.Background(), itemID, svc.ListPermissionsQueryOptions{})
permissions, err := service.ListPermissions(context.Background(), itemID, false, map[string]struct{}{})
Expect(err).ToNot(HaveOccurred())
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
Expect(len(permissions.LibreGraphPermissionsRolesAllowedValues)).ToNot(BeZero())
@@ -508,126 +508,13 @@ var _ = Describe("DriveItemPermissionsService", func() {
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(listSharesResponse, nil)
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID, svc.ListPermissionsQueryOptions{})
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID, false, map[string]struct{}{})
Expect(err).ToNot(HaveOccurred())
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
Expect(len(permissions.LibreGraphPermissionsRolesAllowedValues)).ToNot(BeZero())
Expect(len(permissions.Value)).To(Equal(1))
Expect(permissions.Value[0].GetLibreGraphPermissionsActions()[0]).To(Equal("none"))
})
It("Does not list public shares when requested so", func() {
opt := svc.ListPermissionsQueryOptions{
NoLinkPermissions: true,
}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(listSharesResponse, nil)
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID, opt)
Expect(err).ToNot(HaveOccurred())
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
Expect(len(permissions.LibreGraphPermissionsRolesAllowedValues)).ToNot(BeZero())
})
It("Does not return permissions when the NoValues option is set", func() {
opt := svc.ListPermissionsQueryOptions{
NoValues: true,
}
listSharesResponse.Shares = []*collaboration.Share{
{
Id: &collaboration.ShareId{OpaqueId: "1"},
Permissions: &collaboration.SharePermissions{
Permissions: roleconversions.NewViewerRole().CS3ResourcePermissions(),
},
ResourceId: &provider.ResourceId{
StorageId: "1",
SpaceId: "2",
OpaqueId: "3",
},
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{
UserId: &userpb.UserId{
OpaqueId: "user-id",
},
},
},
},
}
listPublicSharesResponse.Share = []*link.PublicShare{
{
Id: &link.PublicShareId{
OpaqueId: "public-share-id",
},
Token: "public-share-token",
// the link shares the same resource id
ResourceId: &provider.ResourceId{
StorageId: "1",
SpaceId: "2",
OpaqueId: "3",
},
Permissions: &link.PublicSharePermissions{Permissions: roleconversions.NewViewerRole().CS3ResourcePermissions()},
},
}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(listSharesResponse, nil)
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID, opt)
Expect(err).ToNot(HaveOccurred())
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
Expect(len(permissions.LibreGraphPermissionsRolesAllowedValues)).ToNot(BeZero())
Expect(len(permissions.Value)).To(BeZero())
})
It("Returns a count when the Count option is set", func() {
opt := svc.ListPermissionsQueryOptions{
Count: true,
}
listSharesResponse.Shares = []*collaboration.Share{
{
Id: &collaboration.ShareId{OpaqueId: "1"},
Permissions: &collaboration.SharePermissions{
Permissions: roleconversions.NewViewerRole().CS3ResourcePermissions(),
},
ResourceId: &provider.ResourceId{
StorageId: "1",
SpaceId: "2",
OpaqueId: "3",
},
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{
UserId: &userpb.UserId{
OpaqueId: "user-id",
},
},
},
},
}
listPublicSharesResponse.Share = []*link.PublicShare{
{
Id: &link.PublicShareId{
OpaqueId: "public-share-id",
},
Token: "public-share-token",
// the link shares the same resource id
ResourceId: &provider.ResourceId{
StorageId: "1",
SpaceId: "2",
OpaqueId: "3",
},
Permissions: &link.PublicSharePermissions{Permissions: roleconversions.NewViewerRole().CS3ResourcePermissions()},
},
}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(listSharesResponse, nil)
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
permissions, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID, opt)
Expect(err).ToNot(HaveOccurred())
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
Expect(len(permissions.LibreGraphPermissionsRolesAllowedValues)).ToNot(BeZero())
count := int(permissions.GetOdataCount())
Expect(count).To(Equal(2)) // 1 share + 1 public share
Expect(len(permissions.Value)).To(Equal(count))
})
})
Describe("ListSpaceRootPermissions", func() {
var (
@@ -668,7 +555,7 @@ var _ = Describe("DriveItemPermissionsService", func() {
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
statResponse.Info.Id = listSpacesResponse.StorageSpaces[0].Root
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
permissions, err := driveItemPermissionsService.ListSpaceRootPermissions(context.Background(), driveId, svc.ListPermissionsQueryOptions{})
permissions, err := driveItemPermissionsService.ListSpaceRootPermissions(context.Background(), driveId, map[string]struct{}{})
Expect(err).ToNot(HaveOccurred())
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
})
@@ -1381,7 +1268,8 @@ var _ = Describe("DriveItemPermissionsApi", func() {
Expect(err).ToNot(HaveOccurred())
mockProvider.On("ListPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(func(ctx context.Context, itemid *provider.ResourceId, opt svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
Return(func(ctx context.Context, itemid *provider.ResourceId, listFederatedRoles bool, selected map[string]struct{}) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
Expect(listFederatedRoles).To(Equal(false))
Expect(storagespace.FormatResourceID(itemid)).To(Equal("1$2!3"))
return libregraph.CollectionOfPermissionsWithAllowedValues{}, nil
}).Once()

View File

@@ -30,6 +30,9 @@ type (
)
var (
// profilePhotoSpaceID is the space ID for the profile photo
profilePhotoSpaceID = "f2bdd61a-da7c-49fc-8203-0558109d1b4f"
// ErrNoBytes is returned when no bytes are found
ErrNoBytes = errors.New("no bytes")
@@ -47,6 +50,10 @@ type UsersUserProfilePhotoService struct {
// NewUsersUserProfilePhotoService creates a new UsersUserProfilePhotoService
func NewUsersUserProfilePhotoService(storage metadata.Storage) (UsersUserProfilePhotoService, error) {
if err := storage.Init(context.Background(), profilePhotoSpaceID); err != nil {
return UsersUserProfilePhotoService{}, err
}
return UsersUserProfilePhotoService{
storage: storage,
}, nil

View File

@@ -19,7 +19,10 @@ import (
)
func TestNewUsersUserProfilePhotoService(t *testing.T) {
service, err := svc.NewUsersUserProfilePhotoService(mocks.NewStorage(t))
storage := mocks.NewStorage(t)
storage.EXPECT().Init(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, id string) error { return nil })
service, err := svc.NewUsersUserProfilePhotoService(storage)
assert.NoError(t, err)
t.Run("UpdatePhoto", func(t *testing.T) {

View File

@@ -71,9 +71,13 @@ var _ = Describe("Applications", func() {
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
cfg.Application.ID = "some-application-ID"
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -81,9 +81,13 @@ var _ = Describe("AppRoleAssignments", func() {
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
cfg.Application.ID = "some-application-ID"
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -49,20 +49,19 @@ type BaseGraphService struct {
availableRoles []*libregraph.UnifiedRoleDefinition
}
func (g BaseGraphService) getSpaceRootPermissions(ctx context.Context, spaceID *storageprovider.StorageSpaceId, countOnly bool) ([]libregraph.Permission, int, error) {
func (g BaseGraphService) getSpaceRootPermissions(ctx context.Context, spaceID *storageprovider.StorageSpaceId) ([]libregraph.Permission, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
return nil, 0, err
return nil, err
}
space, err := utils.GetSpace(ctx, spaceID.GetOpaqueId(), gatewayClient)
if err != nil {
return nil, 0, errorcode.FromUtilsStatusCodeError(err)
return nil, errorcode.FromUtilsStatusCodeError(err)
}
perm, count := g.cs3SpacePermissionsToLibreGraph(ctx, space, countOnly, APIVersion_1_Beta_1)
return perm, count, nil
return g.cs3SpacePermissionsToLibreGraph(ctx, space, APIVersion_1_Beta_1), nil
}
func (g BaseGraphService) getDriveItem(ctx context.Context, ref *storageprovider.Reference) (*libregraph.DriveItem, error) {
@@ -100,9 +99,9 @@ func (g BaseGraphService) CS3ReceivedOCMSharesToDriveItems(ctx context.Context,
return cs3ReceivedOCMSharesToDriveItems(ctx, g.logger, gatewayClient, g.identityCache, receivedShares, g.availableRoles)
}
func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, space *storageprovider.StorageSpace, countOnly bool, apiVersion APIVersion) ([]libregraph.Permission, int) {
func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, space *storageprovider.StorageSpace, apiVersion APIVersion) []libregraph.Permission {
if space.Opaque == nil {
return nil, 0
return nil
}
logger := g.logger.SubloggerWithRequestID(ctx)
@@ -119,12 +118,7 @@ func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, s
}
}
if len(permissionsMap) == 0 {
return nil, 0
}
if countOnly {
// If we only need the count, we can return early
return nil, len(permissionsMap)
return nil
}
var permissionsExpirations map[string]*types.Timestamp
@@ -225,7 +219,7 @@ func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, s
permissions = append(permissions, p)
}
return permissions, len(permissions)
return permissions
}
func (g BaseGraphService) libreGraphPermissionFromCS3PublicShare(createdLink *link.PublicShare) (*libregraph.Permission, error) {
@@ -1074,7 +1068,7 @@ func (g BaseGraphService) getPermissionByID(ctx context.Context, permissionID st
return nil, nil, err
}
perms, _, err := g.getSpaceRootPermissions(ctx, resourceInfo.GetSpace().GetId(), false)
perms, err := g.getSpaceRootPermissions(ctx, resourceInfo.GetSpace().GetId())
if err != nil {
return nil, nil, err
}

View File

@@ -85,9 +85,13 @@ var _ = Describe("Driveitems", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -8,7 +8,6 @@ import (
"net/http"
"net/url"
"path"
"slices"
"sort"
"strconv"
"strings"
@@ -20,10 +19,10 @@ import (
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/pkg/errors"
merrors "go-micro.dev/v4/errors"
"golang.org/x/sync/errgroup"
@@ -33,7 +32,6 @@ import (
v0 "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/settings/v0"
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
"github.com/opencloud-eu/opencloud/services/graph/pkg/odata"
settingsServiceExt "github.com/opencloud-eu/opencloud/services/settings/pkg/store/defaults"
)
@@ -170,60 +168,31 @@ func (g Graph) GetAllDrivesV1Beta1(w http.ResponseWriter, r *http.Request) {
}
}
func sanitizePath(path string, apiVersion APIVersion) string {
switch apiVersion {
case APIVersion_1:
return strings.TrimPrefix(path, "/graph/v1.0/")
case APIVersion_1_Beta_1:
return strings.TrimPrefix(path, "/graph/v1beta1/")
default:
return path
}
}
// parseDriveRequest parses the odata request and returns the parsed request and a boolean indicating if the request should expand root driveItems.
func parseDriveRequest(r *http.Request) (*godata.GoDataRequest, bool, error) {
odataReq, err := godata.ParseRequest(r.Context(), sanitizePath(r.URL.Path, APIVersion_1), r.URL.Query())
if err != nil {
return nil, false, errorcode.New(errorcode.InvalidRequest, err.Error())
}
exp, err := odata.GetExpandValues(odataReq.Query)
if err != nil {
return nil, false, errorcode.New(errorcode.InvalidRequest, err.Error())
}
expandPermissions := slices.Contains(exp, "root.permissions")
return odataReq, expandPermissions, nil
}
// getDrives implements the Service interface.
func (g Graph) getDrives(r *http.Request, unrestricted bool, apiVersion APIVersion) ([]*libregraph.Drive, error) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().
Interface("query", r.URL.Query()).
Bool("unrestricted", unrestricted).
Msg("calling get drives")
sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
// Parse the request with odata parser
odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get drives: query error")
return nil, errorcode.New(errorcode.InvalidRequest, err.Error())
}
ctx := r.Context()
log := g.logger.SubloggerWithRequestID(ctx).With().Interface("query", r.URL.Query()).Bool("unrestricted", unrestricted).Logger()
log.Debug().Msg("calling get drives")
webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
log.Error().Err(err).Msg("could not get drives: error parsing url")
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
log = log.With().Str("url", webDavBaseURL.String()).Logger()
odataReq, expandPermissions, err := parseDriveRequest(r)
if err != nil {
log.Debug().Err(err).Msg("could not get drives: error parsing odata request")
return nil, err
}
filters, err := generateCs3Filters(odataReq)
if err != nil {
log.Debug().Err(err).Msg("could not get drives: error parsing filters")
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get drives: error parsing filters")
return nil, errorcode.New(errorcode.NotSupported, err.Error())
}
if !unrestricted {
user, ok := revactx.ContextGetUser(r.Context())
if !ok {
log.Debug().Msg("could not create drive: invalid user")
logger.Debug().Msg("could not create drive: invalid user")
return nil, errorcode.New(errorcode.AccessDenied, "invalid user")
}
filters = append(filters, &storageprovider.ListStorageSpacesRequest_Filter{
@@ -234,32 +203,39 @@ func (g Graph) getDrives(r *http.Request, unrestricted bool, apiVersion APIVersi
})
}
log.Debug().
logger.Debug().
Interface("filters", filters).
Bool("unrestricted", unrestricted).
Msg("calling list storage spaces on backend")
res, err := g.ListStorageSpacesWithFilters(ctx, filters, unrestricted)
switch {
case err != nil:
log.Error().Err(err).Msg("could not get drives: transport error")
logger.Error().Err(err).Msg("could not get drives: transport error")
return nil, errorcode.New(errorcode.GeneralException, err.Error())
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
// ok, empty return
return nil, nil
}
log.Debug().Str("message", res.GetStatus().GetMessage()).Msg("could not get drives: grpc error")
logger.Debug().Str("message", res.GetStatus().GetMessage()).Msg("could not get drives: grpc error")
return nil, errorcode.New(errorcode.GeneralException, res.Status.Message)
}
spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces, apiVersion, expandPermissions)
webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
log.Debug().Err(err).Msg("could not get drives: error parsing grpc response")
logger.Error().Err(err).Str("url", webDavBaseURL.String()).Msg("could not get drives: error parsing url")
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces, apiVersion)
if err != nil {
logger.Debug().Err(err).Msg("could not get drives: error parsing grpc response")
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
spaces, err = sortSpaces(odataReq, spaces)
if err != nil {
log.Debug().Err(err).Msg("could not get drives: error sorting the spaces list according to query")
logger.Debug().Err(err).Msg("could not get drives: error sorting the spaces list according to query")
return nil, errorcode.New(errorcode.InvalidRequest, err.Error())
}
@@ -269,32 +245,15 @@ func (g Graph) getDrives(r *http.Request, unrestricted bool, apiVersion APIVersi
// GetSingleDrive does a lookup of a single space by spaceId
func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := g.logger.SubloggerWithRequestID(ctx).With().Interface("query", r.URL.Query()).Logger()
log.Debug().Msg("calling get drive")
logger := g.logger.SubloggerWithRequestID(ctx)
logger.Info().Interface("query", r.URL.Query()).Msg("calling get drive")
rid, err := parseIDParam(r, "driveID")
if err != nil {
errorcode.RenderError(w, r, err)
return
}
log = log.With().Str("storage", rid.StorageId).Str("space", rid.SpaceId).Str("node", rid.OpaqueId).Logger()
webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
log.Error().Err(err).Msg("could not get drive: error parsing webdav base url")
errorcode.RenderError(w, r, err)
return
}
log = log.With().Str("url", webDavBaseURL.String()).Logger()
_, expandPermissions, err := parseDriveRequest(r)
if err != nil {
log.Debug().Err(err).Msg("could not get drives: error parsing odata request")
errorcode.RenderError(w, r, err)
return
}
log := logger.With().Str("storage", rid.StorageId).Str("space", rid.SpaceId).Str("node", rid.OpaqueId).Logger()
log.Debug().Msg("calling list storage spaces with id filter")
@@ -322,7 +281,13 @@ func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) {
return
}
spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces, APIVersion_1, expandPermissions)
webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
log.Error().Err(err).Str("url", webDavBaseURL.String()).Msg("could not get drive: error parsing webdav base url")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces, APIVersion_1)
if err != nil {
log.Debug().Err(err).Msg("could not get drive: error parsing grpc response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
@@ -359,22 +324,14 @@ func (g Graph) canCreateSpace(ctx context.Context, ownPersonalHome bool) bool {
// CreateDrive creates a storage drive (space).
func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling create drive")
ctx := r.Context()
log := g.logger.SubloggerWithRequestID(ctx).With().Interface("query", r.URL.Query()).Logger()
log.Debug().Msg("calling create drive")
webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
log.Error().Err(err).Msg("could not create drive: error parsing webdav base url")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
log = log.With().Str("url", webDavBaseURL.String()).Logger()
us, ok := revactx.ContextGetUser(ctx)
if !ok {
log.Debug().Msg("could not create drive: invalid user")
logger.Debug().Msg("could not create drive: invalid user")
errorcode.NotAllowed.Render(w, r, http.StatusUnauthorized, "invalid user")
return
}
@@ -382,7 +339,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
// TODO determine if the user tries to create his own personal space and pass that as a boolean
canCreateSpace := g.canCreateSpace(ctx, false)
if !canCreateSpace {
log.Debug().Bool("cancreatespace", canCreateSpace).Msg("could not create drive: insufficient permissions")
logger.Debug().Bool("cancreatespace", canCreateSpace).Msg("could not create drive: insufficient permissions")
// if the permission is not existing for the user in context we can assume we don't have it. Return 401.
errorcode.NotAllowed.Render(w, r, http.StatusForbidden, "insufficient permissions to create a space.")
return
@@ -390,20 +347,20 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
log.Error().Err(err).Msg("could not select next gateway client")
logger.Error().Err(err).Msg("could not select next gateway client")
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "could not select next gateway client, aborting")
return
}
drive := libregraph.Drive{}
if err := StrictJSONUnmarshal(r.Body, &drive); err != nil {
log.Debug().Err(err).Interface("body", r.Body).Msg("could not create drive: invalid body schema definition")
logger.Debug().Err(err).Interface("body", r.Body).Msg("could not create drive: invalid body schema definition")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid body schema definition")
return
}
spaceName := strings.TrimSpace(drive.Name)
if err := validateSpaceName(spaceName); err != nil {
log.Debug().Str("name", spaceName).Err(err).Msg("could not create drive: name validation failed")
logger.Debug().Str("name", spaceName).Err(err).Msg("could not create drive: name validation failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid spacename: %s", err.Error()))
return
}
@@ -416,7 +373,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
case "", _spaceTypeProject:
driveType = _spaceTypeProject
default:
log.Debug().Str("type", driveType).Msg("could not create drive: drives of this type cannot be created via this api")
logger.Debug().Str("type", driveType).Msg("could not create drive: drives of this type cannot be created via this api")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "drives of this type cannot be created via this api")
return
}
@@ -441,32 +398,39 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
resp, err := gatewayClient.CreateStorageSpace(ctx, &csr)
if err != nil {
log.Error().Err(err).Msg("could not create drive: transport error")
logger.Error().Err(err).Msg("could not create drive: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
if resp.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
if resp.GetStatus().GetCode() == cs3rpc.Code_CODE_PERMISSION_DENIED {
log.Debug().Str("grpcmessage", resp.GetStatus().GetMessage()).Msg("could not create drive: permission denied")
logger.Debug().Str("grpcmessage", resp.GetStatus().GetMessage()).Msg("could not create drive: permission denied")
errorcode.NotAllowed.Render(w, r, http.StatusForbidden, "permission denied")
return
}
if resp.GetStatus().GetCode() == cs3rpc.Code_CODE_INVALID_ARGUMENT {
log.Debug().Str("grpcmessage", resp.GetStatus().GetMessage()).Msg("could not create drive: bad request")
logger.Debug().Str("grpcmessage", resp.GetStatus().GetMessage()).Msg("could not create drive: bad request")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, resp.GetStatus().GetMessage())
return
}
log.Debug().Interface("grpcmessage", csr).Str("grpc", resp.GetStatus().GetMessage()).Msg("could not create drive: grpc error")
logger.Debug().Interface("grpcmessage", csr).Str("grpc", resp.GetStatus().GetMessage()).Msg("could not create drive: grpc error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, resp.GetStatus().GetMessage())
return
}
webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
logger.Error().Str("url", webDavBaseURL.String()).Err(err).Msg("could not create drive: error parsing webdav base url")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
space := resp.GetStorageSpace()
if t := r.URL.Query().Get(TemplateParameter); t != "" && driveType == _spaceTypeProject {
loc := l10n.MustGetUserLocale(ctx, us.GetId().GetOpaqueId(), r.Header.Get(HeaderAcceptLanguage), g.valueService)
if err := g.applySpaceTemplate(ctx, gatewayClient, space.GetRoot(), t, loc); err != nil {
log.Error().Err(err).Msg("could not apply template to space")
logger.Error().Err(err).Msg("could not apply template to space")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
@@ -474,20 +438,20 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
// refetch the drive to get quota information - should we calculate this ourselves to avoid the extra call?
space, err = utils.GetSpace(ctx, space.GetId().GetOpaqueId(), gatewayClient)
if err != nil {
log.Error().Err(err).Msg("could not refetch space")
logger.Error().Err(err).Msg("could not refetch space")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
}
spaces, err := g.formatDrives(ctx, webDavBaseURL, []*storageprovider.StorageSpace{space}, APIVersion_1, false)
spaces, err := g.formatDrives(ctx, webDavBaseURL, []*storageprovider.StorageSpace{space}, APIVersion_1)
if err != nil {
log.Debug().Err(err).Msg("could not get drive: error parsing grpc response")
logger.Debug().Err(err).Msg("could not get drive: error parsing grpc response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
if len(spaces) == 0 {
log.Error().Msg("could not convert space")
logger.Error().Msg("could not convert space")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not convert space")
return
}
@@ -498,8 +462,8 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
// UpdateDrive updates the properties of a storage drive (space).
func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
log := g.logger.SubloggerWithRequestID(r.Context()).With().Interface("query", r.URL.Query()).Logger()
log.Debug().Msg("calling update drive")
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling update drive")
rid, err := parseIDParam(r, "driveID")
if err != nil {
@@ -507,20 +471,9 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
return
}
log = log.With().Str("storage", rid.StorageId).Str("space", rid.SpaceId).Str("node", rid.OpaqueId).Logger()
webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
log.Error().Err(err).Interface("url", webDavBaseURL.String()).Msg("could not update drive: error parsing url")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
log = log.With().Str("url", webDavBaseURL.String()).Logger()
drive := libregraph.DriveUpdate{}
if err = StrictJSONUnmarshal(r.Body, &drive); err != nil {
log.Debug().Err(err).Interface("body", r.Body).Msg("could not update drive, invalid request body")
logger.Debug().Err(err).Interface("body", r.Body).Msg("could not update drive, invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: error: %v", err.Error()))
return
}
@@ -573,7 +526,7 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
if drive.GetName() != "" {
spacename := strings.TrimSpace(drive.GetName())
if err := validateSpaceName(spacename); err != nil {
log.Info().Err(err).Msg("could not update drive: spacename invalid")
logger.Info().Err(err).Msg("could not update drive: spacename invalid")
errorcode.GeneralException.Render(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -599,12 +552,12 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
canSetSpaceQuota, err := g.canSetSpaceQuota(r.Context(), user, dt)
if err != nil {
log.Error().Err(err).Msg("could not update drive: failed to check if the user can set space quota")
logger.Error().Err(err).Msg("could not update drive: failed to check if the user can set space quota")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
if !canSetSpaceQuota {
log.Debug().
logger.Debug().
Bool("cansetspacequota", canSetSpaceQuota).
Msg("could not update drive: user is not allowed to set the space quota")
errorcode.NotAllowed.Render(w, r, http.StatusForbidden, "user is not allowed to set the space quota")
@@ -615,10 +568,10 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
}
}
log.Debug().Interface("payload", updateSpaceRequest).Msg("calling update space on backend")
logger.Debug().Interface("payload", updateSpaceRequest).Msg("calling update space on backend")
resp, err := gatewayClient.UpdateStorageSpace(r.Context(), updateSpaceRequest)
if err != nil {
log.Error().Err(err).Msg("could not update drive: transport error")
logger.Error().Err(err).Msg("could not update drive: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "transport error")
return
}
@@ -626,31 +579,38 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
if resp.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
switch resp.Status.GetCode() {
case cs3rpc.Code_CODE_NOT_FOUND:
log.Debug().Msg("could not update drive: drive not found")
logger.Debug().Interface("id", rid).Msg("could not update drive: drive not found")
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "drive not found")
return
case cs3rpc.Code_CODE_PERMISSION_DENIED:
log.Debug().Msg("could not update drive, permission denied")
logger.Debug().Interface("id", rid).Msg("could not update drive, permission denied")
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "drive not found")
return
case cs3rpc.Code_CODE_INVALID_ARGUMENT:
log.Debug().Msg("could not update drive, invalid argument")
logger.Debug().Interface("id", rid).Msg("could not update drive, invalid argument")
errorcode.NotAllowed.Render(w, r, http.StatusBadRequest, resp.GetStatus().GetMessage())
return
case cs3rpc.Code_CODE_UNIMPLEMENTED:
log.Debug().Msg("could not delete drive: delete not implemented for this type of drive")
logger.Debug().Interface("id", rid).Msg("could not delete drive: delete not implemented for this type of drive")
errorcode.NotAllowed.Render(w, r, http.StatusMethodNotAllowed, "drive cannot be updated")
return
default:
log.Debug().Str("grpc", resp.GetStatus().GetMessage()).Msg("could not update drive: grpc error")
logger.Debug().Interface("id", rid).Str("grpc", resp.GetStatus().GetMessage()).Msg("could not update drive: grpc error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "grpc error")
return
}
}
spaces, err := g.formatDrives(r.Context(), webDavBaseURL, []*storageprovider.StorageSpace{resp.StorageSpace}, APIVersion_1, false)
webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
log.Debug().Err(err).Msg("could not update drive: error parsing grpc response")
logger.Error().Err(err).Interface("url", webDavBaseURL.String()).Msg("could not update drive: error parsing url")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
spaces, err := g.formatDrives(r.Context(), webDavBaseURL, []*storageprovider.StorageSpace{resp.StorageSpace}, APIVersion_1)
if err != nil {
logger.Debug().Err(err).Msg("could not update drive: error parsing grpc response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
@@ -659,7 +619,7 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, spaces[0])
}
func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, storageSpaces []*storageprovider.StorageSpace, apiVersion APIVersion, expandPermissions bool) ([]*libregraph.Drive, error) {
func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, storageSpaces []*storageprovider.StorageSpace, apiVersion APIVersion) ([]*libregraph.Drive, error) {
errg, ctx := errgroup.WithContext(ctx)
work := make(chan *storageprovider.StorageSpace, len(storageSpaces))
results := make(chan *libregraph.Drive, len(storageSpaces))
@@ -689,7 +649,7 @@ func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, storageSpaces
// skip OCM shares they are no supposed to show up in the drives list
continue
}
res, err := g.cs3StorageSpaceToDrive(ctx, baseURL, storageSpace, apiVersion, expandPermissions)
res, err := g.cs3StorageSpaceToDrive(ctx, baseURL, storageSpace, apiVersion)
if err != nil {
return err
}
@@ -773,7 +733,7 @@ func (g Graph) ListStorageSpacesWithFilters(ctx context.Context, filters []*stor
return res, err
}
func (g Graph) cs3StorageSpaceToDrive(ctx context.Context, baseURL *url.URL, space *storageprovider.StorageSpace, apiVersion APIVersion, expandPermissions bool) (*libregraph.Drive, error) {
func (g Graph) cs3StorageSpaceToDrive(ctx context.Context, baseURL *url.URL, space *storageprovider.StorageSpace, apiVersion APIVersion) (*libregraph.Drive, error) {
logger := g.logger.SubloggerWithRequestID(ctx)
if space.Root == nil {
logger.Error().Msg("unable to parse space: space has no root")
@@ -785,20 +745,18 @@ func (g Graph) cs3StorageSpaceToDrive(ctx context.Context, baseURL *url.URL, spa
}
spaceID := storagespace.FormatResourceID(spaceRid)
permissions := g.cs3SpacePermissionsToLibreGraph(ctx, space, apiVersion)
drive := &libregraph.Drive{
Id: libregraph.PtrString(spaceID),
Name: space.Name,
//"createdDateTime": "string (timestamp)", // TODO read from StorageSpace ... needs Opaque for now
DriveType: &space.SpaceType,
// we currently always expandt the root because it carries the deleted property that indiccates if a space is trashed
Root: &libregraph.DriveItem{
Id: libregraph.PtrString(storagespace.FormatResourceID(spaceRid)),
Id: libregraph.PtrString(storagespace.FormatResourceID(spaceRid)),
Permissions: permissions,
},
}
if expandPermissions {
drive.Root.Permissions, _ = g.cs3SpacePermissionsToLibreGraph(ctx, space, false, apiVersion)
}
if space.SpaceType == _spaceTypeMountpoint {
var remoteItem *libregraph.RemoteItem
grantID := storageprovider.ResourceId{

View File

@@ -79,9 +79,13 @@ var _ = Describe("EducationClass", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
@@ -330,8 +334,12 @@ var _ = Describe("EducationClass", func() {
cfg.API.GroupMembersPatchLimit = 21
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -24,6 +24,7 @@ import (
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/graph/mocks"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
@@ -79,9 +80,13 @@ var _ = Describe("Schools", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.WithIdentityEducationBackend(identityEducationBackend),
)

View File

@@ -81,9 +81,13 @@ var _ = Describe("EducationUsers", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityEducationBackend(identityEducationBackend),

View File

@@ -81,9 +81,13 @@ var _ = Describe("Graph", func() {
eventsPublisher = mocks.Publisher{}
permissionService = mocks.Permissions{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.PermissionService(&permissionService),
@@ -489,42 +493,8 @@ var _ = Describe("Graph", func() {
Expect(libreError.Error.Message).To(Equal("internal quota error"))
Expect(libreError.Error.Code).To(Equal(errorcode.GeneralException.String()))
})
It("omit permissions by default", func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Times(1).Return(&provider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*provider.StorageSpace{
{
Opaque: utils.AppendJSONToOpaque(nil, "grants", map[string]provider.ResourcePermissions{
"1": *conversions.NewManagerRole().CS3ResourcePermissions(),
}),
Root: &provider.ResourceId{},
},
},
}, nil)
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Return(&gateway.InitiateFileDownloadResponse{
Status: status.NewNotFound(ctx, "not found"),
}, nil)
gatewayClient.On("GetQuota", mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
Status: status.NewUnimplemented(ctx, fmt.Errorf("not supported"), "not supported"),
}, nil)
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&userprovider.GetUserResponse{
Status: status.NewUnimplemented(ctx, fmt.Errorf("not supported"), "not supported"),
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives", nil)
r = r.WithContext(ctx)
rr := httptest.NewRecorder()
svc.GetDrivesV1(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
jsonData := gjson.Get(rr.Body.String(), "value")
Expect(jsonData.Get("#").Num).To(Equal(float64(1)))
Expect(jsonData.Get("0.root.permissions").Exists()).To(BeFalse())
})
})
DescribeTable("GetDrivesV1Beta1 and GetAllDrivesV1Beta1 expands root permissions",
DescribeTable("GetDrivesV1Beta1 and GetAllDrivesV1Beta1",
func(check func(gjson.Result), resourcePermissions provider.ResourcePermissions) {
permissionService.On("GetPermissionByID", mock.Anything, mock.Anything).Return(&settingssvc.GetPermissionByIDResponse{
Permission: &v0.Permission{
@@ -552,7 +522,7 @@ var _ = Describe("Graph", func() {
Status: status.NewUnimplemented(ctx, fmt.Errorf("not supported"), "not supported"),
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1beta1.0/me/drives?$expand=root($expand=permissions)", nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives", nil)
r = r.WithContext(ctx)
rr := httptest.NewRecorder()
svc.GetDrivesV1Beta1(rr, r)

View File

@@ -85,9 +85,13 @@ var _ = Describe("Groups", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
@@ -413,9 +417,13 @@ var _ = Describe("Groups", func() {
updatedGroupJson, err := json.Marshal(updatedGroup)
Expect(err).ToNot(HaveOccurred())
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
cfg.API.GroupMembersPatchLimit = 21
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -7,6 +7,7 @@ import (
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"go.opentelemetry.io/otel/trace"
"github.com/opencloud-eu/opencloud/pkg/keycloak"
@@ -33,7 +34,6 @@ type Options struct {
IdentityBackend identity.Backend
IdentityEducationBackend identity.EducationBackend
RoleService RoleService
UserProfilePhotoService UsersUserProfilePhotoProvider
PermissionService Permissions
ValueService settingssvc.ValueService
RoleManager *roles.Manager
@@ -43,6 +43,7 @@ type Options struct {
KeycloakClient keycloak.Client
EventHistoryClient ehsvc.EventHistoryService
TraceProvider trace.TracerProvider
Storage metadata.Storage
}
// newOptions initializes the available default options.
@@ -182,9 +183,9 @@ func TraceProvider(val trace.TracerProvider) Option {
}
}
// UserProfilePhotoService provides a function to set the UserProfilePhotoService option.
func UserProfilePhotoService(p UsersUserProfilePhotoProvider) Option {
// MetadataStorage provides a function to set the MetadataStorage option.
func MetadataStorage(ms metadata.Storage) Option {
return func(o *Options) {
o.UserProfilePhotoService = p
o.Storage = ms
}
}

View File

@@ -81,9 +81,13 @@ var _ = Describe("Users changing their own password", func() {
eventsPublisher = mocks.Publisher{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.WithIdentityBackend(identityBackend),
service.EventsPublisher(&eventsPublisher),

View File

@@ -17,12 +17,6 @@ import (
"github.com/jellydator/ttlcache/v3"
microstore "go-micro.dev/v4/store"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/store"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
ocldap "github.com/opencloud-eu/opencloud/pkg/ldap"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/registry"
@@ -32,6 +26,11 @@ import (
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity"
graphm "github.com/opencloud-eu/opencloud/services/graph/pkg/middleware"
"github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/store"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
)
const (
@@ -171,7 +170,12 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
return Graph{}, err
}
usersUserProfilePhotoApi, err := NewUsersUserProfilePhotoApi(options.UserProfilePhotoService, options.Logger)
usersUserProfilePhotoService, err := NewUsersUserProfilePhotoService(options.Storage)
if err != nil {
return Graph{}, err
}
usersUserProfilePhotoApi, err := NewUsersUserProfilePhotoApi(usersUserProfilePhotoService, options.Logger)
if err != nil {
return Graph{}, err
}

View File

@@ -246,8 +246,12 @@ var _ = Describe("sharedbyme", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -29,6 +29,7 @@ import (
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/graph/mocks"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
@@ -70,9 +71,13 @@ var _ = Describe("SharedWithMe", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.WithIdentityBackend(identityBackend),
)

View File

@@ -24,7 +24,6 @@ import (
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity"
"github.com/opencloud-eu/opencloud/services/graph/pkg/odata"
ocsettingssvc "github.com/opencloud-eu/opencloud/services/settings/pkg/service/v0"
"github.com/opencloud-eu/opencloud/services/settings/pkg/store/defaults"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
@@ -54,7 +53,7 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) {
return
}
exp, err := odata.GetExpandValues(odataReq.Query)
exp, err := identity.GetExpandValues(odataReq.Query)
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: $expand error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
@@ -120,85 +119,74 @@ func (g Graph) fetchAppRoleAssignments(ctx context.Context, accountuuid string)
// GetUserDrive implements the Service interface.
func (g Graph) GetUserDrive(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := g.logger.SubloggerWithRequestID(ctx).With().Interface("query", r.URL.Query()).Logger()
log.Debug().Msg("calling get user drive")
webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
log.Error().Err(err).Msg("could not get personal drive: error parsing webdav base url")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
log = log.With().Str("url", webDavBaseURL.String()).Logger()
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Debug().Interface("query", r.URL.Query()).Msg("calling get user drive")
userID, err := url.PathUnescape(chi.URLParam(r, "userID"))
if err != nil {
log.Debug().Err(err).Str("userID", chi.URLParam(r, "userID")).Msg("could not get drive: unescaping drive id failed")
logger.Debug().Err(err).Str("userID", chi.URLParam(r, "userID")).Msg("could not get drive: unescaping drive id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping user id failed")
return
}
log = log.With().Str("userID", userID).Logger()
_, expandPermissions, err := parseDriveRequest(r)
if err != nil {
log.Debug().Err(err).Msg("could not get drives: error parsing odata request")
errorcode.RenderError(w, r, err)
return
}
if userID == "" {
u, ok := revactx.ContextGetUser(ctx)
u, ok := revactx.ContextGetUser(r.Context())
if !ok {
log.Debug().Msg("could not get user: user not in context")
logger.Debug().Msg("could not get user: user not in context")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context")
return
}
userID = u.GetId().GetOpaqueId()
}
log.Debug().Msg("calling list storage spaces with user and personal filter")
logger.Debug().Str("userID", userID).Msg("calling list storage spaces with user and personal filter")
ctx := r.Context()
filters := []*storageprovider.ListStorageSpacesRequest_Filter{listStorageSpacesTypeFilter("personal"), listStorageSpacesUserFilter(userID)}
res, err := g.ListStorageSpacesWithFilters(ctx, filters, true)
switch {
case err != nil:
log.Error().Err(err).Msg("could not get drive: transport error")
logger.Error().Err(err).Msg("could not get drive: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
// the client is doing a lookup for a specific space, therefore we need to return
// not found to the caller
log.Debug().Msg("could not get personal drive for user: not found")
logger.Debug().Str("userID", userID).Msg("could not get personal drive for user: not found")
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "drive not found")
return
}
log.Debug().
logger.Debug().
Str("userID", userID).
Str("grpcmessage", res.GetStatus().GetMessage()).
Msg("could not get personal drive for user: grpc error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
return
}
spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces, APIVersion_1, expandPermissions)
webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
log.Debug().Err(err).Msg("could not get personal drive: error parsing grpc response")
logger.Error().Err(err).Str("url", webDavBaseURL.String()).Msg("could not get personal drive: error parsing webdav base url")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces, APIVersion_1)
if err != nil {
logger.Debug().Err(err).Msg("could not get personal drive: error parsing grpc response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
switch num := len(spaces); {
case num == 0:
log.Debug().Msg("could not get personal drive: no drive returned from storage")
logger.Debug().Str("userID", userID).Msg("could not get personal drive: no drive returned from storage")
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "no drive returned from storage")
return
case num == 1:
render.Status(r, http.StatusOK)
render.JSON(w, r, spaces[0])
default:
log.Debug().Int("number", num).Msg("could not get personal drive: expected to find a single drive but fetched more")
logger.Debug().Int("number", num).Msg("could not get personal drive: expected to find a single drive but fetched more")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not get personal drive: expected to find a single drive but fetched more")
return
}
@@ -313,7 +301,7 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
users = finalUsers
}
exp, err := odata.GetExpandValues(odataReq.Query)
exp, err := identity.GetExpandValues(odataReq.Query)
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: $expand error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
@@ -461,7 +449,7 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) {
return
}
exp, err := odata.GetExpandValues(odataReq.Query)
exp, err := identity.GetExpandValues(odataReq.Query)
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: $expand error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
@@ -478,8 +466,6 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) {
listDrives := slices.Contains(exp, "drives")
listDrive := slices.Contains(exp, "drive")
expandDrivePermissions := slices.Contains(exp, "drive.root.permissions")
expandDrivesPermissions := slices.Contains(exp, "drives.root.permissions")
// do we need to list all or only the personal drive
filters := []*storageprovider.ListStorageSpacesRequest_Filter{}
@@ -540,13 +526,7 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) {
user.Drive = &libregraph.Drive{}
}
for _, sp := range lspr.GetStorageSpaces() {
expandPermissions := false
if sp.GetSpaceType() == "personal" && sp.GetOwner().GetId().GetOpaqueId() != user.GetId() {
expandPermissions = expandDrivePermissions
} else {
expandPermissions = expandDrivesPermissions
}
d, err := g.cs3StorageSpaceToDrive(r.Context(), wdu, sp, APIVersion_1, expandPermissions)
d, err := g.cs3StorageSpaceToDrive(r.Context(), wdu, sp, APIVersion_1)
if err != nil {
logger.Debug().Err(err).Interface("id", sp.Id).Msg("error converting space to drive")
continue
@@ -1043,7 +1023,7 @@ func (g Graph) searchOCMAcceptedUsers(ctx context.Context, odataReq *godata.GoDa
if err != nil {
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
term, err := odata.GetSearchValues(odataReq.Query)
term, err := identity.GetSearchValues(odataReq.Query)
if err != nil {
return nil, errorcode.New(errorcode.InvalidRequest, err.Error())
}

View File

@@ -95,9 +95,13 @@ var _ = Describe("Users", func() {
When("OCM is disabled", func() {
BeforeEach(func() {
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
@@ -907,8 +911,12 @@ var _ = Describe("Users", func() {
localCfg.API.UsernameMatch = usernameMatch
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
localSvc, err := service.NewService(
service.Config(localCfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
@@ -1129,9 +1137,13 @@ var _ = Describe("Users", func() {
BeforeEach(func() {
cfg.IncludeOCMSharees = true
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -7,7 +7,7 @@
"analyze": "source-map-explorer 'build/static/js/*.js'",
"build": "node --openssl-legacy-provider scripts/build.js && rm -f build/service-worker.js",
"licenses": "NODE_PATH=./node_modules node ../scripts/js-license-ranger.js",
"licenses:check": "license-checker-rseidelsohn --summary --relativeLicensePath --onlyAllow 'Python-2.0;Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Unicode-TOU;Unlicense;WTFPL;ODC-By-1.0;BlueOak-1.0.0;OFL-1.1' --excludePackages 'identifier;kpop;unicoderegexp' --clarificationsFile license-checker-clarifications.json",
"licenses:check": "license-checker-rseidelsohn --summary --relativeLicensePath --onlyAllow 'Python-2.0;Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Unicode-TOU;Unlicense;WTFPL;ODC-By-1.0;BlueOak-1.0.0' --excludePackages 'identifier;kpop;unicoderegexp' --clarificationsFile license-checker-clarifications.json",
"licenses:csv": "license-checker-rseidelsohn --relativeLicensePath --csv --out ../../third-party-licenses/node/idp/third-party-licenses.csv",
"licenses:save": "license-checker-rseidelsohn --relativeLicensePath --out /dev/null --files ../../third-party-licenses/node/idp/third-party-licenses",
"lint": "eslint ./**/*.{tsx,ts,jsx,js}",
@@ -71,21 +71,21 @@
]
},
"dependencies": {
"@fontsource/roboto": "^5.2.5",
"@fontsource/roboto": "^5.1.0",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^12.1.5",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.30",
"@types/jest": "^29.5.12",
"@types/node": "^22.15.19",
"@types/react": "^17.0.80",
"@types/react-dom": "^17.0.25",
"@types/react-redux": "^7.1.33",
"@types/redux-logger": "^3.0.13",
"axios": "^1.8.2",
"classnames": "^2.5.1",
"i18next": "^25.2.1",
"i18next": "^25.1.2",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"i18next-resources-to-backend": "^1.2.1",
@@ -102,7 +102,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"render-if": "^0.1.1",
"web-vitals": "^5.0.2"
"web-vitals": "^4.2.4"
},
"devDependencies": {
"@babel/core": "7.26.10",
@@ -113,7 +113,7 @@
"babel-plugin-named-asset-import": "^0.3.8",
"babel-preset-react-app": "^10.1.0",
"case-sensitive-paths-webpack-plugin": "2.4.0",
"cldr": "^7.9.0",
"cldr": "^7.5.0",
"css-loader": "7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
"dotenv": "16.4.7",
@@ -126,7 +126,7 @@
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^24.7.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-testing-library": "^3.10.2",
"eslint-webpack-plugin": "^3.2.0",
@@ -140,13 +140,13 @@
"pnp-webpack-plugin": "1.7.0",
"postcss-flexbugs-fixes": "5.0.2",
"postcss-loader": "4.3.0",
"postcss-normalize": "13.0.1",
"postcss-normalize": "13.0.0",
"postcss-preset-env": "10.1.3",
"postcss-safe-parser": "7.0.1",
"react-dev-utils": "^12.0.1",
"resolve": "1.22.8",
"resolve-url-loader": "^5.0.0",
"sass-loader": "^16.0.5",
"sass-loader": "^16.0.4",
"source-map-explorer": "^2.5.3",
"typescript": "^5.8.3",
"url-loader": "4.1.1",

View File

File diff suppressed because it is too large Load Diff

View File

@@ -105,8 +105,9 @@ When all instances of a federation should trust each other, an `ocmproviders.jso
]
```
::: info
{{< hint info >}}
Note: the `domain` must not contain the protocol as it has to match the [GOCDB site object domain](https://developer.sciencemesh.io/docs/technical-documentation/central-database/#site-object).
{{< /hint >}}
The above federation consists of two instances: `cloud1.opencloud.test` and `cloud2.opencloud.test` that can use the Invitation workflow described below to generate, send and accept invitations.
@@ -120,8 +121,9 @@ The data backend of the `ocminvitemanager` is configurable. The only supported b
## Creating Shares
::: info
{{< hint info >}}
The below info is outdated as we allow creating federated shares using the graph API. Clients can now discover the available sharing roles and invite federated users using the graph API.
{{< /hint >}}
OCM Shares are currently created using the ocs API, just like regular shares. The difference is the share type, which is 6 (ShareTypeFederatedCloudShare) in this case, and a few additional parameters required for identifying the remote user.

View File

@@ -1,6 +1,6 @@
SHELL := bash
NAME := web
WEB_ASSETS_VERSION = v3.0.0
WEB_ASSETS_VERSION = v2.4.0
WEB_ASSETS_BRANCH = main
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI

View File

@@ -39,7 +39,7 @@ func DefaultConfig() *config.Config {
Service: config.Service{
Name: "webdav",
},
OpenCloudPublicURL: "https://localhost:9200",
OpenCloudPublicURL: "https://127.0.0.1:9200",
WebdavNamespace: "/users/{{.Id.OpaqueId}}",
RevaGateway: shared.DefaultRevaConfig().Address,
}

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"strings"
@@ -15,17 +14,16 @@ import (
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/tags"
"github.com/opencloud-eu/reva/v2/pkg/utils"
searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0"
searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
"github.com/opencloud-eu/opencloud/services/webdav/pkg/constants"
"github.com/opencloud-eu/opencloud/services/webdav/pkg/net"
"github.com/opencloud-eu/opencloud/services/webdav/pkg/prop"
"github.com/opencloud-eu/opencloud/services/webdav/pkg/propfind"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/tags"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
const (
@@ -93,7 +91,7 @@ func (g Webdav) Search(w http.ResponseWriter, r *http.Request) {
func (g Webdav) sendSearchResponse(rsp *searchsvc.SearchResponse, w http.ResponseWriter, r *http.Request) {
logger := g.log.SubloggerWithRequestID(r.Context())
responsesXML, err := multistatusResponse(r.Context(), g.config.OpenCloudPublicURL, rsp.Matches)
responsesXML, err := multistatusResponse(r.Context(), rsp.Matches)
if err != nil {
logger.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
@@ -111,10 +109,10 @@ func (g Webdav) sendSearchResponse(rsp *searchsvc.SearchResponse, w http.Respons
}
// multistatusResponse converts a list of matches into a multistatus response string
func multistatusResponse(ctx context.Context, publicURL string, matches []*searchmsg.Match) ([]byte, error) {
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, publicURL, matches[i])
res, err := matchToPropResponse(ctx, matches[i])
if err != nil {
return nil, err
}
@@ -130,14 +128,14 @@ func multistatusResponse(ctx context.Context, publicURL string, matches []*searc
return msg, nil
}
func matchToPropResponse(ctx context.Context, publicURL string, match *searchmsg.Match) (*propfind.ResponseXML, error) {
// unfortunately, search uses own versions of ResourceId and Ref. So we need to assert them here
func matchToPropResponse(ctx context.Context, match *searchmsg.Match) (*propfind.ResponseXML, error) {
// unfortunately search uses own versions of ResourceId and Ref. So we need to assert them here
var (
ref string
err error
)
// to copy PROPFIND behaviour, we need to deliver different ids
// to copy PROPFIND behaviour we need to deliver different ids
// for shares it needs to be sharestorageproviderid!shareid
// for other spaces it needs to be storageproviderid$spaceid
switch match.Entity.Ref.ResourceId.StorageId {
@@ -218,15 +216,6 @@ func matchToPropResponse(ctx context.Context, publicURL string, match *searchmsg
score := strconv.FormatFloat(float64(match.Score), 'f', -1, 64)
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:score", score))
if privateURL, err := url.Parse(publicURL); err == nil && match.Entity.Id != nil {
privateURL.Path = path.Join(privateURL.Path, "f", storagespace.FormatResourceID(&provider.ResourceId{
StorageId: match.Entity.Id.StorageId,
SpaceId: match.Entity.Id.SpaceId,
OpaqueId: match.Entity.Id.OpaqueId,
}))
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:privatelink", privateURL.String()))
}
if len(propstatOK.Prop) > 0 {
response.Propstat = append(response.Propstat, propstatOK)
}

View File

@@ -2168,7 +2168,6 @@ class GraphHelper {
* @param string $user
* @param string $password
* @param string $spaceId
* @param string|null $query
*
* @return ResponseInterface
* @throws GuzzleException
@@ -2178,14 +2177,9 @@ class GraphHelper {
string $xRequestId,
string $user,
string $password,
string $spaceId,
?string $query = null
string $spaceId
): ResponseInterface {
$url = self::getBetaFullUrl($baseUrl, "drives/$spaceId/root/permissions");
if ($query !== null) {
$url .= "?$query";
}
return HttpRequestHelper::get(
$url,
$xRequestId,
@@ -2474,4 +2468,109 @@ class GraphHelper {
self::getRequestHeaders()
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
* @param string $source
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function addUserPhoto(
string $baseUrl,
string $xRequestId,
string $user,
string $password,
string $source
): ResponseInterface {
$url = self::getFullUrl($baseUrl, "me/photo/\$value");
return HttpRequestHelper::put(
$url,
$xRequestId,
$user,
$password,
['Content-Type' => 'image/jpeg'],
$source
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function getUserPhoto(
string $baseUrl,
string $xRequestId,
string $user,
string $password
): ResponseInterface {
$url = self::getFullUrl($baseUrl, "me/photo/\$value");
return HttpRequestHelper::get(
$url,
$xRequestId,
$user,
$password
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
* @param string $source
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function changeUserPhoto(
string $baseUrl,
string $xRequestId,
string $user,
string $password,
string $source
): ResponseInterface {
$url = self::getFullUrl($baseUrl, "me/photo/\$value");
return HttpRequestHelper::sendRequest(
$url,
$xRequestId,
"PATCH",
$user,
$password,
['Content-Type' => 'image/jpeg'],
$source
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function deleteUserPhoto(
string $baseUrl,
string $xRequestId,
string $user,
string $password
): ResponseInterface {
$url = self::getFullUrl($baseUrl, "me/photo/\$value");
return HttpRequestHelper::delete(
$url,
$xRequestId,
$user,
$password
);
}
}

View File

@@ -3178,4 +3178,145 @@ class GraphContext implements Context {
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" sets profile photo to "([^"]*)" using the Graph API$/
*
* @param string $user
* @param string $photo
*
* @return void
* @throws GuzzleException
*/
public function userSetsUserProfilePhotoUsingTheGraphApi(
string $user,
string $photo
): void {
$source = \file_get_contents(
$this->featureContext->acceptanceTestsDirLocation() . $photo
);
$response = GraphHelper::addUserPhoto(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$source
);
$this->featureContext->setResponse($response);
}
/**
* @Given /^user "([^"]*)" has set the profile photo to "([^"]*)"$/
*
* @param string $user
* @param string $photo
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public function theUserHasSetPhoto(string $user, string $photo): void {
$response = $this->userSetsUserProfilePhotoUsingTheGraphApi($user, $photo);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
}
/**
* @When /^user "([^"]*)" (gets|tries to get) a profile photo using the Graph API$/
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userShouldHasAProfilePhotoUsingTheGraphApi(string $user): void {
$response = GraphHelper::getUserPhoto(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user)
);
$this->featureContext->setResponse($response);
}
/**
* @Then /^the profile photo should contain file "([^"]*)"$/
*
* @param string $file
*
* @return void
* @throws GuzzleException
*/
public function profilePhotoShouldContainFile(string $file): void {
$source = \file_get_contents(
$this->featureContext->acceptanceTestsDirLocation() . $file
);
Assert::assertEquals(
$source,
$this->featureContext->getResponse()->getBody()->getContents(),
"The profile photo binary does not match expected content of $file"
);
}
/**
* @Then /^for user "([^"]*)" the profile photo should contain file "([^"]*)"$/
*
* @param string $user
* @param string $file
*
* @return void
* @throws GuzzleException
*/
public function profilePhotoForUserShouldContainFile(string $user, string $file): void {
$this->featureContext->theHTTPStatusCodeShouldBe(
200,
"Expected response status code should be 200",
$this->userShouldHasAProfilePhotoUsingTheGraphApi($user)
);
$this->profilePhotoShouldContainFile($file);
}
/**
* @When /^user "([^"]*)" changes the profile photo to "([^"]*)" using the Graph API$/
*
* @param string $user
* @param string $photo
*
* @return void
* @throws GuzzleException
*/
public function userChangesUserProfilePhotoUsingTheGraphApi(
string $user,
string $photo
): void {
$source = \file_get_contents(
$this->featureContext->acceptanceTestsDirLocation() . $photo
);
$response = GraphHelper::changeUserPhoto(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$source
);
$this->featureContext->setResponse($response);
}
/**
* @When /^user "([^"]*)" deletes the profile photo using the Graph API$/
*
* @param string $user
*
* @return void
* @throws GuzzleException
*/
public function userDeletesAProfilePhotoUsingTheGraphApi(string $user): void {
$response = GraphHelper::deleteUserPhoto(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user)
);
$this->featureContext->setResponse($response);
}
}

View File

@@ -181,37 +181,6 @@ class SharingNgContext implements Context {
);
}
/**
* @param string $user
* @param string $space
* @param string|null $query
* @param string|null $spaceOwner
*
* @return ResponseInterface
* @throws GuzzleException
*/
private function getDrivePermissionsList(
string $user,
string $space,
?string $query = null,
?string $spaceOwner = null,
): ResponseInterface {
if ($spaceOwner) {
$spaceId = ($this->spacesContext->getSpaceByName($spaceOwner, $space))["id"];
} else {
$spaceId = ($this->spacesContext->getSpaceByName($user, $space))["id"];
}
return GraphHelper::getDrivePermissionsList(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$spaceId,
$query
);
}
/**
* @When /^user "([^"]*)" gets permissions list for (folder|file) "([^"]*)" of the space "([^"]*)" using the Graph API$/
*
@@ -296,7 +265,7 @@ class SharingNgContext implements Context {
public function sendShareInvitation(
string $user,
array $shareInfo,
?string $fileId = null,
string $fileId = null,
bool $federatedShare = false
): ResponseInterface {
if ($shareInfo['space'] === 'Personal' || $shareInfo['space'] === 'Shares') {
@@ -1607,7 +1576,16 @@ class SharingNgContext implements Context {
*
*/
public function userListsThePermissionsOfDriveUsingRootEndPointOFTheGraphApi(string $user, string $space): void {
$this->featureContext->setResponse($this->getDrivePermissionsList($user, $space));
$spaceId = ($this->spacesContext->getSpaceByName($user, $space))["id"];
$response = GraphHelper::getDrivePermissionsList(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$spaceId
);
$this->featureContext->setResponse($response);
}
/**
@@ -1793,7 +1771,15 @@ class SharingNgContext implements Context {
* @throws GuzzleException
*/
public function userShouldNotHaveAnyPermissionsOnSpace(string $user, string $shareType, string $space): void {
$response = $this->getDrivePermissionsList($user, $space);
$spaceId = ($this->spacesContext->getSpaceByName($user, $space))["id"];
$response = GraphHelper::getDrivePermissionsList(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$spaceId
);
$responseBody = $this->featureContext->getJsonDecodedResponse($response);
foreach ($responseBody['value'] as $value) {
switch ($shareType) {
@@ -1892,7 +1878,16 @@ class SharingNgContext implements Context {
string $space,
string $spaceOwner
): void {
$this->featureContext->setResponse($this->getDrivePermissionsList($user, $space, null, $spaceOwner));
$spaceId = ($this->spacesContext->getSpaceByName($spaceOwner, $space))["id"];
$response = GraphHelper::getDrivePermissionsList(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
$spaceId
);
$this->featureContext->setResponse($response);
}
/**
@@ -2123,75 +2118,4 @@ class SharingNgContext implements Context {
$this->getPermissionsList($user, $fileOrFolder, $space, $resource, $query)
);
}
/**
* @When /^user "([^"]*)" gets the permittion list of (folder|file) "([^"]*)" from the space "([^"]*)" using the Graph API with query "([^"]*)"$/
*
* @param string $user
* @param string $fileOrFolder (file|folder)
* @param string $resource
* @param string $space
* @param string $query
*
* @return void
* @throws Exception
*/
public function userGetsPermissionsListWithQueryForFileOfTheSpaceUsingTheGraphApi(
string $user,
string $fileOrFolder,
string $resource,
string $space,
string $query
): void {
$this->featureContext->setResponse(
$this->getPermissionsList($user, $fileOrFolder, $space, $resource, $query)
);
}
/**
* @When /^user "([^"]*)" gets the drive permittion list of the space "([^"]*)" using the Graph API with query "([^"]*)"$/
*
* @param string $user
* @param string $space
* @param string $query
*
* @return void
* @throws Exception
*/
public function userGetsDrivePermissionsListWithQueryUsingTheGraphApi(
string $user,
string $space,
string $query
): void {
$this->featureContext->setResponse($this->getDrivePermissionsList($user, $space, $query));
}
/**
* @Then /^the JSON data of the response should (not |)contain the following keys:$/
*
* @param string|null $shouldOrNot (not| )
* @param TableNode $table
*
* @return void
* @throws Exception
*/
public function theJsonDataResponseShouldOrNotContainData(string $shouldOrNot, TableNode $table): void {
$response = $this->featureContext->getJsonDecodedResponse($this->featureContext->getResponse());
foreach ($table->getColumn(0) as $key) {
$keyExists = \array_key_exists($key, $response);
if (\trim($shouldOrNot) !== "not") {
Assert::assertTrue(
$keyExists,
"Expected key '$key' to exist in the JSON response, but it doesn't."
);
} else {
Assert::assertFalse(
$keyExists,
"Key '$key' should not exist in the JSON response, but it does."
);
}
}
}
}

View File

@@ -1046,7 +1046,7 @@ class SpacesContext implements Context {
string $grantedUser,
string $role
): void {
$response = $this->listAllAvailableSpacesOfUser($user, '$expand=root($expand=permissions)');
$response = $this->listAllAvailableSpacesOfUser($user);
$this->featureContext->theHTTPStatusCodeShouldBe(
200,
"Expected response status code should be 200",
@@ -4618,7 +4618,7 @@ class SpacesContext implements Context {
string $role,
?string $expirationDate = null
): void {
$response = $this->listAllAvailableSpacesOfUser($user, '$expand=root($expand=permissions)');
$response = $this->listAllAvailableSpacesOfUser($user);
$this->featureContext->theHTTPStatusCodeShouldBe(
200,
"Expected response status code should be 200",

View File

@@ -205,5 +205,27 @@
- [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)
#### [No notification triggered for .zip virus file](https://github.com/opencloud-eu/opencloud/issues/382)
- [apiAntivirus/antivirus.feature:41](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L41)
- [apiAntivirus/antivirus.feature:43](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L43)
- [apiAntivirus/antivirus.feature:45](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L45)
- [apiAntivirus/antivirus.feature:69](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L69)
- [apiAntivirus/antivirus.feature:71](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L71)
- [apiAntivirus/antivirus.feature:73](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L73)
- [apiAntivirus/antivirus.feature:115](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L115)
- [apiAntivirus/antivirus.feature:117](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L117)
- [apiAntivirus/antivirus.feature:119](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L119)
- [apiAntivirus/antivirus.feature:141](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L141)
- [apiAntivirus/antivirus.feature:143](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L143)
- [apiAntivirus/antivirus.feature:145](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L145)
- [apiAntivirus/antivirus.feature:169](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L169)
- [apiAntivirus/antivirus.feature:171](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L171)
- [apiAntivirus/antivirus.feature:173](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L173)
- [apiAntivirus/antivirus.feature:199](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L199)
- [apiAntivirus/antivirus.feature:201](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L201)
- [apiAntivirus/antivirus.feature:203](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L203)
- [apiAntivirus/antivirus.feature:228](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L228)
- [apiAntivirus/antivirus.feature:253](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L253)
Note: always have an empty line at the end of this file.
The bash script that processes this file requires that the last line has a newline on the end.

View File

@@ -205,5 +205,27 @@
- [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)
#### [No notification triggered for .zip virus file](https://github.com/opencloud-eu/opencloud/issues/382)
- [apiAntivirus/antivirus.feature:41](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L41)
- [apiAntivirus/antivirus.feature:43](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L43)
- [apiAntivirus/antivirus.feature:45](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L45)
- [apiAntivirus/antivirus.feature:69](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L69)
- [apiAntivirus/antivirus.feature:71](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L71)
- [apiAntivirus/antivirus.feature:73](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L73)
- [apiAntivirus/antivirus.feature:115](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L115)
- [apiAntivirus/antivirus.feature:117](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L117)
- [apiAntivirus/antivirus.feature:119](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L119)
- [apiAntivirus/antivirus.feature:141](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L141)
- [apiAntivirus/antivirus.feature:143](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L143)
- [apiAntivirus/antivirus.feature:145](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L145)
- [apiAntivirus/antivirus.feature:169](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L169)
- [apiAntivirus/antivirus.feature:171](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L171)
- [apiAntivirus/antivirus.feature:173](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L173)
- [apiAntivirus/antivirus.feature:199](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L199)
- [apiAntivirus/antivirus.feature:201](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L201)
- [apiAntivirus/antivirus.feature:203](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L203)
- [apiAntivirus/antivirus.feature:228](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L228)
- [apiAntivirus/antivirus.feature:253](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L253)
Note: always have an empty line at the end of this file.
The bash script that processes this file requires that the last line has a newline on the end.

View File

@@ -2,16 +2,16 @@
#### [REPORT request without remote.php returns empty result (only with dav/spaces path)](https://github.com/owncloud/ocis/issues/10329)
- [apiContract/sharesReport.feature:43](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L43)
- [apiContract/sharesReport.feature:68](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L68)
- [apiContract/sharesReport.feature:134](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L134)
- [apiContract/sharesReport.feature:164](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L164)
- [apiContract/sharesReport.feature:42](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L42)
- [apiContract/sharesReport.feature:66](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L66)
- [apiContract/sharesReport.feature:130](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L130)
- [apiContract/sharesReport.feature:159](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/sharesReport.feature#L159)
- [apiContract/spacesReport.feature:16](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L16)
- [apiContract/spacesReport.feature:35](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L35)
- [apiContract/spacesReport.feature:55](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L55)
- [apiContract/spacesReport.feature:74](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L74)
- [apiContract/spacesSharesReport.feature:46](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesSharesReport.feature#L46)
- [apiContract/spacesSharesReport.feature:77](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesSharesReport.feature#L77)
- [apiContract/spacesReport.feature:34](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L34)
- [apiContract/spacesReport.feature:53](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L53)
- [apiContract/spacesReport.feature:71](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesReport.feature#L71)
- [apiContract/spacesSharesReport.feature:45](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesSharesReport.feature#L45)
- [apiContract/spacesSharesReport.feature:75](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiContract/spacesSharesReport.feature#L75)
- [apiSearch1/dateSearch.feature:19](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L19)
- [apiSearch1/dateSearch.feature:39](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L39)
- [apiSearch1/dateSearch.feature:40](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSearch1/dateSearch.feature#L40)
@@ -204,17 +204,11 @@
- [apiLocks/unlockFiles.feature:322](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L322)
- [apiLocks/unlockFiles.feature:323](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiLocks/unlockFiles.feature#L323)
- [apiAntivirus/antivirus.feature:114](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L114)
- [apiAntivirus/antivirus.feature:115](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L115)
- [apiAntivirus/antivirus.feature:116](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L116)
- [apiAntivirus/antivirus.feature:117](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L117)
- [apiAntivirus/antivirus.feature:118](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L118)
- [apiAntivirus/antivirus.feature:119](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L119)
- [apiAntivirus/antivirus.feature:140](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L140)
- [apiAntivirus/antivirus.feature:141](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L141)
- [apiAntivirus/antivirus.feature:142](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L142)
- [apiAntivirus/antivirus.feature:143](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L143)
- [apiAntivirus/antivirus.feature:144](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L144)
- [apiAntivirus/antivirus.feature:145](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L145)
- [apiAntivirus/antivirus.feature:356](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L356)
- [apiAntivirus/antivirus.feature:357](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L357)
- [apiAntivirus/antivirus.feature:358](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiAntivirus/antivirus.feature#L358)

View File

@@ -32,17 +32,17 @@ Feature: antivirus
# antivirus service can scan files during post-processing. on demand scanning is currently not available
Then the HTTP status code should be "201"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| <message> |
| message |
| Virus found in <new-file-name>. Upload not possible. Virus: Eicar-Signature |
And as "Alice" file "<new-file-name>" should not exist
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| dav-path-version | file-name | new-file-name |
| old | eicar.com | virusFile1.txt |
| old | eicar_com.zip | virusFile2.zip |
| new | eicar.com | virusFile1.txt |
| new | eicar_com.zip | virusFile2.zip |
| spaces | eicar.com | virusFile1.txt |
| spaces | eicar_com.zip | virusFile2.zip |
Scenario Outline: upload a file with virus and a file without virus
@@ -53,8 +53,8 @@ Feature: antivirus
And user "Alice" uploads file "filesForUpload/textfile.txt" to "/normalfile.txt" using the WebDAV API
And the HTTP status code should be "201"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| <message> |
| message |
| Virus found in <new-file-name>. Upload not possible. Virus: Eicar-Signature |
And as "Alice" file "<new-file-name>" should not exist
But as "Alice" file "/normalfile.txt" should exist
And the content of file "/normalfile.txt" for user "Alice" should be:
@@ -64,13 +64,13 @@ Feature: antivirus
Cheers.
"""
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| dav-path-version | file-name | new-file-name |
| old | eicar.com | virusFile1.txt |
| old | eicar_com.zip | virusFile2.zip |
| new | eicar.com | virusFile1.txt |
| new | eicar_com.zip | virusFile2.zip |
| spaces | eicar.com | virusFile1.txt |
| spaces | eicar_com.zip | virusFile2.zip |
Scenario Outline: upload a file with virus in chunks
@@ -92,7 +92,7 @@ Feature: antivirus
| new |
| spaces |
@issue-10331 @env-config
@issue-10331
Scenario Outline: public uploads a file with the virus to a public share
Given using <dav-path-version> DAV path
And the config "OC_SHARING_PUBLIC_SHARE_MUST_HAVE_PASSWORD" has been set to "false"
@@ -106,17 +106,17 @@ Feature: antivirus
When the public uploads file "filesForUpload/filesWithVirus/<file-name>" to "<new-file-name>" inside last link shared folder using the public WebDAV API
Then the HTTP status code should be "201"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| <message> |
| message |
| Virus found in <new-file-name>. Upload not possible. Virus: Eicar-Signature |
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| dav-path-version | file-name | new-file-name |
| old | eicar.com | virusFile1.txt |
| old | eicar_com.zip | virusFile2.zip |
| new | eicar.com | virusFile1.txt |
| new | eicar_com.zip | virusFile2.zip |
| spaces | eicar.com | virusFile1.txt |
| spaces | eicar_com.zip | virusFile2.zip |
@issue-10331
Scenario Outline: public uploads a file with the virus to a password-protected public share
@@ -132,17 +132,17 @@ Feature: antivirus
When the public uploads file "filesForUpload/filesWithVirus/<file-name>" to "<new-file-name>" inside last link shared folder with password "%public%" using the public WebDAV API
Then the HTTP status code should be "201"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| <message> |
| message |
| Virus found in <new-file-name>. Upload not possible. Virus: Eicar-Signature |
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| dav-path-version | file-name | new-file-name |
| old | eicar.com | virusFile1.txt |
| old | eicar_com.zip | virusFile2.zip |
| new | eicar.com | virusFile1.txt |
| new | eicar_com.zip | virusFile2.zip |
| spaces | eicar.com | virusFile1.txt |
| spaces | eicar_com.zip | virusFile2.zip |
Scenario Outline: upload a file with virus to a user share
@@ -159,18 +159,18 @@ Feature: antivirus
When user "Brian" uploads file "filesForUpload/filesWithVirus/<file-name>" to "/Shares/uploadFolder/<new-file-name>" using the WebDAV API
Then the HTTP status code should be "201"
And user "Brian" should get a notification with subject "Virus found" and message:
| message |
| <message> |
| message |
| Virus found in <new-file-name>. Upload not possible. Virus: Eicar-Signature |
And as "Brian" file "/Shares/uploadFolder/<new-file-name>" should not exist
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| dav-path-version | file-name | new-file-name |
| old | eicar.com | virusFile1.txt |
| old | eicar_com.zip | virusFile2.zip |
| new | eicar.com | virusFile1.txt |
| new | eicar_com.zip | virusFile2.zip |
| spaces | eicar.com | virusFile1.txt |
| spaces | eicar_com.zip | virusFile2.zip |
Scenario Outline: upload a file with virus to a group share
@@ -189,18 +189,18 @@ Feature: antivirus
When user "Brian" uploads file "filesForUpload/filesWithVirus/<file-name>" to "/Shares/uploadFolder/<new-file-name>" using the WebDAV API
Then the HTTP status code should be "201"
And user "Brian" should get a notification with subject "Virus found" and message:
| message |
| <message> |
| message |
| Virus found in <new-file-name>. Upload not possible. Virus: Eicar-Signature |
And as "Brian" file "/Shares/uploadFolder/<new-file-name>" should not exist
And as "Alice" file "/uploadFolder/<new-file-name>" should not exist
Examples:
| dav-path-version | file-name | new-file-name | message |
| old | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| old | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| new | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| new | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| spaces | eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| spaces | eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| dav-path-version | file-name | new-file-name |
| old | eicar.com | virusFile1.txt |
| old | eicar_com.zip | virusFile2.zip |
| new | eicar.com | virusFile1.txt |
| new | eicar_com.zip | virusFile2.zip |
| spaces | eicar.com | virusFile1.txt |
| spaces | eicar_com.zip | virusFile2.txt |
Scenario Outline: upload a file with virus to a project space
@@ -211,21 +211,21 @@ Feature: antivirus
When user "Alice" uploads a file "filesForUpload/filesWithVirus/<file-name>" to "/uploadFolder/<new-file-name>" in space "new-space" using the WebDAV API
Then the HTTP status code should be "201"
And user "Alice" should get a notification for resource "<new-file-name>" with subject "Virus found" and message:
| message |
| <message> |
| message |
| Virus found in <new-file-name>. Upload not possible. Virus: Eicar-Signature |
And for user "Alice" folder "uploadFolder" of the space "new-space" should not contain these entries:
| <new-file-name> |
When user "Alice" uploads a file "filesForUpload/filesWithVirus/<file-name>" to "/<new-file-name>" in space "new-space" using the WebDAV API
Then the HTTP status code should be "201"
And user "Alice" should get a notification for resource "<new-file-name>" with subject "Virus found" and message:
| message |
| <message> |
| message |
| Virus found in <new-file-name>. Upload not possible. Virus: Eicar-Signature |
And for user "Alice" the space "new-space" should not contain these entries:
| /<new-file-name> |
Examples:
| file-name | new-file-name | message |
| eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| file-name | new-file-name |
| eicar.com | virusFile1.txt |
| eicar_com.zip | virusFile2.zip |
Scenario Outline: upload a file with virus to a shared project space
@@ -241,16 +241,16 @@ Feature: antivirus
When user "Brian" uploads a file "/filesForUpload/filesWithVirus/<file-name>" to "/<new-file-name>" in space "new-space" using the WebDAV API
Then the HTTP status code should be "201"
And user "Brian" should get a notification with subject "Virus found" and message:
| message |
| <message> |
| message |
| Virus found in <new-file-name>. Upload not possible. Virus: Eicar-Signature |
And for user "Brian" the space "new-space" should not contain these entries:
| /<new-file-name> |
And for user "Alice" the space "new-space" should not contain these entries:
| /<new-file-name> |
Examples:
| file-name | new-file-name | message |
| eicar.com | virusFile1.txt | Virus found in virusFile1.txt. Upload not possible. Virus: Eicar-Signature |
| eicar_com.zip | virusFile2.zip | Virus found in virusFile2.zip. Upload not possible. Virus: Win.Test.EICAR_HDB-1 |
| file-name | new-file-name |
| eicar.com | virusFile1.txt |
| eicar_com.zip | virusFile2.zip |
@env-config @issue-6494
Scenario Outline: upload a file with virus by setting antivirus infected file handling config to continue
@@ -481,3 +481,13 @@ Feature: antivirus
| Virus found in text.txt. Upload not possible. Virus: Eicar-Signature |
And for user "Brian" the content of the file "/text.txt" of the space "new-space" should be "hello world"
And for user "Alice" the content of the file "/text.txt" of the space "new-space" should be "hello world"
Scenario Outline: try adding a photo of the user containing the virus
When user "Alice" sets profile photo to "filesForUpload/filesWithVirus/eicar-image.jpeg" using the Graph API
Then the HTTP status code should be "200"
And user "Alice" should get a notification with subject "Virus found" and message:
| message |
| Virus found in eicar-image.jpeg. Upload not possible. Virus: Eicar-Signature |
When user "Alice" tries to get a profile photo using the Graph API
Then the HTTP status code should be "404"

View File

@@ -27,15 +27,14 @@ Feature: REPORT request to Shares space
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "SubFolder1" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | SubFolder1 |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | S |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:remote-item-id | %file_id_pattern% |
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | SubFolder1 |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | S |
| oc:remote-item-id | %file_id_pattern% |
Examples:
| dav-path-version |
| old |
@@ -51,16 +50,15 @@ Feature: REPORT request to Shares space
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "frodo.txt" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | frodo.txt |
| d:getcontenttype | text/plain |
| oc:permissions | S |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 34 |
| oc:remote-item-id | %file_id_pattern% |
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | frodo.txt |
| d:getcontenttype | text/plain |
| oc:permissions | S |
| d:getcontentlength | 34 |
| oc:remote-item-id | %file_id_pattern% |
Examples:
| dav-path-version |
| old |
@@ -107,26 +105,24 @@ Feature: REPORT request to Shares space
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "secureFolder" with these key and value pairs:
| key | value |
| oc:shareroot | /secureFolder |
| oc:name | secureFolder |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | SMX |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:size | 14 |
| oc:remote-item-id | %file_id_pattern% |
| key | value |
| oc:shareroot | /secureFolder |
| oc:name | secureFolder |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | SMX |
| oc:size | 14 |
| oc:remote-item-id | %file_id_pattern% |
When user "Brian" searches for "secure.txt" using the WebDAV API
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
Then the HTTP status code should be "207"
And as user "Brian" the REPORT response should contain a resource "secure.txt" with these key and value pairs:
| key | value |
| oc:shareroot | /secureFolder |
| oc:name | secure.txt |
| d:getcontenttype | text/plain |
| oc:permissions | SX |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 14 |
| key | value |
| oc:shareroot | /secureFolder |
| oc:name | secure.txt |
| d:getcontenttype | text/plain |
| oc:permissions | SX |
| d:getcontentlength | 14 |
Examples:
| dav-path-version |
| old |
@@ -150,13 +146,12 @@ Feature: REPORT request to Shares space
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "secure.txt" with these key and value pairs:
| key | value |
| oc:shareroot | /secure.txt |
| oc:name | secure.txt |
| d:getcontenttype | text/plain |
| oc:permissions | SMX |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 14 |
| key | value |
| oc:shareroot | /secure.txt |
| oc:name | secure.txt |
| d:getcontenttype | text/plain |
| oc:permissions | SMX |
| d:getcontentlength | 14 |
Examples:
| dav-path-version |
| old |

View File

@@ -22,14 +22,13 @@ Feature: REPORT request to project space
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Alice" the REPORT response should contain a resource "testFile.txt" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | testFile.txt |
| d:getcontenttype | text/plain |
| oc:permissions | RDNVW |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 12 |
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | testFile.txt |
| d:getcontenttype | text/plain |
| oc:permissions | RDNVW |
| d:getcontentlength | 12 |
@issue-10329
Scenario: check the response of the searched sub-file
@@ -42,14 +41,13 @@ Feature: REPORT request to project space
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Alice" the REPORT response should contain a resource "insideTheFolder.txt" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | insideTheFolder.txt |
| d:getcontenttype | text/plain |
| oc:permissions | RDNVW |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 12 |
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | insideTheFolder.txt |
| d:getcontenttype | text/plain |
| oc:permissions | RDNVW |
| d:getcontentlength | 12 |
@issue-10329
Scenario: check the response of the searched folder
@@ -61,14 +59,13 @@ Feature: REPORT request to project space
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Alice" the REPORT response should contain a resource "folderMain" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | folderMain |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | RDNVCK |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:size | 0 |
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | folderMain |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | RDNVCK |
| oc:size | 0 |
@issue-10329
Scenario: check the response of the searched sub-folder
@@ -81,11 +78,10 @@ Feature: REPORT request to project space
| X-Request-Id | %request_id_pattern% |
And the HTTP status code should be "207"
And as user "Alice" the REPORT response should contain a resource "sub-folder" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | sub-folder |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | RDNVCK |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:size | 0 |
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | sub-folder |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | RDNVCK |
| oc:size | 0 |

View File

@@ -29,16 +29,15 @@ Feature: Report test
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "SubFolder1" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | SubFolder1 |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | S |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| oc:size | 12 |
| oc:remote-item-id | %file_id_pattern% |
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | SubFolder1 |
| d:getcontenttype | httpd/unix-directory |
| oc:permissions | S |
| oc:size | 12 |
| oc:remote-item-id | %file_id_pattern% |
Examples:
| dav-path-version |
| old |
@@ -60,16 +59,15 @@ Feature: Report test
And the following headers should match these regular expressions
| X-Request-Id | %request_id_pattern% |
And as user "Brian" the REPORT response should contain a resource "insideTheFolder.txt" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | insideTheFolder.txt |
| d:getcontenttype | text/plain |
| oc:permissions | SD |
| oc:privatelink | %base_url%/f/[0-9a-z-$%]+ |
| d:getcontentlength | 12 |
| oc:remote-item-id | %file_id_pattern% |
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:shareroot | /folderMain |
| oc:name | insideTheFolder.txt |
| d:getcontenttype | text/plain |
| oc:permissions | SD |
| d:getcontentlength | 12 |
| oc:remote-item-id | %file_id_pattern% |
Examples:
| dav-path-version |
| old |

View File

@@ -0,0 +1,64 @@
Feature: user profile photo
As a user, I want to provide my avatar to make my actions more visible
Background:
Given user "Alice" has been created with default attributes
Scenario Outline: add profile photo
When user "Alice" sets profile photo to "<file>" using the Graph API
Then the HTTP status code should be "<http-status-code>"
And for user "Alice" the profile photo should contain file "<file>"
Examples:
| file | http-status-code |
| filesForUpload/testavatar.jpg | 200 |
| filesForUpload/testavatar.png | 200 |
| filesForUpload/example.gif | 200 |
| filesForUpload/lorem.txt | 400 |
| filesForUpload/simple.pdf | 400 |
| filesForUpload/broken-image-file.png | 400 |
Scenario: user tries to get profile photo when none is set
When user "Alice" tries to get a profile photo using the Graph API
Then the HTTP status code should be "404"
Scenario Outline: get profile photo
Given user "Alice" has set the profile photo to "<file>"
When user "Alice" gets a profile photo using the Graph API
Then the HTTP status code should be "200"
And the profile photo should contain file "<file>"
Examples:
| file |
| filesForUpload/testavatar.jpg |
| filesForUpload/testavatar.png |
| filesForUpload/example.gif |
Scenario Outline: change profile photo
Given user "Alice" has set the profile photo to "filesForUpload/testavatar.jpg"
When user "Alice" changes the profile photo to "<file>" using the Graph API
Then the HTTP status code should be "<http-status-code>"
And for user "Alice" the profile photo should contain file "<file>"
Examples:
| file | http-status-code |
| filesForUpload/testavatar.jpg | 200 |
| filesForUpload/testavatar.png | 200 |
| filesForUpload/example.gif | 200 |
| filesForUpload/lorem.txt | 400 |
| filesForUpload/simple.pdf | 400 |
| filesForUpload/broken-image-file.png | 400 |
Scenario Outline: delete profile photo
Given user "Alice" has set the profile photo to "<file>"
When user "Alice" deletes the profile photo using the Graph API
Then the HTTP status code should be "200"
When user "Alice" tries to get a profile photo using the Graph API
Then the HTTP status code should be "404"
Examples:
| file |
| filesForUpload/testavatar.jpg |
| filesForUpload/testavatar.png |
| filesForUpload/example.gif |

View File

@@ -462,8 +462,80 @@ Feature: enable disable permissions role
"required": [
"eTag",
"id",
"permissions",
"webDavUrl"
]
],
"properties": {
"permissions": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"uniqueItems": true,
"items": {
"oneOf": [
{
"type": "object",
"required": ["grantedToV2", "roles"],
"properties": {
"grantedToV2": {
"type": "object",
"required": ["user"],
"properties": {
"user" : {
"type": "object",
"required": ["@libre.graph.userType", "displayName", "id"],
"properties": {
"@libre.graph.userType": { "const": "Member" },
"displayName": { "const": "Alice Hansen" },
"id": { "pattern": "^%user_id_pattern%$" }
}
}
}
},
"roles": { "pattern": "^%role_id_pattern%$" }
}
},
{
"type": "object",
"required": ["@libre.graph.permissions.actions", "grantedToV2"],
"properties": {
"@libre.graph.permissions.actions": {
"const": [
"libre.graph/driveItem/children/create",
"libre.graph/driveItem/standard/delete",
"libre.graph/driveItem/path/read",
"libre.graph/driveItem/quota/read",
"libre.graph/driveItem/content/read",
"libre.graph/driveItem/upload/create",
"libre.graph/driveItem/permissions/read",
"libre.graph/driveItem/children/read",
"libre.graph/driveItem/deleted/read",
"libre.graph/driveItem/path/update",
"libre.graph/driveItem/deleted/update",
"libre.graph/driveItem/basic/read"
]
},
"grantedToV2": {
"type": "object",
"required": ["user"],
"properties": {
"user": {
"type": "object",
"required": ["@libre.graph.userType", "displayName", "id"],
"properties": {
"@libre.graph.userType": { "const": "Member" },
"displayName": { "const": "Brian Murphy" },
"id": { "pattern": "^%user_id_pattern%$" }
}
}
}
}
}
}
]
}
}
}
}
}
}

View File

@@ -2250,7 +2250,7 @@ Feature: List a sharing permissions
}
"""
@env-config
Scenario: user lists permissions of a space after enabling 'Space Editor Without Versions' role
Given the administrator has enabled the permissions role "Space Editor Without Versions"
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
@@ -2567,206 +2567,3 @@ Feature: List a sharing permissions
}
}
"""
Scenario: user lists permissions of a folder in personal space with select filter
Given user "Brian" has been created with default attributes
And user "Alice" has created folder "folder"
And user "Alice" has sent the following resource share invitation:
| resource | folder |
| space | Personal |
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
When user "Alice" gets the permittion list of folder "folder" from the space "Personal" using the Graph API with query "$select=@libre.graph.permissions.roles.allowedValues"
Then the HTTP status code should be "200"
And the JSON data of the response should contain the following keys:
| @libre.graph.permissions.roles.allowedValues |
And the JSON data of the response should not contain the following keys:
| @libre.graph.permissions.actions.allowedValues |
| value |
When user "Alice" gets the permittion list of folder "folder" from the space "Personal" using the Graph API with query "$select=@libre.graph.permissions.actions.allowedValues"
Then the HTTP status code should be "200"
And the JSON data of the response should contain the following keys:
| @libre.graph.permissions.actions.allowedValues |
And the JSON data of the response should not contain the following keys:
| @libre.graph.permissions.roles.allowedValues |
| value |
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"@libre.graph.permissions.actions.allowedValues"
],
"properties": {
"@libre.graph.permissions.actions.allowedValues": {
"const": [
"libre.graph/driveItem/permissions/create",
"libre.graph/driveItem/children/create",
"libre.graph/driveItem/standard/delete",
"libre.graph/driveItem/path/read",
"libre.graph/driveItem/quota/read",
"libre.graph/driveItem/content/read",
"libre.graph/driveItem/upload/create",
"libre.graph/driveItem/permissions/read",
"libre.graph/driveItem/children/read",
"libre.graph/driveItem/versions/read",
"libre.graph/driveItem/deleted/read",
"libre.graph/driveItem/path/update",
"libre.graph/driveItem/permissions/delete",
"libre.graph/driveItem/deleted/delete",
"libre.graph/driveItem/versions/update",
"libre.graph/driveItem/deleted/update",
"libre.graph/driveItem/basic/read",
"libre.graph/driveItem/permissions/update",
"libre.graph/driveItem/permissions/deny"
]
}
}
}
"""
Scenario: user lists permissions of a project space with select filter
Given using spaces DAV path
And user "Brian" has been created with default attributes
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | Space Viewer |
When user "Alice" gets the drive permittion list of the space "new-space" using the Graph API with query "$select=@libre.graph.permissions.actions.allowedValues"
Then the HTTP status code should be "200"
And the JSON data of the response should contain the following keys:
| @libre.graph.permissions.actions.allowedValues |
And the JSON data of the response should not contain the following keys:
| @libre.graph.permissions.roles.allowedValues |
| value |
When user "Alice" gets the drive permittion list of the space "new-space" using the Graph API with query "$select=@libre.graph.permissions.roles.allowedValues"
Then the HTTP status code should be "200"
And the JSON data of the response should contain the following keys:
| @libre.graph.permissions.roles.allowedValues |
And the JSON data of the response should not contain the following keys:
| @libre.graph.permissions.actions.allowedValues |
| value |
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"@libre.graph.permissions.roles.allowedValues"
],
"properties": {
"@libre.graph.permissions.roles.allowedValues": {
"type": "array",
"minItems": 3,
"maxItems": 3,
"uniqueItems": true,
"items": {
"oneOf": [
{
"type": "object",
"required": [
"@libre.graph.weight",
"description",
"displayName",
"id"
],
"properties": {
"@libre.graph.weight": {
"const": 40
},
"description": {
"const": "View and download."
},
"displayName": {
"const": "Can view"
},
"id": {
"const": "a8d5fe5e-96e3-418d-825b-534dbdf22b99"
}
}
},
{
"type": "object",
"required": [
"@libre.graph.weight",
"description",
"displayName",
"id"
],
"properties": {
"@libre.graph.weight": {
"const": 90
},
"description": {
"const": "View, download, upload, edit, add, delete including the history."
},
"displayName": {
"const": "Can edit"
},
"id": {
"const": "58c63c02-1d89-4572-916a-870abc5a1b7d"
}
}
},
{
"type": "object",
"required": [
"@libre.graph.weight",
"description",
"displayName",
"id"
],
"properties": {
"@libre.graph.weight": {
"const": 120
},
"description": {
"const": "View, download, upload, edit, add, delete and manage members."
},
"displayName": {
"const": "Can manage"
},
"id": {
"const": "312c0871-5ef7-4b3a-85b6-0e4074c64049"
}
}
}
]
}
}
}
}
"""
Scenario: user lists permissions of a project space with count filter
Given using spaces DAV path
And user "Brian" has been created with default attributes
And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
And user "Alice" has created a space "new-space" with the default quota using the Graph API
And user "Alice" has sent the following space share invitation:
| space | new-space |
| sharee | Brian |
| shareType | user |
| permissionsRole | Space Viewer |
When user "Alice" gets the drive permittion list of the space "new-space" using the Graph API with query "$count=true&$top=0"
Then the HTTP status code should be "200"
And the JSON data of the response should match
"""
{
"type": "object",
"required": [
"@odata.count"
],
"properties": {
"@odata.count": {
"const": 2
}
}
}
"""

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

View File

@@ -0,0 +1 @@
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

View File

@@ -2,11 +2,11 @@
We look forward to your contributions, but ask that you first review these guidelines.
## Sign the CLA
### Sign the CLA
As Bleve is a Couchbase project we require contributors accept the [Couchbase Contributor License Agreement](http://review.couchbase.org/static/individual_agreement.html). To sign this agreement log into the Couchbase [code review tool](http://review.couchbase.org/). The Bleve project does not use this code review tool but it is still used to track acceptance of the contributor license agreements.
## Submitting a Pull Request
### Submitting a Pull Request
All types of contributions are welcome, but please keep the following in mind:

View File

@@ -16,41 +16,41 @@ A modern indexing + search library in GO
* Index any GO data structure or JSON
* Intelligent defaults backed up by powerful configuration ([scorch](https://github.com/blevesearch/bleve/blob/master/index/scorch/README.md))
* Supported field types:
* `text`, `number`, `datetime`, `boolean`, `geopoint`, `geoshape`, `IP`, `vector`
* `text`, `number`, `datetime`, `boolean`, `geopoint`, `geoshape`, `IP`, `vector`
* Supported query types:
* `term`, `phrase`, `match`, `match_phrase`, `prefix`, `regexp`, `wildcard`, `fuzzy`
* term range, numeric range, date range, boolean field
* compound queries: `conjuncts`, `disjuncts`, boolean (`must`/`should`/`must_not`)
* [query string syntax](http://www.blevesearch.com/docs/Query-String-Query/)
* [geo spatial search](https://github.com/blevesearch/bleve/blob/master/geo/README.md)
* approximate k-nearest neighbors via [vector search](https://github.com/blevesearch/bleve/blob/master/docs/vectors.md)
* [synonym search](https://github.com/blevesearch/bleve/blob/master/docs/synonyms.md)
* `term`, `phrase`, `match`, `match_phrase`, `prefix`, `regexp`, `wildcard`, `fuzzy`
* term range, numeric range, date range, boolean field
* compound queries: `conjuncts`, `disjuncts`, boolean (`must`/`should`/`must_not`)
* [query string syntax](http://www.blevesearch.com/docs/Query-String-Query/)
* [geo spatial search](https://github.com/blevesearch/bleve/blob/master/geo/README.md)
* approximate k-nearest neighbors via [vector search](https://github.com/blevesearch/bleve/blob/master/docs/vectors.md)
* [synonym search](https://github.com/blevesearch/bleve/blob/master/docs/synonyms.md)
* [tf-idf](https://github.com/blevesearch/bleve/blob/master/docs/scoring.md#tf-idf) / [bm25](https://github.com/blevesearch/bleve/blob/master/docs/scoring.md#bm25) scoring models
* Hybrid search: exact + semantic
* Query time boosting
* Search result match highlighting with document fragments
* Aggregations/faceting support:
* terms facet
* numeric range facet
* date range facet
* terms facet
* numeric range facet
* date range facet
## Indexing
```go
message := struct {
Id string
From string
Body string
message := struct{
Id string
From string
Body string
}{
Id: "example",
From: "xyz@couchbase.com",
Body: "bleve indexing is easy",
Id: "example",
From: "xyz@couchbase.com",
Body: "bleve indexing is easy",
}
mapping := bleve.NewIndexMapping()
index, err := bleve.New("example.bleve", mapping)
if err != nil {
panic(err)
panic(err)
}
index.Index(message.Id, message)
```
@@ -69,10 +69,10 @@ searchResult, _ := index.Search(searchRequest)
To install the CLI for the latest release of bleve, run:
```bash
go install github.com/blevesearch/bleve/v2/cmd/bleve@latest
$ go install github.com/blevesearch/bleve/v2/cmd/bleve@latest
```
```text
```
$ bleve --help
Bleve is a command-line tool to interact with a bleve index.
@@ -113,7 +113,6 @@ Arabic (ar), Bulgarian (bg), Catalan (ca), Chinese-Japanese-Korean (cjk), Kurdis
## Discussion/Issues
Discuss usage/development of bleve and/or report issues here:
* [Github issues](https://github.com/blevesearch/bleve/issues)
* [Google group](https://groups.google.com/forum/#!forum/bleve)

View File

@@ -2,12 +2,11 @@
## Supported Versions
We support the latest release (for example, bleve v2.5.x).
We support the latest release (for example, bleve v2.3.x).
## Reporting a Vulnerability
All security issues for this project should be reported via email to [security@couchbase.com](mailto:security@couchbase.com) and [fts-team@couchbase.com](mailto:fts-team@couchbase.com).
All security issues for this project should be reported by email to security@couchbase.com and fts-team@couchbase.com.
This mail will be delivered to the owners of this project.
- To ensure your report is NOT marked as spam, please include the word "security/vulnerability" along with the project name (blevesearch/bleve) in the subject of the email.

View File

@@ -1,7 +1,8 @@
# Geo spatial search support in bleve
Latest bleve spatial capabilities are powered by spatial hierarchical tokens generated from s2geometry.
You can find more details about the [s2geometry basics here](http://s2geometry.io/), and explore the extended functionality of our forked golang port of [s2geometry lib here](https://github.com/blevesearch/geo).
You can find more details about the [s2geometry basics here](http://s2geometry.io/), and explore the
extended functionality of our forked golang port of [s2geometry lib here](https://github.com/blevesearch/geo).
Users can continue to index and query `geopoint` field type and the existing queries like,
@@ -13,7 +14,7 @@ as before.
## New Spatial Field Type - geoshape
We have introduced a field type (`geoshape`) for representing the new spatial types.
We have introduced a field type (`geoshape`) for representing the new spatial types.
Using the new `geoshape` field type, users can unblock the spatial capabilities
for the [geojson](https://datatracker.ietf.org/doc/html/rfc7946) shapes like,
@@ -36,7 +37,7 @@ To specify GeoJSON data, use a nested field with:
- a field named type that specifies the GeoJSON object type and the type value will be case-insensitive.
- a field named coordinates that specifies the object's coordinates.
```text
```
"fieldName": {
"type": "GeoJSON Type",
"coordinates": <coordinates>
@@ -49,67 +50,69 @@ To specify GeoJSON data, use a nested field with:
- Shapes would be internally represented as geodesics.
- The GeoJSON specification strongly suggests splitting geometries so that neither of their parts crosses the antimeridian.
Examples for the various geojson shapes representations are as below.
## Point
The following specifies a [Point](https://tools.ietf.org/html/rfc7946#section-3.1.2) field in a document:
```json
{
"type": "point",
"coordinates": [75.05687713623047, 22.53539059204079]
}
```
{
"type": "point",
"coordinates": [75.05687713623047,22.53539059204079]
}
```
## Linestring
The following specifies a [Linestring](https://tools.ietf.org/html/rfc7946#section-3.1.4) field in a document:
```json
{
"type": "linestring",
"coordinates": [
[77.01416015625, 23.0797317624497],
[78.134765625, 20.385825381874263]
]
```
{
"type": "linestring",
"coordinates": [
[ 77.01416015625, 23.0797317624497],
[ 78.134765625, 20.385825381874263]
]
}
```
## Polygon
The following specifies a [Polygon](https://tools.ietf.org/html/rfc7946#section-3.1.6) field in a document:
```json
```
{
"type": "polygon",
"coordinates": [
[
[85.605, 57.207],
[86.396, 55.998],
[87.033, 56.716],
[85.605, 57.207]
]
]
"type": "polygon",
"coordinates": [ [ [ 85.605, 57.207],
[ 86.396, 55.998],
[ 87.033, 56.716],
[ 85.605, 57.207]
] ]
}
```
The first and last coordinates must match in order to close the polygon.
The first and last coordinates must match in order to close the polygon.
And the exterior coordinates have to be in Counter Clockwise Order in a polygon. (CCW)
## MultiPoint
The following specifies a [Multipoint](https://tools.ietf.org/html/rfc7946#section-3.1.3) field in a document:
```json
```
{
"type": "multipoint",
"coordinates": [
[-115.8343505859375, 38.45789034424927],
[-115.81237792968749, 38.19502155795575],
[-120.80017089843749, 36.54053616262899],
[-120.67932128906249, 36.33725319397006]
]
"type": "multipoint",
"coordinates": [
[ -115.8343505859375, 38.45789034424927],
[ -115.81237792968749, 38.19502155795575],
[ -120.80017089843749, 36.54053616262899],
[ -120.67932128906249, 36.33725319397006]
]
}
```
@@ -117,23 +120,14 @@ The following specifies a [Multipoint](https://tools.ietf.org/html/rfc7946#secti
The following specifies a [MultiLineString](https://tools.ietf.org/html/rfc7946#section-3.1.5) field in a document:
```json
```
{
"type": "multilinestring",
"coordinates": [
[
[-118.31726074, 35.250105158],
[-117.509765624, 35.3756141]
],
[
[-118.696289, 34.624167789],
[-118.317260742, 35.03899204]
],
[
[-117.9492187, 35.146862906],
[-117.6745605, 34.41144164]
]
]
"type": "multilinestring",
"coordinates": [
[ [ -118.31726074, 35.250105158],[ -117.509765624, 35.3756141] ],
[ [ -118.6962890, 34.624167789],[ -118.317260742, 35.03899204] ],
[ [ -117.9492187, 35.146862906], [ -117.6745605, 34.41144164] ]
]
}
```
@@ -141,138 +135,112 @@ The following specifies a [MultiLineString](https://tools.ietf.org/html/rfc7946#
The following specifies a [MultiPolygon](https://tools.ietf.org/html/rfc7946#section-3.1.7) field in a document:
```json
```
{
"type": "multipolygon",
"coordinates": [
[
[
[-73.958, 40.8003],
[-73.9498, 40.7968],
[-73.9737, 40.7648],
[-73.9814, 40.7681],
[-73.958, 40.8003]
]
],
[
[
[-73.958, 40.8003],
[-73.9498, 40.7968],
[-73.9737, 40.7648],
[-73.958, 40.8003]
]
]
]
"type": "multipolygon",
"coordinates": [
[ [ [ -73.958, 40.8003 ], [ -73.9498, 40.7968 ],
[ -73.9737, 40.7648 ], [ -73.9814, 40.7681 ],
[ -73.958, 40.8003 ] ] ],
[ [ [ -73.958, 40.8003 ], [ -73.9498, 40.7968 ],
[ -73.9737, 40.7648 ], [ -73.958, 40.8003 ] ] ]
]
}
```
## GeometryCollection
The following specifies a [GeometryCollection](https://tools.ietf.org/html/rfc7946#section-3.1.8) field in a document:
```json
```
{
"type": "geometrycollection",
"geometries": [
"type": "geometrycollection",
"geometries": [
{
"type": "multipoint",
"coordinates": [
[-73.958, 40.8003],
[-73.9498, 40.7968],
[-73.9737, 40.7648],
[-73.9814, 40.7681]
[ -73.9580, 40.8003 ],
[ -73.9498, 40.7968 ],
[ -73.9737, 40.7648 ],
[ -73.9814, 40.7681 ]
]
},
{
"type": "multilinestring",
"coordinates": [
[
[-73.96943, 40.78519],
[-73.96082, 40.78095]
],
[
[-73.96415, 40.79229],
[-73.95544, 40.78854]
],
[
[-73.97162, 40.78205],
[-73.96374, 40.77715]
],
[
[-73.9788, 40.77247],
[-73.97036, 40.76811]
]
[ [ -73.96943, 40.78519 ], [ -73.96082, 40.78095 ] ],
[ [ -73.96415, 40.79229 ], [ -73.95544, 40.78854 ] ],
[ [ -73.97162, 40.78205 ], [ -73.96374, 40.77715 ] ],
[ [ -73.97880, 40.77247 ], [ -73.97036, 40.76811 ] ]
]
},
{
"type": "polygon",
"coordinates": [
[
[0, 0],
[3, 6],
[6, 1],
[0, 0]
],
[
[2, 2],
[3, 3],
[4, 2],
[2, 2]
]
]
}
]
"type" : "polygon",
"coordinates" : [
[ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0 ] ],
[ [ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 2 ] , [ 2 , 2 ] ]
]
}
]
}
```
## Circle
If the user wishes to cover a circular region over the earth's surface, then they could use this shape.
If the user wishes to cover a circular region over the earths surface, then they could use this shape.
A sample circular shape is as below.
```json
{
"type": "circle",
"coordinates": [75.05687713623047, 22.53539059204079],
"radius": "1000m"
```
{
"type": "circle",
"coordinates": [75.05687713623047,22.53539059204079],
"radius": "1000m"
}
```
Circle is specified over the center point coordinates along with the radius.
Example formats supported for radius are:
"5in" , "5inch" , "7yd" , "7yards", "9ft" , "9feet", "11km", "11kilometers", "3nm", "3nauticalmiles", "13mm" , "13millimeters", "15cm", "15centimeters", "17mi", "17miles", "19m" or "19meters".
Example formats supported for radius are:
"5in" , "5inch" , "7yd" , "7yards", "9ft" , "9feet", "11km", "11kilometers", "3nm"
"3nauticalmiles", "13mm" , "13millimeters", "15cm", "15centimeters", "17mi", "17miles" "19m" or "19meters".
If the unit cannot be determined, the entire string is parsed and the unit of meters is assumed.
## Envelope
Envelope type, which consists of coordinates for upper left and lower right points of the shape to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]].
Envelope type, which consists of coordinates for upper left and lower right points of the shape
to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]].
```json
```
{
"type": "envelope",
"coordinates": [
[72.83, 18.979],
[78.508, 17.4555]
]
"type": "envelope",
"coordinates": [
[72.83, 18.979],
[78.508,17.4555]
]
}
```
## GeoShape Query
Geoshape query support three types/filters of spatial querying capability across those heterogeneous types of documents indexed.
Geoshape query support three types/filters of spatial querying capability across those
heterogeneous types of documents indexed.
### Query Structure
### Query Structure:
```json
```
{
"query": {
"geometry": {
"shape": {
"type": "<shapeType>",
"coordinates": [
[[]]
]
"type": "<shapeType>",
"coordinates": [[[ ]]]
},
"relation": "<<filterName>>"
}
@@ -280,6 +248,7 @@ Geoshape query support three types/filters of spatial querying capability across
}
```
*shapeType* => can be any of the aforementioned types like Point, LineString, Polygon, MultiPoint,
Geometrycollection, MultiLineString, MultiPolygon, Circle and Envelope.
@@ -287,14 +256,16 @@ Geometrycollection, MultiLineString, MultiPolygon, Circle and Envelope.
### Relation
| FilterName | Description |
| :-----------:| :-----------------------------------------------------------------: |
| `intersects` | Return all documents whose shape field intersects the query geometry. |
| `contains` | Return all documents whose shape field contains the query geometry |
| `within` | Return all documents whose shape field is within the query geometry. |
| FilterName | Description |
| :-----------:| :-----------------------------------------------------------------: |
| `intersects` | Return all documents whose shape field intersects the query geometry. |
| `contains` | Return all documents whose shape field contains the query geometry |
| `within` | Return all documents whose shape field is within the query geometry. |
------------------------------------------------------------------------------------------------------------------------
### Older Implementation
First, all of this geo code is a Go adaptation of the [Lucene 5.3.2 sandbox geo support](https://lucene.apache.org/core/5_3_2/sandbox/org/apache/lucene/util/package-summary.html).

View File

@@ -3,16 +3,13 @@
## Definitions
Batch
- A collection of Documents to mutate in the index.
Document
- Has a unique identifier (arbitrary bytes).
- Is comprised of a list of fields.
Field
- Has a name (string).
- Has a type (text, number, date, geopoint).
- Has a value (depending on type).
@@ -44,7 +41,7 @@ NOTE: If a document already contains a field \_id, it will be replaced. If this
### Proposed Structures
```go
```
type Segment interface {
Dictionary(field string) TermDictionary
@@ -95,11 +92,9 @@ type IndexSnapshot struct {
segment []SegmentSnapshot
}
```
**What about errors?**
**What about memory mgmnt or context?**
**Postings List separate iterator to separate stateful from stateless**
### Mutating the Index
The bleve.index API has methods for directly making individual mutations (Update/Delete/SetInternal/DeleteInternal), however for this first implementation, we assume that all of these calls can simply be turned into a Batch of size 1. This may be highly inefficient, but it will be correct. This decision is made based on the fact that Couchbase FTS always uses Batches.
@@ -110,9 +105,9 @@ From this point forward, only Batch mutations will be discussed.
Sequence of Operations:
1. For each document in the batch, search through all existing segments. The goal is to build up a per-segment bitset which tells us which documents in that segment are obsoleted by the addition of the new segment we're currently building. NOTE: we're not ready for this change to take effect yet, so rather than this operation mutating anything, they simply return bitsets, which we can apply later. Logically, this is something like:
1. For each document in the batch, search through all existing segments. The goal is to build up a per-segment bitset which tells us which documents in that segment are obsoleted by the addition of the new segment we're currently building. NOTE: we're not ready for this change to take effect yet, so rather than this operation mutating anything, they simply return bitsets, which we can apply later. Logically, this is something like:
```go
```
foreach segment {
dict := segment.Dictionary("\_id")
postings := empty postings list
@@ -124,21 +119,21 @@ Sequence of Operations:
NOTE: it is illustrated above as nested for loops, but some or all of these could be concurrently. The end result is that for each segment, we have (possibly empty) bitset.
2. Also concurrent with 1, the documents in the batch are analyzed. This analysis proceeds using the existing analyzer pool.
2. Also concurrent with 1, the documents in the batch are analyzed. This analysis proceeds using the existing analyzer pool.
3. (after 2 completes) Analyzed documents are fed into a function which builds a new Segment representing this information.
4. We now have everything we need to update the state of the system to include this new snapshot.
- Acquire a lock
- Create a new IndexSnapshot
- For each SegmentSnapshot in the IndexSnapshot, take the deleted PostingsList and OR it with the new postings list for this Segment. Construct a new SegmentSnapshot for the segment using this new deleted PostingsList. Append this SegmentSnapshot to the IndexSnapshot.
- Create a new SegmentSnapshot wrapping our new segment with nil deleted docs.
- Append the new SegmentSnapshot to the IndexSnapshot
- Release the lock
4. We now have everything we need to update the state of the system to include this new snapshot.
- Acquire a lock
- Create a new IndexSnapshot
- For each SegmentSnapshot in the IndexSnapshot, take the deleted PostingsList and OR it with the new postings list for this Segment. Construct a new SegmentSnapshot for the segment using this new deleted PostingsList. Append this SegmentSnapshot to the IndexSnapshot.
- Create a new SegmentSnapshot wrapping our new segment with nil deleted docs.
- Append the new SegmentSnapshot to the IndexSnapshot
- Release the lock
An ASCII art example:
```text
```
0 - Empty Index
No segments
@@ -214,7 +209,7 @@ Term search is the only searching primitive exposed in today's bleve.index API.
A term search for term T in field F will look something like this:
```go
```
searchResultPostings = empty
foreach segment {
dict := segment.Dictionary(F)
@@ -227,31 +222,31 @@ The searchResultPostings will be a new implementation of the TermFieldReader int
As a reminder this interface is:
```go
```
// TermFieldReader is the interface exposing the enumeration of documents
// containing a given term in a given field. Documents are returned in byte
// lexicographic order over their identifiers.
type TermFieldReader interface {
// Next returns the next document containing the term in this field, or nil
// when it reaches the end of the enumeration. The preAlloced TermFieldDoc
// is optional, and when non-nil, will be used instead of allocating memory.
Next(preAlloced *TermFieldDoc) (*TermFieldDoc, error)
// Next returns the next document containing the term in this field, or nil
// when it reaches the end of the enumeration. The preAlloced TermFieldDoc
// is optional, and when non-nil, will be used instead of allocating memory.
Next(preAlloced *TermFieldDoc) (*TermFieldDoc, error)
// Advance resets the enumeration at specified document or its immediate
// follower.
Advance(ID IndexInternalID, preAlloced *TermFieldDoc) (*TermFieldDoc, error)
// Advance resets the enumeration at specified document or its immediate
// follower.
Advance(ID IndexInternalID, preAlloced *TermFieldDoc) (*TermFieldDoc, error)
// Count returns the number of documents contains the term in this field.
Count() uint64
Close() error
// Count returns the number of documents contains the term in this field.
Count() uint64
Close() error
}
```
At first glance this appears problematic, we have no way to return documents in order of their identifiers. But it turns out the wording of this perhaps too strong, or a bit ambiguous. Originally, this referred to the external identifiers, but with the introduction of a distinction between internal/external identifiers, returning them in order of their internal identifiers is also acceptable. **ASIDE**: the reason for this is that most callers just use Next() and literally don't care what the order is, they could be in any order and it would be fine. There is only one search that cares and that is the ConjunctionSearcher, which relies on Next/Advance having very specific semantics. Later in this document we will have a proposal to split into multiple interfaces:
- The weakest interface, only supports Next() no ordering at all.
- Ordered, supporting Advance()
- And/Or'able capable of internally efficiently doing these ops with like interfaces (if not capable then can always fall back to external walking)
- The weakest interface, only supports Next() no ordering at all.
- Ordered, supporting Advance()
- And/Or'able capable of internally efficiently doing these ops with like interfaces (if not capable then can always fall back to external walking)
But, the good news is that we don't even have to do that for our first implementation. As long as the global numbers we use for internal identifiers are consistent within this IndexSnapshot, then Next() will be ordered by ascending document number, and Advance() will still work correctly.
@@ -259,7 +254,7 @@ NOTE: there is another place where we rely on the ordering of these hits, and th
An ASCII art example:
```text
```
Let's start with the IndexSnapshot we ended with earlier:
3 - Index Batch [ C' ]
@@ -325,6 +320,7 @@ In the future, interfaces to detect these non-serially operating TermFieldReader
Another related topic is that of peak memory usage. With serially operating TermFieldReaders it was necessary to start them all at the same time and operate in unison. However, with these non-serially operating TermFieldReaders we have the option of doing a few at a time, consolidating them, dispoting the intermediaries, and then doing a few more. For very complex queries with many clauses this could reduce peak memory usage.
### Memory Tracking
All segments must be able to produce two statistics, an estimate of their explicit memory usage, and their actual size on disk (if any). For in-memory segments, disk usage could be zero, and the memory usage represents the entire information content. For mmap-based disk segments, the memory could be as low as the size of tracking structure itself (say just a few pointers).
@@ -339,12 +335,14 @@ At runtime, the state of an index (it's IndexSnapshot) is not only the contents
This also relates to the topic rollback, addressed next...
### Rollback
One desirable property in the Couchbase ecosystem is the ability to rollback to some previous (though typically not long ago) state. One idea for keeping this property in this design is to protect some of the most recent segments from merging. Then, if necessary, they could be "undone" to reveal previous states of the system. In these scenarios "undone" has to properly undo the deleted bitmasks on the other segments. Again, the current thinking is that rather than "undo" anything, it could be work that was deferred in the first place, thus making it easier to logically undo.
Another possibly related approach would be to tie this into our existing snapshot mechanism. Perhaps simulating a slow reader (holding onto index snapshots) for some period of time, can be the mechanism to achieve the desired end goal.
### Internal Storage
The bleve.index API has support for "internal storage". The ability to store information under a separate name space.

View File

@@ -295,10 +295,8 @@ func plan(segmentsIn []Segment, o *MergePlanOptions) (*MergePlan, error) {
if len(bestRoster) == 0 {
return rv, nil
}
// create tasks with valid merges - i.e. there should be atleast 2 non-empty segments
if len(bestRoster) > 1 {
rv.Tasks = append(rv.Tasks, &MergeTask{Segments: bestRoster})
}
rv.Tasks = append(rv.Tasks, &MergeTask{Segments: bestRoster})
eligibles = removeSegments(eligibles, bestRoster)
}

View File

@@ -393,7 +393,5 @@ func (i *IndexSnapshot) unadornedTermFieldReader(
includeNorm: false,
includeTermVectors: false,
recycle: false,
// signal downstream that this is a special unadorned termFieldReader
unadorned: true,
}
}

View File

@@ -50,7 +50,6 @@ type IndexSnapshotTermFieldReader struct {
recycle bool
bytesRead uint64
ctx context.Context
unadorned bool
}
func (i *IndexSnapshotTermFieldReader) incrementBytesRead(val uint64) {
@@ -147,29 +146,14 @@ func (i *IndexSnapshotTermFieldReader) Advance(ID index.IndexInternalID, preAllo
// FIXME do something better
// for now, if we need to seek backwards, then restart from the beginning
if i.currPosting != nil && bytes.Compare(i.currID, ID) >= 0 {
// Check if the TFR is a special unadorned composite optimization.
// Such a TFR will NOT have a valid `term` or `field` set, making it
// impossible for the TFR to replace itself with a new one.
if !i.unadorned {
i2, err := i.snapshot.TermFieldReader(context.TODO(), i.term, i.field,
i.includeFreq, i.includeNorm, i.includeTermVectors)
if err != nil {
return nil, err
}
// close the current term field reader before replacing it with a new one
_ = i.Close()
*i = *(i2.(*IndexSnapshotTermFieldReader))
} else {
// unadorned composite optimization
// we need to reset all the iterators
// back to the beginning, which effectively
// achives the same thing as the above
for _, iter := range i.iterators {
if optimizedIterator, ok := iter.(ResetablePostingsIterator); ok {
optimizedIterator.ResetIterator()
}
}
i2, err := i.snapshot.TermFieldReader(context.TODO(), i.term, i.field,
i.includeFreq, i.includeNorm, i.includeTermVectors)
if err != nil {
return nil, err
}
// close the current term field reader before replacing it with a new one
_ = i.Close()
*i = *(i2.(*IndexSnapshotTermFieldReader))
}
num, err := docInternalToNumber(ID)
if err != nil {

View File

@@ -96,12 +96,6 @@ func (i *unadornedPostingsIteratorBitmap) ReplaceActual(actual *roaring.Bitmap)
i.actual = actual.Iterator()
}
// Resets the iterator to the beginning of the postings list.
// by resetting the actual iterator.
func (i *unadornedPostingsIteratorBitmap) ResetIterator() {
i.actual = i.actualBM.Iterator()
}
func newUnadornedPostingsIteratorFromBitmap(bm *roaring.Bitmap) segment.PostingsIterator {
return &unadornedPostingsIteratorBitmap{
actualBM: bm,
@@ -112,8 +106,7 @@ func newUnadornedPostingsIteratorFromBitmap(bm *roaring.Bitmap) segment.Postings
const docNum1HitFinished = math.MaxUint64
type unadornedPostingsIterator1Hit struct {
docNumOrig uint64 // original 1-hit docNum used to create this iterator
docNum uint64 // current docNum
docNum uint64
}
func (i *unadornedPostingsIterator1Hit) Next() (segment.Posting, error) {
@@ -160,22 +153,12 @@ func (i *unadornedPostingsIterator1Hit) BytesWritten() uint64 {
func (i *unadornedPostingsIterator1Hit) ResetBytesRead(uint64) {}
// ResetIterator resets the iterator to the original state.
func (i *unadornedPostingsIterator1Hit) ResetIterator() {
i.docNum = i.docNumOrig
}
func newUnadornedPostingsIteratorFrom1Hit(docNum1Hit uint64) segment.PostingsIterator {
return &unadornedPostingsIterator1Hit{
docNumOrig: docNum1Hit,
docNum: docNum1Hit,
docNum1Hit,
}
}
type ResetablePostingsIterator interface {
ResetIterator()
}
type UnadornedPosting uint64
func (p UnadornedPosting) Number() uint64 {

View File

@@ -20,6 +20,7 @@ import (
"strconv"
"time"
"github.com/blevesearch/bleve/v2/numeric"
"github.com/blevesearch/bleve/v2/search"
"github.com/blevesearch/bleve/v2/size"
index "github.com/blevesearch/bleve_index_api"
@@ -500,7 +501,23 @@ func (hc *TopNCollector) finalizeResults(r index.IndexReader) error {
doc.Complete(nil)
return nil
})
if err != nil {
return err
}
// Decode geo sort keys back to its distance values
for i, so := range hc.sort {
if _, ok := so.(*search.SortGeoDistance); ok {
for _, dm := range hc.results {
// The string is a int64 bit representation of a float64 distance
distInt, err := numeric.PrefixCoded(dm.Sort[i]).Int64()
if err != nil {
return err
}
dm.Sort[i] = strconv.FormatFloat(numeric.Int64ToFloat64(distInt), 'f', -1, 64)
}
}
}
return err
}

View File

@@ -154,7 +154,6 @@ type DocumentMatch struct {
Locations FieldTermLocationMap `json:"locations,omitempty"`
Fragments FieldFragmentMap `json:"fragments,omitempty"`
Sort []string `json:"sort,omitempty"`
DecodedSort []string `json:"decoded_sort,omitempty"`
// Fields contains the values for document fields listed in
// SearchRequest.Fields. Text fields are returned as strings, numeric
@@ -225,7 +224,6 @@ func (dm *DocumentMatch) Reset() *DocumentMatch {
dm.IndexInternalID = indexInternalID[:0]
// reuse the []interface{} already allocated (and reset len to 0)
dm.Sort = sort[:0]
dm.DecodedSort = dm.DecodedSort[:0]
// reuse the FieldTermLocations already allocated (and reset len to 0)
dm.FieldTermLocations = ftls[:0]
return dm
@@ -265,10 +263,6 @@ func (dm *DocumentMatch) Size() int {
sizeInBytes += size.SizeOfString + len(entry)
}
for _, entry := range dm.DecodedSort {
sizeInBytes += size.SizeOfString + len(entry)
}
for k := range dm.Fields {
sizeInBytes += size.SizeOfString + len(k) +
size.SizeOfPtr

View File

@@ -20,9 +20,7 @@ import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/blevesearch/bleve/v2/geo"
@@ -38,7 +36,6 @@ var (
type SearchSort interface {
UpdateVisitor(field string, term []byte)
Value(a *DocumentMatch) string
DecodeValue(value string) string
Descending() bool
RequiresDocID() bool
@@ -215,9 +212,7 @@ type SortOrder []SearchSort
func (so SortOrder) Value(doc *DocumentMatch) {
for _, soi := range so {
value := soi.Value(doc)
doc.Sort = append(doc.Sort, value)
doc.DecodedSort = append(doc.DecodedSort, soi.DecodeValue(value))
doc.Sort = append(doc.Sort, soi.Value(doc))
}
}
@@ -395,25 +390,6 @@ func (s *SortField) Value(i *DocumentMatch) string {
return iTerm
}
func (s *SortField) DecodeValue(value string) string {
switch s.Type {
case SortFieldAsNumber:
i64, err := numeric.PrefixCoded(value).Int64()
if err != nil {
return value
}
return strconv.FormatFloat(numeric.Int64ToFloat64(i64), 'f', -1, 64)
case SortFieldAsDate:
i64, err := numeric.PrefixCoded(value).Int64()
if err != nil {
return value
}
return time.Unix(0, i64).UTC().String()
default:
return value
}
}
// Descending determines the order of the sort
func (s *SortField) Descending() bool {
return s.Desc
@@ -569,10 +545,6 @@ func (s *SortDocID) Value(i *DocumentMatch) string {
return i.ID
}
func (s *SortDocID) DecodeValue(value string) string {
return value
}
// Descending determines the order of the sort
func (s *SortDocID) Descending() bool {
return s.Desc
@@ -618,10 +590,6 @@ func (s *SortScore) Value(i *DocumentMatch) string {
return "_score"
}
func (s *SortScore) DecodeValue(value string) string {
return value
}
// Descending determines the order of the sort
func (s *SortScore) Descending() bool {
return s.Desc
@@ -726,14 +694,6 @@ func (s *SortGeoDistance) Value(i *DocumentMatch) string {
return string(numeric.MustNewPrefixCodedInt64(distInt64, 0))
}
func (s *SortGeoDistance) DecodeValue(value string) string {
distInt, err := numeric.PrefixCoded(value).Int64()
if err != nil {
return ""
}
return strconv.FormatFloat(numeric.Int64ToFloat64(distInt), 'f', -1, 64)
}
// Descending determines the order of the sort
func (s *SortGeoDistance) Descending() bool {
return s.Desc

View File

@@ -106,7 +106,7 @@ func (di *docValueReader) curChunkNumber() uint64 {
return di.curChunkNum
}
func (sb *SegmentBase) loadFieldDocValueReader(field string,
func (s *SegmentBase) loadFieldDocValueReader(field string,
fieldDvLocStart, fieldDvLocEnd uint64) (*docValueReader, error) {
// get the docValue offset for the given fields
if fieldDvLocStart == fieldNotUninverted {
@@ -118,15 +118,15 @@ func (sb *SegmentBase) loadFieldDocValueReader(field string,
var numChunks, chunkOffsetsPosition uint64
if fieldDvLocEnd-fieldDvLocStart > 16 {
numChunks = binary.BigEndian.Uint64(sb.mem[fieldDvLocEnd-8 : fieldDvLocEnd])
numChunks = binary.BigEndian.Uint64(s.mem[fieldDvLocEnd-8 : fieldDvLocEnd])
// read the length of chunk offsets
chunkOffsetsLen := binary.BigEndian.Uint64(sb.mem[fieldDvLocEnd-16 : fieldDvLocEnd-8])
chunkOffsetsLen := binary.BigEndian.Uint64(s.mem[fieldDvLocEnd-16 : fieldDvLocEnd-8])
// acquire position of chunk offsets
chunkOffsetsPosition = (fieldDvLocEnd - 16) - chunkOffsetsLen
// 16 bytes since it corresponds to the length
// of chunk offsets and the position of the offsets
sb.incrementBytesRead(16)
s.incrementBytesRead(16)
} else {
return nil, fmt.Errorf("loadFieldDocValueReader: fieldDvLoc too small: %d-%d", fieldDvLocEnd, fieldDvLocStart)
}
@@ -140,14 +140,14 @@ func (sb *SegmentBase) loadFieldDocValueReader(field string,
// read the chunk offsets
var offset uint64
for i := 0; i < int(numChunks); i++ {
loc, read := binary.Uvarint(sb.mem[chunkOffsetsPosition+offset : chunkOffsetsPosition+offset+binary.MaxVarintLen64])
loc, read := binary.Uvarint(s.mem[chunkOffsetsPosition+offset : chunkOffsetsPosition+offset+binary.MaxVarintLen64])
if read <= 0 {
return nil, fmt.Errorf("corrupted chunk offset during segment load")
}
fdvIter.chunkOffsets[i] = loc
offset += uint64(read)
}
sb.incrementBytesRead(offset)
s.incrementBytesRead(offset)
// set the data offset
fdvIter.dvDataLoc = fieldDvLocStart
return fdvIter, nil
@@ -286,15 +286,15 @@ func (di *docValueReader) getDocValueLocs(docNum uint64) (uint64, uint64) {
// VisitDocValues is an implementation of the
// DocValueVisitable interface
func (sb *SegmentBase) VisitDocValues(localDocNum uint64, fields []string,
func (s *SegmentBase) VisitDocValues(localDocNum uint64, fields []string,
visitor index.DocValueVisitor, dvsIn segment.DocVisitState) (
segment.DocVisitState, error) {
dvs, ok := dvsIn.(*docVisitState)
if !ok || dvs == nil {
dvs = &docVisitState{}
} else {
if dvs.segment != sb {
dvs.segment = sb
if dvs.segment != s {
dvs.segment = s
dvs.dvrs = nil
dvs.bytesRead = 0
}
@@ -304,11 +304,11 @@ func (sb *SegmentBase) VisitDocValues(localDocNum uint64, fields []string,
if dvs.dvrs == nil {
dvs.dvrs = make(map[uint16]*docValueReader, len(fields))
for _, field := range fields {
if fieldIDPlus1, ok = sb.fieldsMap[field]; !ok {
if fieldIDPlus1, ok = s.fieldsMap[field]; !ok {
continue
}
fieldID := fieldIDPlus1 - 1
if dvIter, exists := sb.fieldDvReaders[SectionInvertedTextIndex][fieldID]; exists &&
if dvIter, exists := s.fieldDvReaders[SectionInvertedTextIndex][fieldID]; exists &&
dvIter != nil {
dvs.dvrs[fieldID] = dvIter.cloneInto(dvs.dvrs[fieldID])
}
@@ -324,14 +324,14 @@ func (sb *SegmentBase) VisitDocValues(localDocNum uint64, fields []string,
docInChunk := localDocNum / chunkFactor
var dvr *docValueReader
for _, field := range fields {
if fieldIDPlus1, ok = sb.fieldsMap[field]; !ok {
if fieldIDPlus1, ok = s.fieldsMap[field]; !ok {
continue
}
fieldID := fieldIDPlus1 - 1
if dvr, ok = dvs.dvrs[fieldID]; ok && dvr != nil {
// check if the chunk is already loaded
if docInChunk != dvr.curChunkNumber() {
err := dvr.loadDvChunk(docInChunk, sb)
err := dvr.loadDvChunk(docInChunk, s)
if err != nil {
return dvs, err
}
@@ -349,6 +349,6 @@ func (sb *SegmentBase) VisitDocValues(localDocNum uint64, fields []string,
// VisitableDocValueFields returns the list of fields with
// persisted doc value terms ready to be visitable using the
// VisitDocumentFieldTerms method.
func (sb *SegmentBase) VisitableDocValueFields() ([]string, error) {
return sb.fieldDvNames, nil
func (s *SegmentBase) VisitableDocValueFields() ([]string, error) {
return s.fieldDvNames, nil
}

View File

@@ -52,19 +52,31 @@ func (vc *vectorIndexCache) Clear() {
vc.m.Unlock()
}
// loadOrCreate obtains the vector index from the cache or creates it if it's not
// present. It also returns the batch executor for the field if it's present in the
// cache.
// loadDocVecIDMap indicates if a non-nil docVecIDMap should be returned.
// It is true when a filtered kNN query accesses the cache since it requires the
// map. It's false otherwise.
func (vc *vectorIndexCache) loadOrCreate(fieldID uint16, mem []byte,
loadDocVecIDMap bool, except *roaring.Bitmap) (
index *faiss.IndexImpl, vecDocIDMap map[int64]uint32, docVecIDMap map[uint32][]int64,
vecIDsToExclude []int64, err error) {
index, vecDocIDMap, docVecIDMap, vecIDsToExclude, err = vc.loadFromCache(
fieldID, loadDocVecIDMap, mem, except)
return index, vecDocIDMap, docVecIDMap, vecIDsToExclude, err
}
// function to load the vectorDocIDMap and if required, docVecIDMap from cache
// If not, it will create these and add them to the cache.
func (vc *vectorIndexCache) loadFromCache(fieldID uint16, loadDocVecIDMap bool,
mem []byte, except *roaring.Bitmap) (index *faiss.IndexImpl, vecDocIDMap map[int64]uint32,
docVecIDMap map[uint32][]int64, vecIDsToExclude []int64, err error) {
vc.m.RLock()
entry, ok := vc.cache[fieldID]
if ok {
index, vecDocIDMap, docVecIDMap = entry.load()
vecIDsToExclude = getVecIDsToExclude(vecDocIDMap, except)
if !loadDocVecIDMap || len(entry.docVecIDMap) > 0 {
if !loadDocVecIDMap || (loadDocVecIDMap && len(entry.docVecIDMap) > 0) {
vc.m.RUnlock()
return index, vecDocIDMap, docVecIDMap, vecIDsToExclude, nil
}
@@ -114,7 +126,7 @@ func (vc *vectorIndexCache) createAndCacheLOCKED(fieldID uint16, mem []byte,
if entry != nil {
index, vecDocIDMap, docVecIDMap = entry.load()
vecIDsToExclude = getVecIDsToExclude(vecDocIDMap, except)
if !loadDocVecIDMap || len(entry.docVecIDMap) > 0 {
if !loadDocVecIDMap || (loadDocVecIDMap && len(entry.docVecIDMap) > 0) {
return index, vecDocIDMap, docVecIDMap, vecIDsToExclude, nil
}
docVecIDMap = vc.addDocVecIDMapToCacheLOCKED(entry)

View File

@@ -104,44 +104,44 @@ func (vpl *VecPostingsList) Iterator(prealloc segment.VecPostingsIterator) segme
return vpl.iterator(preallocPI)
}
func (vpl *VecPostingsList) iterator(rv *VecPostingsIterator) *VecPostingsIterator {
func (p *VecPostingsList) iterator(rv *VecPostingsIterator) *VecPostingsIterator {
if rv == nil {
rv = &VecPostingsIterator{}
} else {
*rv = VecPostingsIterator{} // clear the struct
}
// think on some of the edge cases over here.
if vpl.postings == nil {
if p.postings == nil {
return rv
}
rv.postings = vpl
rv.all = vpl.postings.Iterator()
if vpl.except != nil {
rv.ActualBM = roaring64.AndNot(vpl.postings, vpl.except)
rv.postings = p
rv.all = p.postings.Iterator()
if p.except != nil {
rv.ActualBM = roaring64.AndNot(p.postings, p.except)
rv.Actual = rv.ActualBM.Iterator()
} else {
rv.ActualBM = vpl.postings
rv.ActualBM = p.postings
rv.Actual = rv.all // Optimize to use same iterator for all & Actual.
}
return rv
}
func (vpl *VecPostingsList) Size() int {
func (p *VecPostingsList) Size() int {
sizeInBytes := reflectStaticSizeVecPostingsList + SizeOfPtr
if vpl.except != nil {
sizeInBytes += int(vpl.except.GetSizeInBytes())
if p.except != nil {
sizeInBytes += int(p.except.GetSizeInBytes())
}
return sizeInBytes
}
func (vpl *VecPostingsList) Count() uint64 {
if vpl.postings != nil {
n := vpl.postings.GetCardinality()
func (p *VecPostingsList) Count() uint64 {
if p.postings != nil {
n := p.postings.GetCardinality()
var e uint64
if vpl.except != nil {
e = vpl.postings.AndCardinality(vpl.except)
if p.except != nil {
e = p.postings.AndCardinality(p.except)
}
return n - e
}
@@ -171,51 +171,51 @@ type VecPostingsIterator struct {
next VecPosting // reused across Next() calls
}
func (vpItr *VecPostingsIterator) nextCodeAtOrAfterClean(atOrAfter uint64) (uint64, bool, error) {
vpItr.Actual.AdvanceIfNeeded(atOrAfter)
func (i *VecPostingsIterator) nextCodeAtOrAfterClean(atOrAfter uint64) (uint64, bool, error) {
i.Actual.AdvanceIfNeeded(atOrAfter)
if !vpItr.Actual.HasNext() {
if !i.Actual.HasNext() {
return 0, false, nil // couldn't find anything
}
return vpItr.Actual.Next(), true, nil
return i.Actual.Next(), true, nil
}
func (vpItr *VecPostingsIterator) nextCodeAtOrAfter(atOrAfter uint64) (uint64, bool, error) {
if vpItr.Actual == nil || !vpItr.Actual.HasNext() {
func (i *VecPostingsIterator) nextCodeAtOrAfter(atOrAfter uint64) (uint64, bool, error) {
if i.Actual == nil || !i.Actual.HasNext() {
return 0, false, nil
}
if vpItr.postings == nil || vpItr.postings == emptyVecPostingsList {
if i.postings == nil || i.postings == emptyVecPostingsList {
// couldn't find anything
return 0, false, nil
}
if vpItr.postings.postings == vpItr.ActualBM {
return vpItr.nextCodeAtOrAfterClean(atOrAfter)
if i.postings.postings == i.ActualBM {
return i.nextCodeAtOrAfterClean(atOrAfter)
}
vpItr.Actual.AdvanceIfNeeded(atOrAfter)
i.Actual.AdvanceIfNeeded(atOrAfter)
if !vpItr.Actual.HasNext() || !vpItr.all.HasNext() {
if !i.Actual.HasNext() || !i.all.HasNext() {
// couldn't find anything
return 0, false, nil
}
n := vpItr.Actual.Next()
allN := vpItr.all.Next()
n := i.Actual.Next()
allN := i.all.Next()
// n is the next actual hit (excluding some postings), and
// allN is the next hit in the full postings, and
// if they don't match, move 'all' forwards until they do.
for allN != n {
if !vpItr.all.HasNext() {
if !i.all.HasNext() {
return 0, false, nil
}
allN = vpItr.all.Next()
allN = i.all.Next()
}
return n, true, nil
return uint64(n), true, nil
}
// a transformation function which stores both the score and the docNum as a single
@@ -225,49 +225,49 @@ func getVectorCode(docNum uint32, score float32) uint64 {
}
// Next returns the next posting on the vector postings list, or nil at the end
func (vpItr *VecPostingsIterator) nextAtOrAfter(atOrAfter uint64) (segment.VecPosting, error) {
func (i *VecPostingsIterator) nextAtOrAfter(atOrAfter uint64) (segment.VecPosting, error) {
// transform the docNum provided to the vector code format and use that to
// get the next entry. the comparison still happens docNum wise since after
// the transformation, the docNum occupies the upper 32 bits just an entry in
// the postings list
atOrAfter = getVectorCode(uint32(atOrAfter), 0)
code, exists, err := vpItr.nextCodeAtOrAfter(atOrAfter)
code, exists, err := i.nextCodeAtOrAfter(atOrAfter)
if err != nil || !exists {
return nil, err
}
vpItr.next = VecPosting{} // clear the struct
rv := &vpItr.next
i.next = VecPosting{} // clear the struct
rv := &i.next
rv.score = math.Float32frombits(uint32(code))
rv.docNum = code >> 32
return rv, nil
}
func (vpItr *VecPostingsIterator) Next() (segment.VecPosting, error) {
return vpItr.nextAtOrAfter(0)
func (itr *VecPostingsIterator) Next() (segment.VecPosting, error) {
return itr.nextAtOrAfter(0)
}
func (vpItr *VecPostingsIterator) Advance(docNum uint64) (segment.VecPosting, error) {
return vpItr.nextAtOrAfter(docNum)
func (itr *VecPostingsIterator) Advance(docNum uint64) (segment.VecPosting, error) {
return itr.nextAtOrAfter(docNum)
}
func (vpItr *VecPostingsIterator) Size() int {
func (i *VecPostingsIterator) Size() int {
sizeInBytes := reflectStaticSizePostingsIterator + SizeOfPtr +
vpItr.next.Size()
i.next.Size()
return sizeInBytes
}
func (vpItr *VecPostingsIterator) ResetBytesRead(val uint64) {
func (vpl *VecPostingsIterator) ResetBytesRead(val uint64) {
}
func (vpItr *VecPostingsIterator) BytesRead() uint64 {
func (vpl *VecPostingsIterator) BytesRead() uint64 {
return 0
}
func (vpItr *VecPostingsIterator) BytesWritten() uint64 {
func (vpl *VecPostingsIterator) BytesWritten() uint64 {
return 0
}
@@ -329,7 +329,7 @@ func (sb *SegmentBase) InterpretVectorIndex(field string, requiresFiltering bool
// it isn't added to the final postings list.
if docID, ok := vecDocIDMap[vecID]; ok {
code := getVectorCode(docID, scores[i])
pl.postings.Add(code)
pl.postings.Add(uint64(code))
}
}
}
@@ -471,7 +471,7 @@ func (sb *SegmentBase) InterpretVectorIndex(field string, requiresFiltering bool
if err != nil {
return nil, err
}
// If no error occurred during the creation of the selector, then
// If no error occured during the creation of the selector, then
// it should be deleted once the search is complete.
defer selector.Delete()
// Ordering the retrieved centroid IDs by increasing order

View File

@@ -537,21 +537,21 @@ func mergeStoredAndRemap(segments []*SegmentBase, drops []*roaring.Bitmap,
// copyStoredDocs writes out a segment's stored doc info, optimized by
// using a single Write() call for the entire set of bytes. The
// newDocNumOffsets is filled with the new offsets for each doc.
func (sb *SegmentBase) copyStoredDocs(newDocNum uint64, newDocNumOffsets []uint64,
func (s *SegmentBase) copyStoredDocs(newDocNum uint64, newDocNumOffsets []uint64,
w *CountHashWriter) error {
if sb.numDocs <= 0 {
if s.numDocs <= 0 {
return nil
}
indexOffset0, storedOffset0, _, _, _ :=
sb.getDocStoredOffsets(0) // the segment's first doc
s.getDocStoredOffsets(0) // the segment's first doc
indexOffsetN, storedOffsetN, readN, metaLenN, dataLenN :=
sb.getDocStoredOffsets(sb.numDocs - 1) // the segment's last doc
s.getDocStoredOffsets(s.numDocs - 1) // the segment's last doc
storedOffset0New := uint64(w.Count())
storedBytes := sb.mem[storedOffset0 : storedOffsetN+readN+metaLenN+dataLenN]
storedBytes := s.mem[storedOffset0 : storedOffsetN+readN+metaLenN+dataLenN]
_, err := w.Write(storedBytes)
if err != nil {
return err
@@ -560,7 +560,7 @@ func (sb *SegmentBase) copyStoredDocs(newDocNum uint64, newDocNumOffsets []uint6
// remap the storedOffset's for the docs into new offsets relative
// to storedOffset0New, filling the given docNumOffsetsOut array
for indexOffset := indexOffset0; indexOffset <= indexOffsetN; indexOffset += 8 {
storedOffset := binary.BigEndian.Uint64(sb.mem[indexOffset : indexOffset+8])
storedOffset := binary.BigEndian.Uint64(s.mem[indexOffset : indexOffset+8])
storedOffsetNew := storedOffset - storedOffset0 + storedOffset0New
newDocNumOffsets[newDocNum] = storedOffsetNew
newDocNum += 1

View File

@@ -16,27 +16,27 @@ package zap
import "encoding/binary"
func (sb *SegmentBase) getDocStoredMetaAndCompressed(docNum uint64) ([]byte, []byte) {
_, storedOffset, n, metaLen, dataLen := sb.getDocStoredOffsets(docNum)
func (s *SegmentBase) getDocStoredMetaAndCompressed(docNum uint64) ([]byte, []byte) {
_, storedOffset, n, metaLen, dataLen := s.getDocStoredOffsets(docNum)
meta := sb.mem[storedOffset+n : storedOffset+n+metaLen]
data := sb.mem[storedOffset+n+metaLen : storedOffset+n+metaLen+dataLen]
meta := s.mem[storedOffset+n : storedOffset+n+metaLen]
data := s.mem[storedOffset+n+metaLen : storedOffset+n+metaLen+dataLen]
return meta, data
}
func (sb *SegmentBase) getDocStoredOffsets(docNum uint64) (
func (s *SegmentBase) getDocStoredOffsets(docNum uint64) (
uint64, uint64, uint64, uint64, uint64) {
indexOffset := sb.storedIndexOffset + (8 * docNum)
indexOffset := s.storedIndexOffset + (8 * docNum)
storedOffset := binary.BigEndian.Uint64(sb.mem[indexOffset : indexOffset+8])
storedOffset := binary.BigEndian.Uint64(s.mem[indexOffset : indexOffset+8])
var n uint64
metaLen, read := binary.Uvarint(sb.mem[storedOffset : storedOffset+binary.MaxVarintLen64])
metaLen, read := binary.Uvarint(s.mem[storedOffset : storedOffset+binary.MaxVarintLen64])
n += uint64(read)
dataLen, read := binary.Uvarint(sb.mem[storedOffset+n : storedOffset+n+binary.MaxVarintLen64])
dataLen, read := binary.Uvarint(s.mem[storedOffset+n : storedOffset+n+binary.MaxVarintLen64])
n += uint64(read)
return indexOffset, storedOffset, n, metaLen, dataLen

View File

@@ -612,10 +612,8 @@ func (io *invertedIndexOpaque) writeDicts(w *CountHashWriter) (dictOffsets []uin
if io.IncludeDocValues[fieldID] {
for docNum, docTerms := range docTermMap {
if fieldTermMap, ok := io.extraDocValues[docNum]; ok {
if sTerms, ok := fieldTermMap[uint16(fieldID)]; ok {
for _, sTerm := range sTerms {
docTerms = append(append(docTerms, sTerm...), termSeparator)
}
if sTerm, ok := fieldTermMap[uint16(fieldID)]; ok {
docTerms = append(append(docTerms, sTerm...), termSeparator)
}
}
if len(docTerms) > 0 {
@@ -799,9 +797,9 @@ func (i *invertedIndexOpaque) realloc() {
if f, ok := field.(index.GeoShapeField); ok {
if _, exists := i.extraDocValues[docNum]; !exists {
i.extraDocValues[docNum] = make(map[uint16][][]byte)
i.extraDocValues[docNum] = make(map[uint16][]byte)
}
i.extraDocValues[docNum][fieldID] = append(i.extraDocValues[docNum][fieldID], f.EncodedShape())
i.extraDocValues[docNum][fieldID] = f.EncodedShape()
}
}
@@ -812,7 +810,7 @@ func (i *invertedIndexOpaque) realloc() {
}
if i.extraDocValues == nil {
i.extraDocValues = map[int]map[uint16][][]byte{}
i.extraDocValues = map[int]map[uint16][]byte{}
}
for docNum, result := range i.results {
@@ -980,8 +978,8 @@ type invertedIndexOpaque struct {
// store terms that are unnecessary for the term dictionaries but needed in doc values
// eg - encoded geoshapes
// docNum -> fieldID -> terms
extraDocValues map[int]map[uint16][][]byte
// docNum -> fieldID -> term
extraDocValues map[int]map[uint16][]byte
builder *vellum.Builder
builderBuf bytes.Buffer

View File

@@ -269,81 +269,81 @@ func (s *Segment) incrementBytesRead(val uint64) {
atomic.AddUint64(&s.bytesRead, val)
}
func (sb *SegmentBase) BytesWritten() uint64 {
return atomic.LoadUint64(&sb.bytesWritten)
func (s *SegmentBase) BytesWritten() uint64 {
return atomic.LoadUint64(&s.bytesWritten)
}
func (sb *SegmentBase) setBytesWritten(val uint64) {
atomic.AddUint64(&sb.bytesWritten, val)
func (s *SegmentBase) setBytesWritten(val uint64) {
atomic.AddUint64(&s.bytesWritten, val)
}
func (sb *SegmentBase) BytesRead() uint64 {
func (s *SegmentBase) BytesRead() uint64 {
return 0
}
func (sb *SegmentBase) ResetBytesRead(val uint64) {}
func (s *SegmentBase) ResetBytesRead(val uint64) {}
func (sb *SegmentBase) incrementBytesRead(val uint64) {
atomic.AddUint64(&sb.bytesRead, val)
func (s *SegmentBase) incrementBytesRead(val uint64) {
atomic.AddUint64(&s.bytesRead, val)
}
func (sb *SegmentBase) loadFields() error {
func (s *SegmentBase) loadFields() error {
// NOTE for now we assume the fields index immediately precedes
// the footer, and if this changes, need to adjust accordingly (or
// store explicit length), where s.mem was sliced from s.mm in Open().
fieldsIndexEnd := uint64(len(sb.mem))
fieldsIndexEnd := uint64(len(s.mem))
// iterate through fields index
var fieldID uint64
for sb.fieldsIndexOffset+(8*fieldID) < fieldsIndexEnd {
addr := binary.BigEndian.Uint64(sb.mem[sb.fieldsIndexOffset+(8*fieldID) : sb.fieldsIndexOffset+(8*fieldID)+8])
for s.fieldsIndexOffset+(8*fieldID) < fieldsIndexEnd {
addr := binary.BigEndian.Uint64(s.mem[s.fieldsIndexOffset+(8*fieldID) : s.fieldsIndexOffset+(8*fieldID)+8])
// accounting the address of the dictLoc being read from file
sb.incrementBytesRead(8)
s.incrementBytesRead(8)
dictLoc, read := binary.Uvarint(sb.mem[addr:fieldsIndexEnd])
dictLoc, read := binary.Uvarint(s.mem[addr:fieldsIndexEnd])
n := uint64(read)
sb.dictLocs = append(sb.dictLocs, dictLoc)
s.dictLocs = append(s.dictLocs, dictLoc)
var nameLen uint64
nameLen, read = binary.Uvarint(sb.mem[addr+n : fieldsIndexEnd])
nameLen, read = binary.Uvarint(s.mem[addr+n : fieldsIndexEnd])
n += uint64(read)
name := string(sb.mem[addr+n : addr+n+nameLen])
name := string(s.mem[addr+n : addr+n+nameLen])
sb.incrementBytesRead(n + nameLen)
sb.fieldsInv = append(sb.fieldsInv, name)
sb.fieldsMap[name] = uint16(fieldID + 1)
s.incrementBytesRead(n + nameLen)
s.fieldsInv = append(s.fieldsInv, name)
s.fieldsMap[name] = uint16(fieldID + 1)
fieldID++
}
return nil
}
func (sb *SegmentBase) loadFieldsNew() error {
pos := sb.sectionsIndexOffset
func (s *SegmentBase) loadFieldsNew() error {
pos := s.sectionsIndexOffset
if pos == 0 {
// this is the case only for older file formats
return sb.loadFields()
return s.loadFields()
}
seek := pos + binary.MaxVarintLen64
if seek > uint64(len(sb.mem)) {
if seek > uint64(len(s.mem)) {
// handling a buffer overflow case.
// a rare case where the backing buffer is not large enough to be read directly via
// a pos+binary.MaxVarintLen64 seek. For eg, this can happen when there is only
// one field to be indexed in the entire batch of data and while writing out
// these fields metadata, you write 1 + 8 bytes whereas the MaxVarintLen64 = 10.
seek = uint64(len(sb.mem))
seek = uint64(len(s.mem))
}
// read the number of fields
numFields, sz := binary.Uvarint(sb.mem[pos:seek])
numFields, sz := binary.Uvarint(s.mem[pos:seek])
// here, the pos is incremented by the valid number bytes read from the buffer
// so in the edge case pointed out above the numFields = 1, the sz = 1 as well.
pos += uint64(sz)
sb.incrementBytesRead(uint64(sz))
s.incrementBytesRead(uint64(sz))
// the following loop will be executed only once in the edge case pointed out above
// since there is only field's offset store which occupies 8 bytes.
@@ -352,17 +352,17 @@ func (sb *SegmentBase) loadFieldsNew() error {
// the specific section's parsing logic.
var fieldID uint64
for fieldID < numFields {
addr := binary.BigEndian.Uint64(sb.mem[pos : pos+8])
sb.incrementBytesRead(8)
addr := binary.BigEndian.Uint64(s.mem[pos : pos+8])
s.incrementBytesRead(8)
fieldSectionMap := make(map[uint16]uint64)
err := sb.loadFieldNew(uint16(fieldID), addr, fieldSectionMap)
err := s.loadFieldNew(uint16(fieldID), addr, fieldSectionMap)
if err != nil {
return err
}
sb.fieldsSectionsMap = append(sb.fieldsSectionsMap, fieldSectionMap)
s.fieldsSectionsMap = append(s.fieldsSectionsMap, fieldSectionMap)
fieldID++
pos += 8
@@ -371,7 +371,7 @@ func (sb *SegmentBase) loadFieldsNew() error {
return nil
}
func (sb *SegmentBase) loadFieldNew(fieldID uint16, pos uint64,
func (s *SegmentBase) loadFieldNew(fieldID uint16, pos uint64,
fieldSectionMap map[uint16]uint64) error {
if pos == 0 {
// there is no indexing structure present for this field/section
@@ -379,23 +379,23 @@ func (sb *SegmentBase) loadFieldNew(fieldID uint16, pos uint64,
}
fieldStartPos := pos // to track the number of bytes read
fieldNameLen, sz := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
fieldNameLen, sz := binary.Uvarint(s.mem[pos : pos+binary.MaxVarintLen64])
pos += uint64(sz)
fieldName := string(sb.mem[pos : pos+fieldNameLen])
fieldName := string(s.mem[pos : pos+fieldNameLen])
pos += fieldNameLen
sb.fieldsInv = append(sb.fieldsInv, fieldName)
sb.fieldsMap[fieldName] = uint16(fieldID + 1)
s.fieldsInv = append(s.fieldsInv, fieldName)
s.fieldsMap[fieldName] = uint16(fieldID + 1)
fieldNumSections, sz := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
fieldNumSections, sz := binary.Uvarint(s.mem[pos : pos+binary.MaxVarintLen64])
pos += uint64(sz)
for sectionIdx := uint64(0); sectionIdx < fieldNumSections; sectionIdx++ {
// read section id
fieldSectionType := binary.BigEndian.Uint16(sb.mem[pos : pos+2])
fieldSectionType := binary.BigEndian.Uint16(s.mem[pos : pos+2])
pos += 2
fieldSectionAddr := binary.BigEndian.Uint64(sb.mem[pos : pos+8])
fieldSectionAddr := binary.BigEndian.Uint64(s.mem[pos : pos+8])
pos += 8
fieldSectionMap[fieldSectionType] = fieldSectionAddr
if fieldSectionType == SectionInvertedTextIndex {
@@ -403,33 +403,33 @@ func (sb *SegmentBase) loadFieldNew(fieldID uint16, pos uint64,
// 0 and during query time, because there is no valid dictionary we
// will just have follow a no-op path.
if fieldSectionAddr == 0 {
sb.dictLocs = append(sb.dictLocs, 0)
s.dictLocs = append(s.dictLocs, 0)
continue
}
read := 0
// skip the doc values
_, n := binary.Uvarint(sb.mem[fieldSectionAddr : fieldSectionAddr+binary.MaxVarintLen64])
_, n := binary.Uvarint(s.mem[fieldSectionAddr : fieldSectionAddr+binary.MaxVarintLen64])
fieldSectionAddr += uint64(n)
read += n
_, n = binary.Uvarint(sb.mem[fieldSectionAddr : fieldSectionAddr+binary.MaxVarintLen64])
_, n = binary.Uvarint(s.mem[fieldSectionAddr : fieldSectionAddr+binary.MaxVarintLen64])
fieldSectionAddr += uint64(n)
read += n
dictLoc, n := binary.Uvarint(sb.mem[fieldSectionAddr : fieldSectionAddr+binary.MaxVarintLen64])
dictLoc, n := binary.Uvarint(s.mem[fieldSectionAddr : fieldSectionAddr+binary.MaxVarintLen64])
// account the bytes read while parsing the field's inverted index section
sb.incrementBytesRead(uint64(read + n))
sb.dictLocs = append(sb.dictLocs, dictLoc)
s.incrementBytesRead(uint64(read + n))
s.dictLocs = append(s.dictLocs, dictLoc)
}
}
// account the bytes read while parsing the sections field index.
sb.incrementBytesRead((pos - uint64(fieldStartPos)) + fieldNameLen)
s.incrementBytesRead((pos - uint64(fieldStartPos)) + fieldNameLen)
return nil
}
// Dictionary returns the term dictionary for the specified field
func (sb *SegmentBase) Dictionary(field string) (segment.TermDictionary, error) {
dict, err := sb.dictionary(field)
func (s *SegmentBase) Dictionary(field string) (segment.TermDictionary, error) {
dict, err := s.dictionary(field)
if err == nil && dict == nil {
return emptyDictionary, nil
}
@@ -479,8 +479,8 @@ func (sb *SegmentBase) dictionary(field string) (rv *Dictionary, err error) {
}
// Thesaurus returns the thesaurus with the specified name, or an empty thesaurus if not found.
func (sb *SegmentBase) Thesaurus(name string) (segment.Thesaurus, error) {
thesaurus, err := sb.thesaurus(name)
func (s *SegmentBase) Thesaurus(name string) (segment.Thesaurus, error) {
thesaurus, err := s.thesaurus(name)
if err == nil && thesaurus == nil {
return emptyThesaurus, nil
}
@@ -537,17 +537,17 @@ var visitDocumentCtxPool = sync.Pool{
// VisitStoredFields invokes the StoredFieldValueVisitor for each stored field
// for the specified doc number
func (sb *SegmentBase) VisitStoredFields(num uint64, visitor segment.StoredFieldValueVisitor) error {
func (s *SegmentBase) VisitStoredFields(num uint64, visitor segment.StoredFieldValueVisitor) error {
vdc := visitDocumentCtxPool.Get().(*visitDocumentCtx)
defer visitDocumentCtxPool.Put(vdc)
return sb.visitStoredFields(vdc, num, visitor)
return s.visitStoredFields(vdc, num, visitor)
}
func (sb *SegmentBase) visitStoredFields(vdc *visitDocumentCtx, num uint64,
func (s *SegmentBase) visitStoredFields(vdc *visitDocumentCtx, num uint64,
visitor segment.StoredFieldValueVisitor) error {
// first make sure this is a valid number in this segment
if num < sb.numDocs {
meta, compressed := sb.getDocStoredMetaAndCompressed(num)
if num < s.numDocs {
meta, compressed := s.getDocStoredMetaAndCompressed(num)
vdc.reader.Reset(meta)
@@ -611,7 +611,7 @@ func (sb *SegmentBase) visitStoredFields(vdc *visitDocumentCtx, num uint64,
}
}
value := uncompressed[offset : offset+l]
keepGoing = visitor(sb.fieldsInv[field], byte(typ), value, arrayPos)
keepGoing = visitor(s.fieldsInv[field], byte(typ), value, arrayPos)
}
vdc.buf = uncompressed
@@ -620,14 +620,14 @@ func (sb *SegmentBase) visitStoredFields(vdc *visitDocumentCtx, num uint64,
}
// DocID returns the value of the _id field for the given docNum
func (sb *SegmentBase) DocID(num uint64) ([]byte, error) {
if num >= sb.numDocs {
func (s *SegmentBase) DocID(num uint64) ([]byte, error) {
if num >= s.numDocs {
return nil, nil
}
vdc := visitDocumentCtxPool.Get().(*visitDocumentCtx)
meta, compressed := sb.getDocStoredMetaAndCompressed(num)
meta, compressed := s.getDocStoredMetaAndCompressed(num)
vdc.reader.Reset(meta)
@@ -644,17 +644,17 @@ func (sb *SegmentBase) DocID(num uint64) ([]byte, error) {
}
// Count returns the number of documents in this segment.
func (sb *SegmentBase) Count() uint64 {
return sb.numDocs
func (s *SegmentBase) Count() uint64 {
return s.numDocs
}
// DocNumbers returns a bitset corresponding to the doc numbers of all the
// provided _id strings
func (sb *SegmentBase) DocNumbers(ids []string) (*roaring.Bitmap, error) {
func (s *SegmentBase) DocNumbers(ids []string) (*roaring.Bitmap, error) {
rv := roaring.New()
if len(sb.fieldsMap) > 0 {
idDict, err := sb.dictionary("_id")
if len(s.fieldsMap) > 0 {
idDict, err := s.dictionary("_id")
if err != nil {
return nil, err
}
@@ -681,8 +681,8 @@ func (sb *SegmentBase) DocNumbers(ids []string) (*roaring.Bitmap, error) {
}
// Fields returns the field names used in this segment
func (sb *SegmentBase) Fields() []string {
return sb.fieldsInv
func (s *SegmentBase) Fields() []string {
return s.fieldsInv
}
// Path returns the path of this segment on disk
@@ -907,44 +907,44 @@ func (s *Segment) loadDvReaders() error {
// since segmentBase is an in-memory segment, it can be called only
// for v16 file formats as part of InitSegmentBase() while introducing
// a segment into the system.
func (sb *SegmentBase) loadDvReaders() error {
func (s *SegmentBase) loadDvReaders() error {
// evaluate -> s.docValueOffset == fieldNotUninverted
if sb.numDocs == 0 {
if s.numDocs == 0 {
return nil
}
for fieldID, sections := range sb.fieldsSectionsMap {
for fieldID, sections := range s.fieldsSectionsMap {
for secID, secOffset := range sections {
if secOffset > 0 {
// fixed encoding as of now, need to uvarint this
pos := secOffset
var read uint64
fieldLocStart, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
fieldLocStart, n := binary.Uvarint(s.mem[pos : pos+binary.MaxVarintLen64])
if n <= 0 {
return fmt.Errorf("loadDvReaders: failed to read the docvalue offset start for field %v", sb.fieldsInv[fieldID])
return fmt.Errorf("loadDvReaders: failed to read the docvalue offset start for field %v", s.fieldsInv[fieldID])
}
pos += uint64(n)
read += uint64(n)
fieldLocEnd, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
fieldLocEnd, n := binary.Uvarint(s.mem[pos : pos+binary.MaxVarintLen64])
if read <= 0 {
return fmt.Errorf("loadDvReaders: failed to read the docvalue offset end for field %v", sb.fieldsInv[fieldID])
return fmt.Errorf("loadDvReaders: failed to read the docvalue offset end for field %v", s.fieldsInv[fieldID])
}
pos += uint64(n)
read += uint64(n)
sb.incrementBytesRead(read)
s.incrementBytesRead(read)
fieldDvReader, err := sb.loadFieldDocValueReader(sb.fieldsInv[fieldID], fieldLocStart, fieldLocEnd)
fieldDvReader, err := s.loadFieldDocValueReader(s.fieldsInv[fieldID], fieldLocStart, fieldLocEnd)
if err != nil {
return err
}
if fieldDvReader != nil {
if sb.fieldDvReaders[secID] == nil {
sb.fieldDvReaders[secID] = make(map[uint16]*docValueReader)
if s.fieldDvReaders[secID] == nil {
s.fieldDvReaders[secID] = make(map[uint16]*docValueReader)
}
sb.fieldDvReaders[secID][uint16(fieldID)] = fieldDvReader
sb.fieldDvNames = append(sb.fieldDvNames, sb.fieldsInv[fieldID])
s.fieldDvReaders[secID][uint16(fieldID)] = fieldDvReader
s.fieldDvNames = append(s.fieldDvNames, s.fieldsInv[fieldID])
}
}
}

View File

@@ -85,7 +85,7 @@ func (v *Version) Set(version string) error {
return fmt.Errorf("failed to validate metadata: %v", err)
}
parsed := make([]int64, 3)
parsed := make([]int64, 3, 3)
for i, v := range dotParts[:3] {
val, err := strconv.ParseInt(v, 10, 64)

View File

@@ -1,7 +1,7 @@
freebsd_task:
name: 'FreeBSD'
freebsd_instance:
image_family: freebsd-14-2
image_family: freebsd-14-1
install_script:
- pkg update -f
- pkg install -y go

View File

@@ -1,39 +1,6 @@
# Changelog
1.9.0 2024-04-04
----------------
### Changes and fixes
- all: make BufferedWatcher buffered again ([#657])
- inotify: fix race when adding/removing watches while a watched path is being
deleted ([#678], [#686])
- inotify: don't send empty event if a watched path is unmounted ([#655])
- inotify: don't register duplicate watches when watching both a symlink and its
target; previously that would get "half-added" and removing the second would
panic ([#679])
- kqueue: fix watching relative symlinks ([#681])
- kqueue: correctly mark pre-existing entries when watching a link to a dir on
kqueue ([#682])
- illumos: don't send error if changed file is deleted while processing the
event ([#678])
[#657]: https://github.com/fsnotify/fsnotify/pull/657
[#678]: https://github.com/fsnotify/fsnotify/pull/678
[#686]: https://github.com/fsnotify/fsnotify/pull/686
[#655]: https://github.com/fsnotify/fsnotify/pull/655
[#681]: https://github.com/fsnotify/fsnotify/pull/681
[#679]: https://github.com/fsnotify/fsnotify/pull/679
[#682]: https://github.com/fsnotify/fsnotify/pull/682
1.8.0 2024-10-31
1.8.0 2023-10-31
----------------
### Additions

View File

@@ -77,7 +77,6 @@ End-of-line escapes with `\` are not supported.
debug [yes/no] # Enable/disable FSNOTIFY_DEBUG (tests are run in
parallel by default, so -parallel=1 is probably a good
idea).
print [any strings] # Print text to stdout; for debugging.
touch path
mkdir [-p] dir

View File

@@ -15,6 +15,7 @@ Platform support:
| ReadDirectoryChangesW | Windows | Supported |
| FEN | illumos | Supported |
| fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) |
| AHAFS | AIX | [aix branch]; experimental due to lack of maintainer and test environment |
| FSEvents | macOS | [Needs support in x/sys/unix][fsevents] |
| USN Journals | Windows | [Needs support in x/sys/windows][usn] |
| Polling | *All* | [Not yet](https://github.com/fsnotify/fsnotify/issues/9) |
@@ -24,6 +25,7 @@ untested.
[fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120
[usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847
[aix branch]: https://github.com/fsnotify/fsnotify/issues/353#issuecomment-1284590129
Usage
-----

View File

@@ -9,7 +9,6 @@ package fsnotify
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sync"
@@ -20,25 +19,27 @@ import (
)
type fen struct {
*shared
Events chan Event
Errors chan error
mu sync.Mutex
port *unix.EventPort
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
dirs map[string]Op // Explicitly watched directories
watches map[string]Op // Explicitly watched non-directories
}
var defaultBufferSize = 0
func newBackend(ev chan Event, errs chan error) (backend, error) {
return newBufferedBackend(0, ev, errs)
}
func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
w := &fen{
shared: newShared(ev, errs),
Events: ev,
Errors: errs,
dirs: make(map[string]Op),
watches: make(map[string]Op),
done: make(chan struct{}),
}
var err error
@@ -51,10 +52,49 @@ func newBackend(ev chan Event, errs chan error) (backend, error) {
return w, nil
}
// sendEvent attempts to send an event to the user, returning true if the event
// was put in the channel successfully and false if the watcher has been closed.
func (w *fen) sendEvent(name string, op Op) (sent bool) {
select {
case <-w.done:
return false
case w.Events <- Event{Name: name, Op: op}:
return true
}
}
// sendError attempts to send an error to the user, returning true if the error
// was put in the channel successfully and false if the watcher has been closed.
func (w *fen) sendError(err error) (sent bool) {
if err == nil {
return true
}
select {
case <-w.done:
return false
case w.Errors <- err:
return true
}
}
func (w *fen) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
func (w *fen) Close() error {
if w.shared.close() {
// Take the lock used by associateFile to prevent lingering events from
// being processed after the close
w.mu.Lock()
defer w.mu.Unlock()
if w.isClosed() {
return nil
}
close(w.done)
return w.port.Close()
}
@@ -169,7 +209,7 @@ func (w *fen) readEvents() {
return
}
// There was an error not caused by calling w.Close()
if !w.sendError(fmt.Errorf("port.Get: %w", err)) {
if !w.sendError(err) {
return
}
}
@@ -237,13 +277,13 @@ func (w *fen) handleEvent(event *unix.PortEvent) error {
isWatched := watchedDir || watchedPath
if events&unix.FILE_DELETE != 0 {
if !w.sendEvent(Event{Name: path, Op: Remove}) {
if !w.sendEvent(path, Remove) {
return nil
}
reRegister = false
}
if events&unix.FILE_RENAME_FROM != 0 {
if !w.sendEvent(Event{Name: path, Op: Rename}) {
if !w.sendEvent(path, Rename) {
return nil
}
// Don't keep watching the new file name
@@ -257,7 +297,7 @@ func (w *fen) handleEvent(event *unix.PortEvent) error {
// inotify reports a Remove event in this case, so we simulate this
// here.
if !w.sendEvent(Event{Name: path, Op: Remove}) {
if !w.sendEvent(path, Remove) {
return nil
}
// Don't keep watching the file that was removed
@@ -291,7 +331,7 @@ func (w *fen) handleEvent(event *unix.PortEvent) error {
// get here, the sudirectory is already gone. Clearly we were watching
// this path but now it is gone. Let's tell the user that it was
// removed.
if !w.sendEvent(Event{Name: path, Op: Remove}) {
if !w.sendEvent(path, Remove) {
return nil
}
// Suppress extra write events on removed directories; they are not
@@ -306,7 +346,7 @@ func (w *fen) handleEvent(event *unix.PortEvent) error {
if err != nil {
// The symlink still exists, but the target is gone. Report the
// Remove similar to above.
if !w.sendEvent(Event{Name: path, Op: Remove}) {
if !w.sendEvent(path, Remove) {
return nil
}
// Don't return the error
@@ -319,7 +359,7 @@ func (w *fen) handleEvent(event *unix.PortEvent) error {
return err
}
} else {
if !w.sendEvent(Event{Name: path, Op: Write}) {
if !w.sendEvent(path, Write) {
return nil
}
}
@@ -327,7 +367,7 @@ func (w *fen) handleEvent(event *unix.PortEvent) error {
if events&unix.FILE_ATTRIB != 0 && stat != nil {
// Only send Chmod if perms changed
if stat.Mode().Perm() != fmode.Perm() {
if !w.sendEvent(Event{Name: path, Op: Chmod}) {
if !w.sendEvent(path, Chmod) {
return nil
}
}
@@ -336,27 +376,17 @@ func (w *fen) handleEvent(event *unix.PortEvent) error {
if stat != nil {
// If we get here, it means we've hit an event above that requires us to
// continue watching the file or directory
err := w.associateFile(path, stat, isWatched)
if errors.Is(err, fs.ErrNotExist) {
// Path may have been removed since the stat.
err = nil
}
return err
return w.associateFile(path, stat, isWatched)
}
return nil
}
// The directory was modified, so we must find unwatched entities and watch
// them. If something was removed from the directory, nothing will happen, as
// everything else should still be watched.
func (w *fen) updateDirectory(path string) error {
// The directory was modified, so we must find unwatched entities and watch
// them. If something was removed from the directory, nothing will happen,
// as everything else should still be watched.
files, err := os.ReadDir(path)
if err != nil {
// Directory no longer exists: probably just deleted since we got the
// event.
if errors.Is(err, fs.ErrNotExist) {
return nil
}
return err
}
@@ -371,15 +401,10 @@ func (w *fen) updateDirectory(path string) error {
return err
}
err = w.associateFile(path, finfo, false)
if errors.Is(err, fs.ErrNotExist) {
// File may have disappeared between getting the dir listing and
// adding the port: that's okay to ignore.
continue
}
if !w.sendError(err) {
return nil
}
if !w.sendEvent(Event{Name: path, Op: Create}) {
if !w.sendEvent(path, Create) {
return nil
}
}
@@ -405,7 +430,7 @@ func (w *fen) associateFile(path string, stat os.FileInfo, follow bool) error {
// has fired but we haven't processed it yet.
err := w.port.DissociatePath(path)
if err != nil && !errors.Is(err, unix.ENOENT) {
return fmt.Errorf("port.DissociatePath(%q): %w", path, err)
return err
}
}
@@ -421,22 +446,14 @@ func (w *fen) associateFile(path string, stat os.FileInfo, follow bool) error {
if true {
events |= unix.FILE_ATTRIB
}
err := w.port.AssociatePath(path, stat, events, stat.Mode())
if err != nil {
return fmt.Errorf("port.AssociatePath(%q): %w", path, err)
}
return nil
return w.port.AssociatePath(path, stat, events, stat.Mode())
}
func (w *fen) dissociateFile(path string, stat os.FileInfo, unused bool) error {
if !w.port.PathIsWatched(path) {
return nil
}
err := w.port.DissociatePath(path)
if err != nil {
return fmt.Errorf("port.DissociatePath(%q): %w", path, err)
}
return nil
return w.port.DissociatePath(path)
}
func (w *fen) WatchList() []string {

View File

@@ -19,7 +19,6 @@ import (
)
type inotify struct {
*shared
Events chan Event
Errors chan error
@@ -28,6 +27,8 @@ type inotify struct {
fd int
inotifyFile *os.File
watches *watches
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
doneMu sync.Mutex
doneResp chan struct{} // Channel to respond to Close
// Store rename cookies in an array, with the index wrapping to 0. Almost
@@ -51,6 +52,7 @@ type inotify struct {
type (
watches struct {
mu sync.RWMutex
wd map[uint32]*watch // wd → watch
path map[string]uint32 // pathname → wd
}
@@ -73,13 +75,34 @@ func newWatches() *watches {
}
}
func (w *watches) byPath(path string) *watch { return w.wd[w.path[path]] }
func (w *watches) byWd(wd uint32) *watch { return w.wd[wd] }
func (w *watches) len() int { return len(w.wd) }
func (w *watches) add(ww *watch) { w.wd[ww.wd] = ww; w.path[ww.path] = ww.wd }
func (w *watches) remove(watch *watch) { delete(w.path, watch.path); delete(w.wd, watch.wd) }
func (w *watches) len() int {
w.mu.RLock()
defer w.mu.RUnlock()
return len(w.wd)
}
func (w *watches) add(ww *watch) {
w.mu.Lock()
defer w.mu.Unlock()
w.wd[ww.wd] = ww
w.path[ww.path] = ww.wd
}
func (w *watches) remove(wd uint32) {
w.mu.Lock()
defer w.mu.Unlock()
watch := w.wd[wd] // Could have had Remove() called. See #616.
if watch == nil {
return
}
delete(w.path, watch.path)
delete(w.wd, wd)
}
func (w *watches) removePath(path string) ([]uint32, error) {
w.mu.Lock()
defer w.mu.Unlock()
path, recurse := recursivePath(path)
wd, ok := w.path[path]
if !ok {
@@ -100,7 +123,7 @@ func (w *watches) removePath(path string) ([]uint32, error) {
wds := make([]uint32, 0, 8)
wds = append(wds, wd)
for p, rwd := range w.path {
if strings.HasPrefix(p, path) {
if filepath.HasPrefix(p, path) {
delete(w.path, p)
delete(w.wd, rwd)
wds = append(wds, rwd)
@@ -109,7 +132,22 @@ func (w *watches) removePath(path string) ([]uint32, error) {
return wds, nil
}
func (w *watches) byPath(path string) *watch {
w.mu.RLock()
defer w.mu.RUnlock()
return w.wd[w.path[path]]
}
func (w *watches) byWd(wd uint32) *watch {
w.mu.RLock()
defer w.mu.RUnlock()
return w.wd[wd]
}
func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error {
w.mu.Lock()
defer w.mu.Unlock()
var existing *watch
wd, ok := w.path[path]
if ok {
@@ -132,9 +170,11 @@ func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error
return nil
}
var defaultBufferSize = 0
func newBackend(ev chan Event, errs chan error) (backend, error) {
return newBufferedBackend(0, ev, errs)
}
func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
// Need to set nonblocking mode for SetDeadline to work, otherwise blocking
// I/O operations won't terminate on close.
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
@@ -143,12 +183,12 @@ func newBackend(ev chan Event, errs chan error) (backend, error) {
}
w := &inotify{
shared: newShared(ev, errs),
Events: ev,
Errors: errs,
fd: fd,
inotifyFile: os.NewFile(uintptr(fd), ""),
watches: newWatches(),
done: make(chan struct{}),
doneResp: make(chan struct{}),
}
@@ -156,10 +196,46 @@ func newBackend(ev chan Event, errs chan error) (backend, error) {
return w, nil
}
// Returns true if the event was sent, or false if watcher is closed.
func (w *inotify) sendEvent(e Event) bool {
select {
case <-w.done:
return false
case w.Events <- e:
return true
}
}
// Returns true if the error was sent, or false if watcher is closed.
func (w *inotify) sendError(err error) bool {
if err == nil {
return true
}
select {
case <-w.done:
return false
case w.Errors <- err:
return true
}
}
func (w *inotify) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
func (w *inotify) Close() error {
if w.shared.close() {
w.doneMu.Lock()
if w.isClosed() {
w.doneMu.Unlock()
return nil
}
close(w.done)
w.doneMu.Unlock()
// Causes any blocking reads to return with an error, provided the file
// still supports deadline operations.
@@ -168,7 +244,9 @@ func (w *inotify) Close() error {
return err
}
<-w.doneResp // Wait for readEvents() to finish.
// Wait for goroutine to close
<-w.doneResp
return nil
}
@@ -188,43 +266,6 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error {
return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
}
add := func(path string, with withOpts, recurse bool) error {
var flags uint32
if with.noFollow {
flags |= unix.IN_DONT_FOLLOW
}
if with.op.Has(Create) {
flags |= unix.IN_CREATE
}
if with.op.Has(Write) {
flags |= unix.IN_MODIFY
}
if with.op.Has(Remove) {
flags |= unix.IN_DELETE | unix.IN_DELETE_SELF
}
if with.op.Has(Rename) {
flags |= unix.IN_MOVED_TO | unix.IN_MOVED_FROM | unix.IN_MOVE_SELF
}
if with.op.Has(Chmod) {
flags |= unix.IN_ATTRIB
}
if with.op.Has(xUnportableOpen) {
flags |= unix.IN_OPEN
}
if with.op.Has(xUnportableRead) {
flags |= unix.IN_ACCESS
}
if with.op.Has(xUnportableCloseWrite) {
flags |= unix.IN_CLOSE_WRITE
}
if with.op.Has(xUnportableCloseRead) {
flags |= unix.IN_CLOSE_NOWRITE
}
return w.register(path, flags, recurse)
}
w.mu.Lock()
defer w.mu.Unlock()
path, recurse := recursivePath(path)
if recurse {
return filepath.WalkDir(path, func(root string, d fs.DirEntry, err error) error {
@@ -248,11 +289,46 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error {
w.sendEvent(Event{Name: root, Op: Create})
}
return add(root, with, true)
return w.add(root, with, true)
})
}
return add(path, with, false)
return w.add(path, with, false)
}
func (w *inotify) add(path string, with withOpts, recurse bool) error {
var flags uint32
if with.noFollow {
flags |= unix.IN_DONT_FOLLOW
}
if with.op.Has(Create) {
flags |= unix.IN_CREATE
}
if with.op.Has(Write) {
flags |= unix.IN_MODIFY
}
if with.op.Has(Remove) {
flags |= unix.IN_DELETE | unix.IN_DELETE_SELF
}
if with.op.Has(Rename) {
flags |= unix.IN_MOVED_TO | unix.IN_MOVED_FROM | unix.IN_MOVE_SELF
}
if with.op.Has(Chmod) {
flags |= unix.IN_ATTRIB
}
if with.op.Has(xUnportableOpen) {
flags |= unix.IN_OPEN
}
if with.op.Has(xUnportableRead) {
flags |= unix.IN_ACCESS
}
if with.op.Has(xUnportableCloseWrite) {
flags |= unix.IN_CLOSE_WRITE
}
if with.op.Has(xUnportableCloseRead) {
flags |= unix.IN_CLOSE_NOWRITE
}
return w.register(path, flags, recurse)
}
func (w *inotify) register(path string, flags uint32, recurse bool) error {
@@ -266,10 +342,6 @@ func (w *inotify) register(path string, flags uint32, recurse bool) error {
return nil, err
}
if e, ok := w.watches.wd[uint32(wd)]; ok {
return e, nil
}
if existing == nil {
return &watch{
wd: uint32(wd),
@@ -293,9 +365,6 @@ func (w *inotify) Remove(name string) error {
fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n",
time.Now().Format("15:04:05.000000000"), name)
}
w.mu.Lock()
defer w.mu.Unlock()
return w.remove(filepath.Clean(name))
}
@@ -330,12 +399,13 @@ func (w *inotify) WatchList() []string {
return nil
}
w.mu.Lock()
defer w.mu.Unlock()
entries := make([]string, 0, w.watches.len())
w.watches.mu.RLock()
for pathname := range w.watches.path {
entries = append(entries, pathname)
}
w.watches.mu.RUnlock()
return entries
}
@@ -348,17 +418,21 @@ func (w *inotify) readEvents() {
close(w.Events)
}()
var buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
var (
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
errno error // Syscall errno
)
for {
// See if we have been closed.
if w.isClosed() {
return
}
n, err := w.inotifyFile.Read(buf[:])
if err != nil {
if errors.Is(err, os.ErrClosed) {
return
}
switch {
case errors.Unwrap(err) == os.ErrClosed:
return
case err != nil:
if !w.sendError(err) {
return
}
@@ -366,9 +440,13 @@ func (w *inotify) readEvents() {
}
if n < unix.SizeofInotifyEvent {
err := errors.New("notify: short read in readEvents()") // Read was too short.
var err error
if n == 0 {
err = io.EOF // If EOF is received. This should really never happen.
} else if n < 0 {
err = errno // If an error occurred while reading.
} else {
err = errors.New("notify: short read in readEvents()") // Read was too short.
}
if !w.sendError(err) {
return
@@ -376,135 +454,132 @@ func (w *inotify) readEvents() {
continue
}
// We don't know how many events we just read into the buffer While the
// offset points to at least one whole event.
// We don't know how many events we just read into the buffer
// While the offset points to at least one whole event...
var offset uint32
for offset <= uint32(n-unix.SizeofInotifyEvent) {
// Point to the event in the buffer.
inEvent := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
var (
// Point "raw" to the event in the buffer
raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
mask = uint32(raw.Mask)
nameLen = uint32(raw.Len)
// Move to the next event in the buffer
next = func() { offset += unix.SizeofInotifyEvent + nameLen }
)
if inEvent.Mask&unix.IN_Q_OVERFLOW != 0 {
if mask&unix.IN_Q_OVERFLOW != 0 {
if !w.sendError(ErrEventOverflow) {
return
}
}
ev, ok := w.handleEvent(inEvent, &buf, offset)
if !ok {
return
}
if !w.sendEvent(ev) {
return
/// If the event happened to the watched directory or the watched
/// file, the kernel doesn't append the filename to the event, but
/// we would like to always fill the the "Name" field with a valid
/// filename. We retrieve the path of the watch from the "paths"
/// map.
watch := w.watches.byWd(uint32(raw.Wd))
/// Can be nil if Remove() was called in another goroutine for this
/// path inbetween reading the events from the kernel and reading
/// the internal state. Not much we can do about it, so just skip.
/// See #616.
if watch == nil {
next()
continue
}
// Move to the next event in the buffer
offset += unix.SizeofInotifyEvent + inEvent.Len
}
}
}
func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offset uint32) (Event, bool) {
w.mu.Lock()
defer w.mu.Unlock()
/// If the event happened to the watched directory or the watched file, the
/// kernel doesn't append the filename to the event, but we would like to
/// always fill the the "Name" field with a valid filename. We retrieve the
/// path of the watch from the "paths" map.
///
/// Can be nil if Remove() was called in another goroutine for this path
/// inbetween reading the events from the kernel and reading the internal
/// state. Not much we can do about it, so just skip. See #616.
watch := w.watches.byWd(uint32(inEvent.Wd))
if watch == nil {
return Event{}, true
}
var (
name = watch.path
nameLen = uint32(inEvent.Len)
)
if nameLen > 0 {
/// Point "bytes" at the first byte of the filename
bb := *buf
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&bb[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
/// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\x00")
}
if debug {
internal.Debug(name, inEvent.Mask, inEvent.Cookie)
}
if inEvent.Mask&unix.IN_IGNORED != 0 || inEvent.Mask&unix.IN_UNMOUNT != 0 {
w.watches.remove(watch)
return Event{}, true
}
// inotify will automatically remove the watch on deletes; just need
// to clean our state here.
if inEvent.Mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
w.watches.remove(watch)
}
// We can't really update the state when a watched path is moved; only
// IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove the watch.
if inEvent.Mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
if watch.recurse { // Do nothing
return Event{}, true
}
err := w.remove(watch.path)
if err != nil && !errors.Is(err, ErrNonExistentWatch) {
if !w.sendError(err) {
return Event{}, false
}
}
}
/// Skip if we're watching both this path and the parent; the parent will
/// already send a delete so no need to do it twice.
if inEvent.Mask&unix.IN_DELETE_SELF != 0 {
_, ok := w.watches.path[filepath.Dir(watch.path)]
if ok {
return Event{}, true
}
}
ev := w.newEvent(name, inEvent.Mask, inEvent.Cookie)
// Need to update watch path for recurse.
if watch.recurse {
isDir := inEvent.Mask&unix.IN_ISDIR == unix.IN_ISDIR
/// New directory created: set up watch on it.
if isDir && ev.Has(Create) {
err := w.register(ev.Name, watch.flags, true)
if !w.sendError(err) {
return Event{}, false
name := watch.path
if nameLen > 0 {
/// Point "bytes" at the first byte of the filename
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
/// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
}
// This was a directory rename, so we need to update all the
// children.
//
// TODO: this is of course pretty slow; we should use a better data
// structure for storing all of this, e.g. store children in the
// watch. I have some code for this in my kqueue refactor we can use
// in the future. For now I'm okay with this as it's not publicly
// available. Correctness first, performance second.
if ev.renamedFrom != "" {
for k, ww := range w.watches.wd {
if k == watch.wd || ww.path == ev.Name {
continue
}
if strings.HasPrefix(ww.path, ev.renamedFrom) {
ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1)
w.watches.wd[k] = ww
if debug {
internal.Debug(name, raw.Mask, raw.Cookie)
}
if mask&unix.IN_IGNORED != 0 { //&& event.Op != 0
next()
continue
}
// inotify will automatically remove the watch on deletes; just need
// to clean our state here.
if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
w.watches.remove(watch.wd)
}
// We can't really update the state when a watched path is moved;
// only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove
// the watch.
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
if watch.recurse {
next() // Do nothing
continue
}
err := w.remove(watch.path)
if err != nil && !errors.Is(err, ErrNonExistentWatch) {
if !w.sendError(err) {
return
}
}
}
/// Skip if we're watching both this path and the parent; the parent
/// will already send a delete so no need to do it twice.
if mask&unix.IN_DELETE_SELF != 0 {
if _, ok := w.watches.path[filepath.Dir(watch.path)]; ok {
next()
continue
}
}
ev := w.newEvent(name, mask, raw.Cookie)
// Need to update watch path for recurse.
if watch.recurse {
isDir := mask&unix.IN_ISDIR == unix.IN_ISDIR
/// New directory created: set up watch on it.
if isDir && ev.Has(Create) {
err := w.register(ev.Name, watch.flags, true)
if !w.sendError(err) {
return
}
// This was a directory rename, so we need to update all
// the children.
//
// TODO: this is of course pretty slow; we should use a
// better data structure for storing all of this, e.g. store
// children in the watch. I have some code for this in my
// kqueue refactor we can use in the future. For now I'm
// okay with this as it's not publicly available.
// Correctness first, performance second.
if ev.renamedFrom != "" {
w.watches.mu.Lock()
for k, ww := range w.watches.wd {
if k == watch.wd || ww.path == ev.Name {
continue
}
if strings.HasPrefix(ww.path, ev.renamedFrom) {
ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1)
w.watches.wd[k] = ww
}
}
w.watches.mu.Unlock()
}
}
}
/// Send the events that are not ignored on the events channel
if !w.sendEvent(ev) {
return
}
next()
}
}
return ev, true
}
func (w *inotify) isRecursive(path string) bool {
@@ -575,8 +650,8 @@ func (w *inotify) xSupports(op Op) bool {
}
func (w *inotify) state() {
w.mu.Lock()
defer w.mu.Unlock()
w.watches.mu.Lock()
defer w.watches.mu.Unlock()
for wd, ww := range w.watches.wd {
fmt.Fprintf(os.Stderr, "%4d: recurse=%t %q\n", wd, ww.recurse, ww.path)
}

View File

@@ -16,13 +16,14 @@ import (
)
type kqueue struct {
*shared
Events chan Event
Errors chan error
kq int // File descriptor (as returned by the kqueue() syscall).
closepipe [2]int // Pipe used for closing kq.
watches *watches
done chan struct{}
doneMu sync.Mutex
}
type (
@@ -131,18 +132,14 @@ func (w *watches) byPath(path string) (watch, bool) {
return info, ok
}
func (w *watches) updateDirFlags(path string, flags uint32) bool {
func (w *watches) updateDirFlags(path string, flags uint32) {
w.mu.Lock()
defer w.mu.Unlock()
fd, ok := w.path[path]
if !ok { // Already deleted: don't re-set it here.
return false
}
fd := w.path[path]
info := w.wd[fd]
info.dirFlags = flags
w.wd[fd] = info
return true
}
func (w *watches) remove(fd int, path string) bool {
@@ -182,20 +179,22 @@ func (w *watches) seenBefore(path string) bool {
return ok
}
var defaultBufferSize = 0
func newBackend(ev chan Event, errs chan error) (backend, error) {
return newBufferedBackend(0, ev, errs)
}
func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
kq, closepipe, err := newKqueue()
if err != nil {
return nil, err
}
w := &kqueue{
shared: newShared(ev, errs),
Events: ev,
Errors: errs,
kq: kq,
closepipe: closepipe,
done: make(chan struct{}),
watches: newWatches(),
}
@@ -211,7 +210,7 @@ func newBackend(ev chan Event, errs chan error) (backend, error) {
// all.
func newKqueue() (kq int, closepipe [2]int, err error) {
kq, err = unix.Kqueue()
if err != nil {
if kq == -1 {
return kq, closepipe, err
}
@@ -240,17 +239,54 @@ func newKqueue() (kq int, closepipe [2]int, err error) {
return kq, closepipe, nil
}
// Returns true if the event was sent, or false if watcher is closed.
func (w *kqueue) sendEvent(e Event) bool {
select {
case <-w.done:
return false
case w.Events <- e:
return true
}
}
// Returns true if the error was sent, or false if watcher is closed.
func (w *kqueue) sendError(err error) bool {
if err == nil {
return true
}
select {
case <-w.done:
return false
case w.Errors <- err:
return true
}
}
func (w *kqueue) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
func (w *kqueue) Close() error {
if w.shared.close() {
w.doneMu.Lock()
if w.isClosed() {
w.doneMu.Unlock()
return nil
}
close(w.done)
w.doneMu.Unlock()
pathsToRemove := w.watches.listPaths(false)
for _, name := range pathsToRemove {
w.Remove(name)
}
unix.Close(w.closepipe[1]) // Send "quit" message to readEvents
// Send "quit" message to the reader goroutine.
unix.Close(w.closepipe[1])
return nil
}
@@ -267,7 +303,7 @@ func (w *kqueue) AddWith(name string, opts ...addOpt) error {
return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
}
_, err := w.addWatch(name, noteAllEvents, false)
_, err := w.addWatch(name, noteAllEvents)
if err != nil {
return err
}
@@ -330,7 +366,7 @@ const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | un
// described in kevent(2).
//
// Returns the real path to the file which was added, with symlinks resolved.
func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, error) {
func (w *kqueue) addWatch(name string, flags uint32) (string, error) {
if w.isClosed() {
return "", ErrClosed
}
@@ -349,15 +385,15 @@ func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, erro
return "", nil
}
// Follow symlinks, but only for paths added with Add(), and not paths
// we're adding from internalWatch from a listdir.
if !listDir && fi.Mode()&os.ModeSymlink == os.ModeSymlink {
// Follow symlinks.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := os.Readlink(name)
if err != nil {
return "", err
}
if !filepath.IsAbs(link) {
link = filepath.Join(filepath.Dir(name), link)
// Return nil because Linux can add unresolvable symlinks to the
// watch list without problems, so maintain consistency with
// that. There will be no file events for broken symlinks.
// TODO: more specific check; returns os.PathError; ENOENT?
return "", nil
}
_, alreadyWatching = w.watches.byPath(link)
@@ -372,7 +408,7 @@ func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, erro
name = link
fi, err = os.Lstat(name)
if err != nil {
return "", err
return "", nil
}
}
@@ -386,6 +422,7 @@ func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, erro
if errors.Is(err, unix.EINTR) {
continue
}
return "", err
}
@@ -407,16 +444,10 @@ func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, erro
if info.isDir {
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
(!alreadyWatching || (info.dirFlags&unix.NOTE_WRITE) != unix.NOTE_WRITE)
if !w.watches.updateDirFlags(name, flags) {
return "", nil
}
w.watches.updateDirFlags(name, flags)
if watchDir {
d := name
if info.linkName != "" {
d = info.linkName
}
if err := w.watchDirectoryFiles(d); err != nil {
if err := w.watchDirectoryFiles(name); err != nil {
return "", err
}
}
@@ -613,22 +644,19 @@ func (w *kqueue) dirChange(dir string) error {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("fsnotify.dirChange %q: %w", dir, err)
return fmt.Errorf("fsnotify.dirChange: %w", err)
}
for _, f := range files {
fi, err := f.Info()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("fsnotify.dirChange: %w", err)
}
err = w.sendCreateIfNew(filepath.Join(dir, fi.Name()), fi)
if err != nil {
// Don't need to send an error if this file isn't readable.
if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) || errors.Is(err, os.ErrNotExist) {
if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) {
return nil
}
return fmt.Errorf("fsnotify.dirChange: %w", err)
@@ -660,11 +688,11 @@ func (w *kqueue) internalWatch(name string, fi os.FileInfo) (string, error) {
// mimic Linux providing delete events for subdirectories, but preserve
// the flags used if currently watching subdirectory
info, _ := w.watches.byPath(name)
return w.addWatch(name, info.dirFlags|unix.NOTE_DELETE|unix.NOTE_RENAME, true)
return w.addWatch(name, info.dirFlags|unix.NOTE_DELETE|unix.NOTE_RENAME)
}
// Watch file to mimic Linux inotify.
return w.addWatch(name, noteAllEvents, true)
// watch file to mimic Linux inotify
return w.addWatch(name, noteAllEvents)
}
// Register events with the queue.
@@ -694,9 +722,9 @@ func (w *kqueue) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) {
}
func (w *kqueue) xSupports(op Op) bool {
//if runtime.GOOS == "freebsd" {
// return true // Supports everything.
//}
if runtime.GOOS == "freebsd" {
//return true // Supports everything.
}
if op.Has(xUnportableOpen) || op.Has(xUnportableRead) ||
op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) {
return false

View File

@@ -9,11 +9,12 @@ type other struct {
Errors chan error
}
var defaultBufferSize = 0
func newBackend(ev chan Event, errs chan error) (backend, error) {
return nil, errors.New("fsnotify not supported on the current platform")
}
func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
return newBackend(ev, errs)
}
func (w *other) Close() error { return nil }
func (w *other) WatchList() []string { return nil }
func (w *other) Add(name string) error { return nil }

View File

@@ -28,16 +28,18 @@ type readDirChangesW struct {
port windows.Handle // Handle to completion port
input chan *input // Inputs to the reader are sent on this channel
done chan chan<- error
quit chan chan<- error
mu sync.Mutex // Protects access to watches, closed
watches watchMap // Map of watches (key: i-number)
closed bool // Set to true when Close() is first called
}
var defaultBufferSize = 50
func newBackend(ev chan Event, errs chan error) (backend, error) {
return newBufferedBackend(50, ev, errs)
}
func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
if err != nil {
return nil, os.NewSyscallError("CreateIoCompletionPort", err)
@@ -48,7 +50,7 @@ func newBackend(ev chan Event, errs chan error) (backend, error) {
port: port,
watches: make(watchMap),
input: make(chan *input, 1),
done: make(chan chan<- error, 1),
quit: make(chan chan<- error, 1),
}
go w.readEvents()
return w, nil
@@ -68,8 +70,8 @@ func (w *readDirChangesW) sendEvent(name, renamedFrom string, mask uint64) bool
event := w.newEvent(name, uint32(mask))
event.renamedFrom = renamedFrom
select {
case ch := <-w.done:
w.done <- ch
case ch := <-w.quit:
w.quit <- ch
case w.Events <- event:
}
return true
@@ -81,10 +83,10 @@ func (w *readDirChangesW) sendError(err error) bool {
return true
}
select {
case <-w.done:
return false
case w.Errors <- err:
return true
case <-w.quit:
return false
}
}
@@ -97,9 +99,9 @@ func (w *readDirChangesW) Close() error {
w.closed = true
w.mu.Unlock()
// Send "done" message to the reader goroutine
// Send "quit" message to the reader goroutine
ch := make(chan error)
w.done <- ch
w.quit <- ch
if err := w.wakeupReader(); err != nil {
return err
}
@@ -493,7 +495,7 @@ func (w *readDirChangesW) readEvents() {
watch := (*watch)(unsafe.Pointer(ov))
if watch == nil {
select {
case ch := <-w.done:
case ch := <-w.quit:
w.mu.Lock()
var indexes []indexMap
for _, index := range w.watches {

View File

@@ -244,13 +244,12 @@ var (
// ErrUnsupported is returned by AddWith() when WithOps() specified an
// Unportable event that's not supported on this platform.
//lint:ignore ST1012 not relevant
xErrUnsupported = errors.New("fsnotify: not supported with this backend")
)
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
ev, errs := make(chan Event, defaultBufferSize), make(chan error)
ev, errs := make(chan Event), make(chan error)
b, err := newBackend(ev, errs)
if err != nil {
return nil, err
@@ -267,8 +266,8 @@ func NewWatcher() (*Watcher, error) {
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
ev, errs := make(chan Event, sz), make(chan error)
b, err := newBackend(ev, errs)
ev, errs := make(chan Event), make(chan error)
b, err := newBufferedBackend(sz, ev, errs)
if err != nil {
return nil, err
}
@@ -338,8 +337,7 @@ func (w *Watcher) Close() error { return w.b.Close() }
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// The order is undefined, and may differ per call. Returns nil if
// [Watcher.Close] was called.
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { return w.b.WatchList() }
// Supports reports if all the listed operations are supported by this platform.

Some files were not shown because too many files have changed in this diff Show More