Compare commits

..

47 Commits

Author SHA1 Message Date
Viktor Scharf
2b4a88feb9 trigger full-ci 2025-11-06 17:44:21 +01:00
Ralf Haferkamp
96042752bb Bump reva
Fixes: #1774
2025-11-06 17:24:42 +01:00
Viktor Scharf
4ffb79b680 lint fix 2025-11-06 16:31:21 +01:00
Viktor Scharf
a0f90fee1a Sync share before action 2025-11-06 16:31:21 +01:00
opencloudeu
d8859757d9 [tx] updated from transifex 2025-11-06 00:02:59 +00:00
Ralf Haferkamp
bb776c7556 fix typo
Co-authored-by: Benedikt Kulmann <benedikt@kulmann.biz>
2025-11-05 11:57:48 +01:00
Ralf Haferkamp
177afc41c7 fix: set global signing secret fallback correctly
When falling back to the transfer secret we need to set the global
cfg.URLSigningSecret as well otherwise the Validate() step fails.
2025-11-05 11:57:48 +01:00
Ralf Haferkamp
500487f2fa test: fix a few more collaborative posixfs test (#1777)
wait for postprocessing to complete before accessing files on disk

Related: #1747
2025-11-04 16:46:34 +01:00
Ralf Haferkamp
8a7d51ca88 Apply typo fixes from code review
Co-authored-by: Michael Barz <michael.barz@zeitgestalten.eu>
2025-11-04 16:45:08 +01:00
Ralf Haferkamp
a2f9cadd9f feat(collaboration): Set IsAnonymousUser flag for Collabora
Closes: #796
2025-11-04 16:45:08 +01:00
Ralf Haferkamp
30ef495c92 feat(collaboration): Set IsAdminUser property for Collabora
This set the 'IsAdminUser' Property correctly in the CheckFileInfo
Response. For that a new Permission 'WebOffice.Manage' is introduced. By
default this permission is only assigned to the Admin role.
User with this permission get access to certain admin features in
Collabora (e.g. the 'Server Audit' dashboard)

Closes: #796
2025-11-04 16:45:08 +01:00
dependabot[bot]
2da203613a build(deps): bump github.com/gabriel-vasile/mimetype (#1775)
Bumps [github.com/gabriel-vasile/mimetype](https://github.com/gabriel-vasile/mimetype) from 1.4.10 to 1.4.11.
- [Release notes](https://github.com/gabriel-vasile/mimetype/releases)
- [Commits](https://github.com/gabriel-vasile/mimetype/compare/v1.4.10...v1.4.11)

---
updated-dependencies:
- dependency-name: github.com/gabriel-vasile/mimetype
  dependency-version: 1.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 16:02:02 +01:00
Ralf Haferkamp
fcff855e16 feat: Add fallback for OC_URL_SIGNING_SECRET
When OC_URL_SIGNING_SECRET is not set. Fall back to the value of the
reva transfer token. This allows handling upgrades on a instance that
was created before the OC_URL_SIGNING_SECRET was introduced to be
handled more graceful.

Unfortunately this still only works reliably for single instance
deployments (or instance that where bootstrapped using 'opencloud init')
that are guaranteed to have the transfer token available.

When running 'proxy' and 'ocdav' as separate services the upgrade might
still require manual intervention.
2025-11-04 16:01:00 +01:00
Ralf Haferkamp
37609e52df feat!: Make the url signing secret a mandatory config option
This is required for allowing the web office to download images to
insert into documents.

The secret is generated by `opencloud init` and the server refuses
to start now without a secret being set. (Breaking Change)

Also the setting is now moved to the shared options as all involved
services need the same secret to work properly.

Related: https://github.com/opencloud-eu/web/issues/704
2025-11-04 16:01:00 +01:00
Ralf Haferkamp
589cee4ab3 collaboration: Enable InsertRemoteImage option
Related: https://github.com/opencloud-eu/web/issues/704
2025-11-04 16:01:00 +01:00
dependabot[bot]
dcaa1ceadb build(deps): bump github.com/nats-io/nats-server/v2
Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.12.0 to 2.12.1.
- [Release notes](https://github.com/nats-io/nats-server/releases)
- [Changelog](https://github.com/nats-io/nats-server/blob/main/.goreleaser.yml)
- [Commits](https://github.com/nats-io/nats-server/compare/v2.12.0...v2.12.1)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats-server/v2
  dependency-version: 2.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 15:41:03 +01:00
dependabot[bot]
6e0bb09aff build(deps): bump github.com/onsi/ginkgo/v2 from 2.27.1 to 2.27.2
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.27.1 to 2.27.2.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.27.1...v2.27.2)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-version: 2.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 12:51:49 +01:00
Viktor Scharf
59eb411024 correct STORAGE_USERS_POSIX_WATCH_FS env typo in CI (#1746)
* correct env typo

* set STORAGE_USERS_POSIX_SCAN_DEBOUNCE_DELAY=0

* tests: wait for postprocessing to finish before accessing files

---------

Co-authored-by: Ralf Haferkamp <r.haferkamp@opencloud.eu>
2025-11-04 10:22:06 +01:00
OpenCloud Devops
b53b4ef1de 🎉 Release 3.7.0 (#1723)
* 🎉 Release 3.7.0

* 🎉 Release 3.7.0

* 🎉 Release 3.7.0

* 🎉 Release 3.7.0

* 🎉 Release 3.7.0

* 🎉 Release 3.7.0

* 🎉 Release 3.7.0

* 🎉 Release 3.7.0

* 🎉 Release 3.7.0

* 🎉 Release 3.7.0
2025-11-03 16:27:10 +01:00
Viktor Scharf
c05c740fa6 bump web 4.2.0 (#1765) 2025-11-03 15:55:36 +01:00
Artur Neumann
5b98860585 check status of postprocessing before accesing the file (#1762) 2025-11-03 14:13:46 +01:00
Benedikt Kulmann
a3c3b6a07c Merge pull request #1755 from opencloud-eu/update-gh-labels
chore: update labels
2025-11-03 13:43:44 +01:00
opencloudeu
790c6b165f [tx] updated from transifex 2025-11-02 00:03:10 +00:00
opencloudeu
a2935abe3d [tx] updated from transifex 2025-11-01 00:02:51 +00:00
Benedikt Kulmann
a4856b4a80 chore: update labels 2025-10-31 15:50:26 +01:00
Ralf Haferkamp
b5b15f29de bump reva
fixes: #1747
2025-10-30 17:17:27 +01:00
opencloudeu
e270cdbfd2 [tx] updated from transifex 2025-10-29 00:03:02 +00:00
Ralf Haferkamp
e2441696c2 graph(education): 'primaryRole' and 'identities' should be optional
Related: #1597
2025-10-28 12:56:42 +01:00
Ralf Haferkamp
28ec9c3282 graph(education): Make 'schoolNumber' attribute optional
It's already optional in the spec. For mulit-tenant provisioning
we want it to be optional as well.

Related: #1597
2025-10-28 12:56:42 +01:00
Shawn Wilsher
920a6916c4 fix: only search LDAP group by name 2025-10-28 10:22:54 +01:00
Thomas Schweiger
10e77768a5 Merge pull request #1708 from opencloud-eu/schweigisito/issue1698
fix: fix #1698 - Notification email doesn't contain Message-Id header
2025-10-27 20:59:06 +01:00
Thomas Schweiger
e7a4cbaae5 fix: fix #1698 - Notification email doesn't contain Message-ID header 2025-10-27 18:56:37 +01:00
OpenCloud Devops
e62e2e0f12 🎉 Release 3.6.0 (#1537)
* 🎉 Release 3.5.1

* 🎉 Release 3.5.1

* 🎉 Release 3.5.1

* 🎉 Release 3.5.1

* 🎉 Release 3.5.1

* 🎉 Release 3.5.1

* 🎉 Release 3.5.1

* 🎉 Release 3.5.1

* 🎉 Release 3.5.1

* 🎉 Release 3.5.1

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0

* 🎉 Release 3.6.0
2025-10-27 16:26:38 +01:00
dependabot[bot]
9cb973baac build(deps): bump github.com/onsi/ginkgo/v2 from 2.26.0 to 2.27.1
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.26.0 to 2.27.1.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.26.0...v2.27.1)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-version: 2.27.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 16:16:33 +01:00
Viktor Scharf
9e16bb9e29 bump-version-v3.6.0 (#1719) 2025-10-27 15:03:02 +01:00
Jörn Friedrich Dreyer
641dac0a88 Merge pull request #1718 from opencloud-eu/revaBump-2.39.1
revaBump-2.39.1
2025-10-27 14:12:01 +01:00
Viktor Scharf
570ec0bf97 revaBump-2.39.1 2025-10-27 13:30:13 +01:00
Jörn Friedrich Dreyer
aaaf5cf5c4 Merge pull request #1622 from opencloud-eu/shutdownorder
allow specifying a shutdown order
2025-10-27 13:13:04 +01:00
Florian Schade
fb8af22073 chore: bump reva (#1701)
* chore: bump reva

* enhancement(test): add postprocessing wait helper
2025-10-27 12:01:27 +01:00
Jörn Friedrich Dreyer
8c9f266ded allow specifying a shutdown order
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2025-10-27 11:58:14 +01:00
Viktor Scharf
f04f6ad470 [full-ci] feat: implement OIDC authentication option (#1676)
* feat: implement Bearer Token authentication option

* fix
2025-10-27 11:17:44 +01:00
opencloudeu
c887947a85 [tx] updated from transifex 2025-10-27 00:01:43 +00:00
opencloudeu
ac8be264f0 [tx] updated from transifex 2025-10-26 00:01:09 +00:00
Thomas Schweiger
2c18d5b010 fix: apply changes from other fixes in compose repo (#1707)
* fix: apply changes from other fixes in compose repo

* temporarily disabled e2e navigation step

---------

Co-authored-by: Viktor Scharf <v.scharf@opencloud.eu>
2025-10-24 22:10:27 +02:00
Viktor Scharf
44ee182aa3 apiTest-coverage for #1523 (#1660)
* apiTest-coverage for #1523

* check propfind contans correct files name

* bump reva for getting #381
2025-10-24 09:45:03 +02:00
dependabot[bot]
d76cacd99f build(deps): bump github.com/kovidgoyal/imaging from 1.6.4 to 1.7.2
Bumps [github.com/kovidgoyal/imaging](https://github.com/kovidgoyal/imaging) from 1.6.4 to 1.7.2.
- [Release notes](https://github.com/kovidgoyal/imaging/releases)
- [Changelog](https://github.com/kovidgoyal/imaging/blob/master/.goreleaser.yaml)
- [Commits](https://github.com/kovidgoyal/imaging/compare/v1.6.4...v1.7.2)

---
updated-dependencies:
- dependency-name: github.com/kovidgoyal/imaging
  dependency-version: 1.7.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-23 17:48:43 +02:00
dependabot[bot]
fb94f34a1f build(deps): bump github.com/blevesearch/bleve/v2 from 2.5.3 to 2.5.4
Bumps [github.com/blevesearch/bleve/v2](https://github.com/blevesearch/bleve) from 2.5.3 to 2.5.4.
- [Release notes](https://github.com/blevesearch/bleve/releases)
- [Commits](https://github.com/blevesearch/bleve/compare/v2.5.3...v2.5.4)

---
updated-dependencies:
- dependency-name: github.com/blevesearch/bleve/v2
  dependency-version: 2.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-23 17:47:05 +02:00
372 changed files with 20872 additions and 4314 deletions

View File

@@ -1 +1,4 @@
_extends: gh-labels

View File

@@ -1,4 +1,4 @@
# The test runner source for UI tests
WEB_COMMITID=425ec35c41a1f034a8f9dbc6ab7f7d27b994578a
WEB_COMMITID=6abffcc9cff31c46a341105eb6030fec56338126
WEB_BRANCH=main

View File

@@ -338,6 +338,7 @@ config = {
"FRONTEND_READONLY_USER_ATTRIBUTES": "user.onPremisesSamAccountName,user.displayName,user.mail,user.passwordProfile,user.accountEnabled,user.appRoleAssignments",
"OC_LDAP_SERVER_WRITE_ENABLED": False,
"OC_EXCLUDE_RUN_SERVICES": "idm",
"OC_LDAP_USER_ENABLED_ATTRIBUTE": "",
},
},
},
@@ -350,7 +351,7 @@ config = {
"part": {
"skip": False,
"totalParts": 4, # divide and run all suites in parts (divide pipelines)
"xsuites": ["search", "app-provider", "app-provider-onlyOffice", "app-store", "keycloak", "oidc", "ocm", "a11y", "mobile-view"], # suites to skip
"xsuites": ["search", "app-provider", "app-provider-onlyOffice", "app-store", "keycloak", "oidc", "ocm", "a11y", "mobile-view", "navigation"], # suites to skip
},
"search": {
"skip": False,
@@ -1076,6 +1077,7 @@ def localApiTests(name, suites, storage = "decomposed", extra_environment = {},
"WITH_REMOTE_PHP": with_remote_php,
"COLLABORATION_SERVICE_URL": "http://wopi-fakeoffice:9300",
"OC_STORAGE_PATH": "$HOME/.opencloud/storage/users",
"USE_BEARER_TOKEN": True,
}
for item in extra_environment:
@@ -1479,7 +1481,7 @@ def multiServiceE2ePipeline(ctx, watch_fs_enabled = False):
}
if watch_fs_enabled:
extra_server_environment["STORAGE_USES_POSIX_WATCH_FS"] = True
extra_server_environment["STORAGE_USERS_POSIX_WATCH_FS"] = True
storage_users_environment = {
"OC_CORS_ALLOW_ORIGINS": "%s,https://%s:9201" % (OC_URL, OC_SERVER_NAME),
@@ -2071,6 +2073,7 @@ def opencloudServer(storage = "decomposed", accounts_hash_difficulty = 4, depend
"WEB_DEBUG_ADDR": "0.0.0.0:9104",
"WEBDAV_DEBUG_ADDR": "0.0.0.0:9119",
"WEBFINGER_DEBUG_ADDR": "0.0.0.0:9279",
"STORAGE_USERS_POSIX_SCAN_DEBOUNCE_DELAY": 0,
}
if storage == "posix":
@@ -2108,7 +2111,7 @@ def opencloudServer(storage = "decomposed", accounts_hash_difficulty = 4, depend
environment["SEARCH_EXTRACTOR_CS3SOURCE_INSECURE"] = True
if watch_fs_enabled:
environment["STORAGE_USES_POSIX_WATCH_FS"] = True
environment["STORAGE_USERS_POSIX_WATCH_FS"] = True
# Pass in "default" accounts_hash_difficulty to not set this environment variable.
# That will allow OpenCloud to use whatever its built-in default is.

View File

@@ -1,5 +1,103 @@
# Changelog
## [3.7.0](https://github.com/opencloud-eu/opencloud/releases/tag/v3.7.0) - 2025-11-03
### ❤️ Thanks to all contributors! ❤️
@ScharfViktor, @individual-it, @kulmann, @rhafer, @schweigisito, @sdwilsh
### ✅ Tests
- check status of postprocessing before accesing the file [[#1762](https://github.com/opencloud-eu/opencloud/pull/1762)]
### 📈 Enhancement
- multi-tenancy: Optional attributes on provision API [[#1663](https://github.com/opencloud-eu/opencloud/pull/1663)]
- fix: fix #1698 - Notification email doesn't contain Message-Id header [[#1708](https://github.com/opencloud-eu/opencloud/pull/1708)]
### 🐛 Bug Fixes
- fix: only search LDAP group by name [[#1724](https://github.com/opencloud-eu/opencloud/pull/1724)]
### 📦️ Dependencies
- [full-ci] bump web 4.2.0 and opencloud 3.7.0 version [[#1765](https://github.com/opencloud-eu/opencloud/pull/1765)]
## [3.6.0](https://github.com/opencloud-eu/opencloud/releases/tag/v3.6.0) - 2025-10-27
### ❤️ Thanks to all contributors! ❤️
@AlexAndBear, @ScharfViktor, @butonic, @dragonchaser, @fschade, @micbar, @prashant-gurung899, @rhafer, @schweigisito, @tammi-23
### 📈 Enhancement
- allow specifying a shutdown order [[#1622](https://github.com/opencloud-eu/opencloud/pull/1622)]
- change: use 404 as status when thumbnail can not be fetched [[#1582](https://github.com/opencloud-eu/opencloud/pull/1582)]
- feat: add dedicated logo (web) for mobile view to theme [[#1579](https://github.com/opencloud-eu/opencloud/pull/1579)]
- feat: make it possible to start the collaboration service in the single process [[#1569](https://github.com/opencloud-eu/opencloud/pull/1569)]
- introduce AppURLs helper for atomic backgroud updates [[#1542](https://github.com/opencloud-eu/opencloud/pull/1542)]
- chore: add config for capability CheckForUpdates [[#1556](https://github.com/opencloud-eu/opencloud/pull/1556)]
### ✅ Tests
- [full-ci] feat: implement OIDC authentication option [[#1676](https://github.com/opencloud-eu/opencloud/pull/1676)]
- apiTest-coverage for #1523 [[#1660](https://github.com/opencloud-eu/opencloud/pull/1660)]
- [full-ci] deleted unused step definitions [[#1639](https://github.com/opencloud-eu/opencloud/pull/1639)]
- check thumbnails in the share with me response [[#1605](https://github.com/opencloud-eu/opencloud/pull/1605)]
- [full-ci][tests-only] fix restore browsers cache workflow [[#1615](https://github.com/opencloud-eu/opencloud/pull/1615)]
- [full-ci] Enhance getSpaceByName: check local cache before Graph API calls [[#1574](https://github.com/opencloud-eu/opencloud/pull/1574)]
- [full-ci] getting personal space by userId instead of userName [[#1553](https://github.com/opencloud-eu/opencloud/pull/1553)]
- apiTest-flaky: sync share before checking [[#1550](https://github.com/opencloud-eu/opencloud/pull/1550)]
- [decomposed] use Alpine for opencloud starting [[#1547](https://github.com/opencloud-eu/opencloud/pull/1547)]
### 🐛 Bug Fixes
- fix: apply changes from other fixes in compose repo [[#1707](https://github.com/opencloud-eu/opencloud/pull/1707)]
- fix(settings): env var precedence [[#1625](https://github.com/opencloud-eu/opencloud/pull/1625)]
- fix(antivirus): update icap-client library which fixes tcp socket reuse [[#1589](https://github.com/opencloud-eu/opencloud/pull/1589)]
- fix: use valid autocomplete values (axe autocomplete-valid) [[#1588](https://github.com/opencloud-eu/opencloud/pull/1588)]
- Fix collaboration service name [[#1577](https://github.com/opencloud-eu/opencloud/pull/1577)]
- let the runtime always create a cancel context [[#1565](https://github.com/opencloud-eu/opencloud/pull/1565)]
- Bump reva and cs3apis [[#1538](https://github.com/opencloud-eu/opencloud/pull/1538)]
- use correct endpoint in nats check [[#1533](https://github.com/opencloud-eu/opencloud/pull/1533)]
### 📚 Documentation
- adr: use eduation api for multi-tenancy provisioning [[#1548](https://github.com/opencloud-eu/opencloud/pull/1548)]
- fix: remove deprecated web ui feature "OpenAppsInTab" [[#1575](https://github.com/opencloud-eu/opencloud/pull/1575)]
### 📦️ Dependencies
- build(deps): bump github.com/onsi/ginkgo/v2 from 2.26.0 to 2.27.1 [[#1705](https://github.com/opencloud-eu/opencloud/pull/1705)]
- [decomposed] bump-version-v3.6.0 [[#1719](https://github.com/opencloud-eu/opencloud/pull/1719)]
- revaBump-2.39.1 [[#1718](https://github.com/opencloud-eu/opencloud/pull/1718)]
- chore: bump reva [[#1701](https://github.com/opencloud-eu/opencloud/pull/1701)]
- build(deps): bump github.com/kovidgoyal/imaging from 1.6.4 to 1.7.2 [[#1696](https://github.com/opencloud-eu/opencloud/pull/1696)]
- build(deps): bump github.com/blevesearch/bleve/v2 from 2.5.3 to 2.5.4 [[#1697](https://github.com/opencloud-eu/opencloud/pull/1697)]
- build(deps): bump golang.org/x/oauth2 from 0.31.0 to 0.32.0 [[#1634](https://github.com/opencloud-eu/opencloud/pull/1634)]
- build(deps): bump golang.org/x/net from 0.44.0 to 0.46.0 [[#1638](https://github.com/opencloud-eu/opencloud/pull/1638)]
- revaBumb: add groupware capabilities [[#1689](https://github.com/opencloud-eu/opencloud/pull/1689)]
- revaUpdate: adding groupware capabilities [[#1659](https://github.com/opencloud-eu/opencloud/pull/1659)]
- chore/bump-web-4.1.0 [[#1652](https://github.com/opencloud-eu/opencloud/pull/1652)]
- build(deps): bump google.golang.org/grpc from 1.75.1 to 1.76.0 [[#1628](https://github.com/opencloud-eu/opencloud/pull/1628)]
- build(deps): bump github.com/coreos/go-oidc/v3 from 3.15.0 to 3.16.0 [[#1627](https://github.com/opencloud-eu/opencloud/pull/1627)]
- build(deps): bump github.com/grpc-ecosystem/grpc-gateway/v2 from 2.27.2 to 2.27.3 [[#1608](https://github.com/opencloud-eu/opencloud/pull/1608)]
- build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.11 to 3.4.12 [[#1609](https://github.com/opencloud-eu/opencloud/pull/1609)]
- build(deps): bump google.golang.org/protobuf from 1.36.9 to 1.36.10 [[#1604](https://github.com/opencloud-eu/opencloud/pull/1604)]
- build(deps): bump github.com/onsi/ginkgo/v2 from 2.25.3 to 2.26.0 [[#1603](https://github.com/opencloud-eu/opencloud/pull/1603)]
- build(deps): bump github.com/nats-io/nats.go from 1.46.0 to 1.46.1 [[#1590](https://github.com/opencloud-eu/opencloud/pull/1590)]
- build(deps): bump github.com/olekukonko/tablewriter from 1.0.9 to 1.1.0 [[#1584](https://github.com/opencloud-eu/opencloud/pull/1584)]
- build(deps): bump github.com/open-policy-agent/opa from 1.8.0 to 1.9.0 [[#1576](https://github.com/opencloud-eu/opencloud/pull/1576)]
- build(deps): bump github.com/nats-io/nats-server/v2 from 2.11.9 to 2.12.0 [[#1568](https://github.com/opencloud-eu/opencloud/pull/1568)]
- build(deps): bump golang.org/x/net from 0.43.0 to 0.44.0 [[#1567](https://github.com/opencloud-eu/opencloud/pull/1567)]
- reva bump. getting #327 [[#1555](https://github.com/opencloud-eu/opencloud/pull/1555)]
- build(deps): bump golang.org/x/image from 0.30.0 to 0.31.0 [[#1552](https://github.com/opencloud-eu/opencloud/pull/1552)]
- build(deps): bump github.com/nats-io/nats.go from 1.45.0 to 1.46.0 [[#1551](https://github.com/opencloud-eu/opencloud/pull/1551)]
- build(deps): bump golang.org/x/crypto from 0.41.0 to 0.42.0 [[#1545](https://github.com/opencloud-eu/opencloud/pull/1545)]
- build(deps): bump github.com/testcontainers/testcontainers-go/modules/opensearch from 0.38.0 to 0.39.0 [[#1544](https://github.com/opencloud-eu/opencloud/pull/1544)]
- build(deps): bump github.com/open-policy-agent/opa from 1.6.0 to 1.8.0 [[#1510](https://github.com/opencloud-eu/opencloud/pull/1510)]
- build(deps): bump google.golang.org/grpc from 1.75.0 to 1.75.1 [[#1534](https://github.com/opencloud-eu/opencloud/pull/1534)]
## [3.5.0](https://github.com/opencloud-eu/opencloud/releases/tag/v3.5.0) - 2025-09-22
### ❤️ Thanks to all contributors! ❤️

View File

@@ -663,6 +663,7 @@
"profile",
"roles",
"groups",
"OpenCloudUnique_ID",
"basic",
"email"
],
@@ -2308,7 +2309,7 @@
"always"
],
"usePasswordModifyExtendedOp": [
"false"
"true"
],
"trustEmail": [
"false"

43
go.mod
View File

@@ -11,14 +11,14 @@ require (
github.com/Nerzal/gocloak/v13 v13.9.0
github.com/bbalet/stopwords v1.0.0
github.com/beevik/etree v1.6.0
github.com/blevesearch/bleve/v2 v2.5.3
github.com/blevesearch/bleve/v2 v2.5.4
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/coreos/go-oidc/v3 v3.16.0
github.com/cs3org/go-cs3apis v0.0.0-20250908152307-4ca807afe54e
github.com/davidbyttow/govips/v2 v2.16.0
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
github.com/gabriel-vasile/mimetype v1.4.10
github.com/gabriel-vasile/mimetype v1.4.11
github.com/ggwhite/go-masker v1.1.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/render v1.0.3
@@ -48,24 +48,24 @@ require (
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/jinzhu/now v1.1.5
github.com/justinas/alice v1.2.0
github.com/kovidgoyal/imaging v1.6.4
github.com/kovidgoyal/imaging v1.7.2
github.com/leonelquinteros/gotext v1.7.2
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.12.0
github.com/nats-io/nats.go v1.46.1
github.com/nats-io/nats-server/v2 v2.12.1
github.com/nats-io/nats.go v1.47.0
github.com/oklog/run v1.2.0
github.com/olekukonko/tablewriter v1.1.0
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.26.0
github.com/onsi/ginkgo/v2 v2.27.2
github.com/onsi/gomega v1.38.2
github.com/open-policy-agent/opa v1.9.0
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
github.com/opencloud-eu/reva/v2 v2.39.1-0.20251020192555-e3aa6a7d6d43
github.com/opencloud-eu/reva/v2 v2.39.2-0.20251106122902-c13e27f55362
github.com/opensearch-project/opensearch-go/v4 v4.5.0
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
@@ -104,7 +104,7 @@ require (
go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/crypto v0.43.0
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/image v0.31.0
golang.org/x/image v0.32.0
golang.org/x/net v0.46.0
golang.org/x/oauth2 v0.32.0
golang.org/x/sync v0.17.0
@@ -140,13 +140,13 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/blevesearch/bleve_index_api v1.2.8 // indirect
github.com/blevesearch/bleve_index_api v1.2.10 // indirect
github.com/blevesearch/geo v0.2.4 // indirect
github.com/blevesearch/go-faiss v1.0.25 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.0.4 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.10 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.3.12 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
@@ -156,12 +156,12 @@ 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.6 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/bombsimon/logrusr/v3 v3.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/ceph/go-ceph v0.35.0 // indirect
github.com/ceph/go-ceph v0.36.0 // indirect
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.6.1 // indirect
@@ -229,14 +229,14 @@ require (
github.com/gobwas/ws v1.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.9.2 // indirect
github.com/gomodule/redigo v1.9.3 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/google/go-tpm v0.9.6 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/renameio/v2 v2.0.0 // indirect
github.com/gookit/goutil v0.7.1 // indirect
@@ -257,6 +257,8 @@ require (
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/kovidgoyal/go-parallel v1.0.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect
@@ -280,10 +282,10 @@ require (
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/mileusna/useragent v1.3.5 // indirect
github.com/minio/crc64nvme v1.0.2 // indirect
github.com/minio/crc64nvme v1.1.0 // 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.95 // indirect
github.com/minio/minio-go/v7 v7.0.97 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
@@ -327,6 +329,7 @@ require (
github.com/rs/xid v1.6.0 // indirect
github.com/russellhaering/goxmldsig v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/segmentio/kafka-go v0.4.49 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
@@ -334,7 +337,7 @@ require (
github.com/sergi/go-diff v1.4.0 // indirect
github.com/sethvargo/go-diceware v0.5.0 // indirect
github.com/sethvargo/go-password v0.3.1 // indirect
github.com/shamaton/msgpack/v2 v2.3.1 // indirect
github.com/shamaton/msgpack/v2 v2.4.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
@@ -369,16 +372,14 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // 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
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/time v0.13.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/tools/godoc v0.1.0-deprecated // indirect
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect

90
go.sum
View File

@@ -151,10 +151,10 @@ 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.3 h1:9l1xtKaETv64SZc1jc4Sy0N804laSa/LeMbYddq1YEM=
github.com/blevesearch/bleve/v2 v2.5.3/go.mod h1:Z/e8aWjiq8HeX+nW8qROSxiE0830yQA071dwR3yoMzw=
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/bleve/v2 v2.5.4 h1:1iur8e+PHsxtncV2xIVuqlQme/V8guEDO2uV6Wll3lQ=
github.com/blevesearch/bleve/v2 v2.5.4/go.mod h1:yB4PnV4N2q5rTEpB2ndG8N2ISexBQEFIYgwx4ztfvoo=
github.com/blevesearch/bleve_index_api v1.2.10 h1:FMFmZCmTX6PdoLLvwUnKF2RsmILFFwO3h0WPevXY9fE=
github.com/blevesearch/bleve_index_api v1.2.10/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
@@ -165,8 +165,8 @@ github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZG
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
github.com/blevesearch/scorch_segment_api/v2 v2.3.10 h1:Yqk0XD1mE0fDZAJXTjawJ8If/85JxnLd8v5vG/jWE/s=
github.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8=
github.com/blevesearch/scorch_segment_api/v2 v2.3.12 h1:GGZc2qwbyRBwtckPPkHkLyXw64mmsLJxdturBI1cM+c=
github.com/blevesearch/scorch_segment_api/v2 v2.3.12/go.mod h1:JBRGAneqgLSI2+jCNjtwMqp2B7EBF3/VUzgDPIU33MM=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
@@ -185,8 +185,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.6 h1:OHuUl2GhM+FpBq9RwNsJ4k/QodqbMMHoQEgn/IHYpu8=
github.com/blevesearch/zapx/v16 v16.2.6/go.mod h1:cuAPB+YoIyRngNhno1S1GPr9SfMk+x/SgAHBLXSIq3k=
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=
@@ -210,8 +210,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/ceph/go-ceph v0.35.0 h1:wcDUbsjeNJ7OfbWCE7I5prqUL794uXchopw3IvrGQkk=
github.com/ceph/go-ceph v0.35.0/go.mod h1:ILF8WKhQQ2p2YuX1oWigkmsfT39U8T/HS2NrqxExq2s=
github.com/ceph/go-ceph v0.36.0 h1:IDE4vEF+4fmjve+CPjD1WStgfQ+Lh6vD+9PMUI712KI=
github.com/ceph/go-ceph v0.36.0/go.mod h1:fGCbndVDLuHW7q2954d6y+tgPFOBnRLqJRe2YXyngw4=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -349,8 +349,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
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/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gdexlab/go-render v1.0.1 h1:rxqB3vo5s4n1kF0ySmoNeSPRYkEsyHgln4jFIQY7v0U=
github.com/gdexlab/go-render v1.0.1/go.mod h1:wRi5nW2qfjiGj4mPukH4UV0IknS1cHD4VgFTmJX5JzM=
github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
@@ -361,8 +361,8 @@ github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BN
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
github.com/gkampitakis/go-snaps v0.5.14 h1:3fAqdB6BCPKHDMHAKRwtPUwYexKtGrNuw8HX/T/4neo=
github.com/gkampitakis/go-snaps v0.5.14/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-acme/lego/v4 v4.4.0 h1:uHhU5LpOYQOdp3aDU+XY2bajseu8fuExphTL1Ss6/Fc=
@@ -480,8 +480,8 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@@ -536,8 +536,8 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
@@ -565,8 +565,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
github.com/google/go-tika v0.3.1/go.mod h1:DJh5N8qxXIl85QkqmXknd+PeeRkUOTbvwyYf7ieDz6c=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -723,14 +723,18 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kobergj/gowebdav v0.0.0-20250102091030-aa65266db202 h1:A1xJ2NKgiYFiaHiLl9B5yw/gUBACSs9crDykTS3GuQI=
github.com/kobergj/gowebdav v0.0.0-20250102091030-aa65266db202/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
github.com/kovidgoyal/go-parallel v1.0.1 h1:nYUjN+EdpbmQjTg3N5eTUInuXTB3/1oD2vHdaMfuHoI=
github.com/kovidgoyal/go-parallel v1.0.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
github.com/kovidgoyal/imaging v1.7.2 h1:mmT6k6Az3mC6dbqdZ6Q9KQCdZFWTAQ+q97NyGZgJ/2c=
github.com/kovidgoyal/imaging v1.7.2/go.mod h1:GdkCORjfZMMGFY0Pb7TDmRhj7PDhxF/QShKukSCj0VU=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -835,14 +839,14 @@ github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
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.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
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=
@@ -897,10 +901,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.12.0 h1:OIwe8jZUqJFrh+hhiyKu8snNib66qsx806OslqJuo74=
github.com/nats-io/nats-server/v2 v2.12.0/go.mod h1:nr8dhzqkP5E/lDwmn+A2CvQPMd1yDKXQI7iGg3lAvww=
github.com/nats-io/nats.go v1.46.1 h1:bqQ2ZcxVd2lpYI97xYASeRTY3I5boe/IVmuUDPitHfo=
github.com/nats-io/nats.go v1.46.1/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats-server/v2 v2.12.1 h1:0tRrc9bzyXEdBLcHr2XEjDzVpUxWx64aZBm7Rl1QDrA=
github.com/nats-io/nats-server/v2 v2.12.1/go.mod h1:OEaOLmu/2e6J9LzUt2OuGjgNem4EpYApO5Rpf26HDs8=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.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=
@@ -931,8 +935,8 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE=
github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
@@ -946,8 +950,8 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76 h1:vD/EdfDUrv4omSFjrinT8Mvf+8D7f9g4vgQ2oiDrVUI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.39.1-0.20251020192555-e3aa6a7d6d43 h1:GQWk2gk8BcbIVoEysI3QRVfARaZJG9jMOpNTKSIr/hY=
github.com/opencloud-eu/reva/v2 v2.39.1-0.20251020192555-e3aa6a7d6d43/go.mod h1:rWCkqbdtVGVcZLZ2uw2kLGGjGnK8NTXfy9y0+rMyL8M=
github.com/opencloud-eu/reva/v2 v2.39.2-0.20251106122902-c13e27f55362 h1:O9oHbqPnC+tAQTbaLD4Tj6I5jmSmTLaQCynTHkFP+cI=
github.com/opencloud-eu/reva/v2 v2.39.2-0.20251106122902-c13e27f55362/go.mod h1:hOCR1OHAhGY8ecpq6sIS5Ru1ZOC/hBgNz+sYf6CrO9Y=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -999,8 +1003,6 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:Om
github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k=
github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/alertmanager v0.28.1 h1:BK5pCoAtaKg01BYRUJhEDV1tqJMEtYBGzPw8QdvnnvA=
github.com/prometheus/alertmanager v0.28.1/go.mod h1:0StpPUDDHi1VXeM7p2yYfeZgLVi/PPlt39vo9LQUHxM=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -1079,6 +1081,8 @@ github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvD
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
@@ -1097,8 +1101,8 @@ github.com/sethvargo/go-diceware v0.5.0 h1:exrQ7GpaBo00GqRVM1N8ChXSsi3oS7tjQiIeh
github.com/sethvargo/go-diceware v0.5.0/go.mod h1:Lg1SyPS7yQO6BBgTN5r4f2MUDkqGfLWsOjHPY0kA8iw=
github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
github.com/shamaton/msgpack/v2 v2.3.1 h1:R3QNLIGA/tbdczNMZ5PCRxrXvy+fnzsIaHG4kKMgWYo=
github.com/shamaton/msgpack/v2 v2.3.1/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4=
github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
@@ -1304,8 +1308,6 @@ go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzK
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/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=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -1360,8 +1362,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.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
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=
@@ -1591,8 +1593,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -2,7 +2,6 @@ package command
import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"errors"
@@ -11,11 +10,8 @@ import (
"net/http"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/olekukonko/tablewriter"
@@ -68,15 +64,8 @@ func BenchmarkClientCommand(cfg *config.Config) *cli.Command {
&cli.StringFlag{
Name: "data",
Aliases: []string{"d"},
Usage: "Sends the specified data in a POST request to the HTTP server, in the same way that a browser does when a user has filled in an HTML form and presses the submit button. If you start the data with the letter @, the rest should be a file name to read the data from, or - if you want to read the data from stdin. When -d, --data is told to read from a file like that, carriage returns and newlines are stripped out. If you do not want the @ character to have a special interpretation use --data-raw instead.",
},
&cli.StringFlag{
Name: "data-binary",
Usage: "This posts data exactly as specified with no extra processing whatsoever. If you start the data with the letter @, the rest should be a file name to read the data from, or - if you want to read the data from stdin.",
},
&cli.StringFlag{
Name: "data-raw",
Usage: "Sends the specified data in a request to the HTTP server.",
Usage: "Sends the specified data in a request to the HTTP server.",
// TODE support multiple data flags, support data-binary, data-raw
},
&cli.StringSliceFlag{
Name: "header",
@@ -118,97 +107,14 @@ func BenchmarkClientCommand(cfg *config.Config) *cli.Command {
},
Category: "benchmark",
Action: func(c *cli.Context) error {
// Set up signal handling for Ctrl+C
ctx, cancel := context.WithCancel(c.Context)
defer cancel()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("\nReceived interrupt signal, shutting down...")
cancel()
}()
opt := clientOptions{
request: c.String("request"),
url: c.Args().First(),
insecure: c.Bool("insecure"),
jobs: c.Int("jobs"),
headers: make(map[string]string),
data: []byte(c.String("data")),
}
if d := c.String("data-raw"); d != "" {
opt.request = "POST"
opt.headers["Content-Type"] = "application/x-www-form-urlencoded"
opt.data = []byte(d)
}
if d := c.String("data"); d != "" {
opt.request = "POST"
opt.headers["Content-Type"] = "application/x-www-form-urlencoded"
if strings.HasPrefix(d, "@") {
filePath := strings.TrimPrefix(d, "@")
var data []byte
var err error
// read from file or stdin and trim trailing newlines
if filePath == "-" {
data, err = os.ReadFile("/dev/stdin")
} else {
data, err = os.ReadFile(filePath)
}
if err != nil {
log.Fatal(errors.New("could not read data from file '" + filePath + "': " + err.Error()))
}
// clean byte array similar to curl's --data parameter
// It removes leading/trailing whitespace and converts line breaks to spaces
// Trim leading and trailing whitespace
data = bytes.TrimSpace(data)
// Replace newlines and carriage returns with spaces
data = bytes.ReplaceAll(data, []byte("\r\n"), []byte(" "))
data = bytes.ReplaceAll(data, []byte("\n"), []byte(" "))
data = bytes.ReplaceAll(data, []byte("\r"), []byte(" "))
// Replace multiple spaces with single space
for bytes.Contains(data, []byte(" ")) {
data = bytes.ReplaceAll(data, []byte(" "), []byte(" "))
}
opt.data = data
} else {
opt.data = []byte(d)
}
}
if d := c.String("data-binary"); d != "" {
opt.request = "POST"
opt.headers["Content-Type"] = "application/x-www-form-urlencoded"
if strings.HasPrefix(d, "@") {
filePath := strings.TrimPrefix(d, "@")
var data []byte
var err error
if filePath == "-" {
data, err = os.ReadFile("/dev/stdin")
} else {
data, err = os.ReadFile(filePath)
}
if err != nil {
log.Fatal(errors.New("could not read data from file '" + filePath + "': " + err.Error()))
}
opt.data = data
} else {
opt.data = []byte(d)
}
}
// override method if specified
if request := c.String("request"); request != "" {
opt.request = request
}
if opt.url == "" {
log.Fatal(errors.New("no URL specified"))
}
@@ -273,7 +179,7 @@ func BenchmarkClientCommand(cfg *config.Config) *cli.Command {
defer opt.ticker.Stop()
}
return client(ctx, opt)
return client(opt)
},
}
@@ -291,19 +197,16 @@ type clientOptions struct {
jobs int
}
func client(ctx context.Context, o clientOptions) error {
func client(o clientOptions) error {
type stat struct {
job int
duration time.Duration
status int
}
stats := make(chan stat)
var wg sync.WaitGroup
for i := 0; i < o.jobs; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
tr := &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
@@ -314,13 +217,6 @@ func client(ctx context.Context, o clientOptions) error {
cookies := map[string]*http.Cookie{}
for {
// Check if context is cancelled
select {
case <-ctx.Done():
return
default:
}
req, err := http.NewRequest(o.request, o.url, bytes.NewReader(o.data))
if err != nil {
log.Printf("client %d: could not create request: %s\n", i, err)
@@ -338,35 +234,20 @@ func client(ctx context.Context, o clientOptions) error {
res, err := client.Do(req)
duration := -time.Until(start)
if err != nil {
// Check if error is due to context cancellation
if ctx.Err() != nil {
return
}
log.Printf("client %d: could not create request: %s\n", i, err)
time.Sleep(time.Second)
} else {
res.Body.Close()
select {
case stats <- stat{
stats <- stat{
job: i,
duration: duration,
status: res.StatusCode,
}:
case <-ctx.Done():
return
}
for _, c := range res.Cookies() {
cookies[c.Name] = c
}
}
// Sleep with context awareness
if o.rateDelay > duration {
select {
case <-time.After(o.rateDelay - duration):
case <-ctx.Done():
return
}
}
time.Sleep(o.rateDelay - duration)
}
}(i)
}
@@ -375,15 +256,9 @@ func client(ctx context.Context, o clientOptions) error {
if o.ticker == nil {
// no ticker, just write every request
for {
select {
case stat := <-stats:
numRequests++
fmt.Printf("req %d took %v and returned status %d\n", numRequests, stat.duration, stat.status)
case <-ctx.Done():
fmt.Println("\nShutting down...")
wg.Wait()
return nil
}
stat := <-stats
numRequests++
fmt.Printf("req %d took %v and returned status %d\n", numRequests, stat.duration, stat.status)
}
}
@@ -399,13 +274,6 @@ func client(ctx context.Context, o clientOptions) error {
numRequests = 0
duration = 0
}
case <-ctx.Done():
if numRequests > 0 {
fmt.Printf("\n%d req at %v/req\n", numRequests, duration/time.Duration(numRequests))
}
fmt.Println("Shutting down...")
wg.Wait()
return nil
}
}

View File

@@ -68,7 +68,7 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
systemUserID, adminUserID, graphApplicationID, storageUsersMountID, serviceAccountID string
idmServicePassword, idpServicePassword, ocAdminServicePassword, revaServicePassword string
tokenManagerJwtSecret, collaborationWOPISecret, machineAuthAPIKey, systemUserAPIKey string
revaTransferSecret, thumbnailsTransferSecret, serviceAccountSecret string
revaTransferSecret, thumbnailsTransferSecret, serviceAccountSecret, urlSigningSecret string
)
if diff {
@@ -95,6 +95,13 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
revaTransferSecret = oldCfg.TransferSecret
thumbnailsTransferSecret = oldCfg.Thumbnails.Thumbnail.TransferSecret
serviceAccountSecret = oldCfg.Graph.ServiceAccount.ServiceAccountSecret
urlSigningSecret = oldCfg.URLSigningSecret
if urlSigningSecret == "" {
urlSigningSecret, err = generators.GenerateRandomPassword(passwordLength)
if err != nil {
return fmt.Errorf("could not generate random secret for urlSigningSecret: %s", err)
}
}
} else {
systemUserID = uuid.Must(uuid.NewV4()).String()
adminUserID = uuid.Must(uuid.NewV4()).String()
@@ -142,13 +149,17 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
if err != nil {
return fmt.Errorf("could not generate random password for revaTransferSecret: %s", err)
}
urlSigningSecret, err = generators.GenerateRandomPassword(passwordLength)
if err != nil {
return fmt.Errorf("could not generate random secret for urlSigningSecret: %s", err)
}
thumbnailsTransferSecret, err = generators.GenerateRandomPassword(passwordLength)
if err != nil {
return fmt.Errorf("could not generate random password for thumbnailsTransferSecret: %s", err)
}
serviceAccountSecret, err = generators.GenerateRandomPassword(passwordLength)
if err != nil {
return fmt.Errorf("could not generate random password for thumbnailsTransferSecret: %s", err)
return fmt.Errorf("could not generate random secret for serviceAccountSecret: %s", err)
}
}
@@ -164,6 +175,7 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
MachineAuthAPIKey: machineAuthAPIKey,
SystemUserAPIKey: systemUserAPIKey,
TransferSecret: revaTransferSecret,
URLSigningSecret: urlSigningSecret,
SystemUserID: systemUserID,
AdminUserID: adminUserID,
Idm: IdmService{

View File

@@ -19,6 +19,7 @@ type OpenCloudConfig struct {
MachineAuthAPIKey string `yaml:"machine_auth_api_key"`
SystemUserAPIKey string `yaml:"system_user_api_key"`
TransferSecret string `yaml:"transfer_secret"`
URLSigningSecret string `yaml:"url_signing_secret"`
SystemUserID string `yaml:"system_user_id"`
AdminUserID string `yaml:"admin_user_id"`
Graph GraphService `yaml:"graph"`

View File

@@ -521,6 +521,24 @@ func trapShutdownCtx(s *Service, srv *http.Server, ctx context.Context) error {
s.Log.Debug().Msg("runtime listener shutdown done")
}()
// shutdown services in the order defined in the config
// any services not listed will be shutdown in parallel afterwards
for _, sName := range s.cfg.Runtime.ShutdownOrder {
if _, ok := s.serviceToken[sName]; !ok {
s.Log.Warn().Str("service", sName).Msg("unknown service for ordered shutdown, skipping")
continue
}
for i := range s.serviceToken[sName] {
if err := s.Supervisor.RemoveAndWait(s.serviceToken[sName][i], _defaultShutdownTimeoutDuration); err != nil && !errors.Is(err, suture.ErrSupervisorNotRunning) {
s.Log.Error().Err(err).Str("service", sName).Msg("could not shutdown service in order, skipping to next")
// continue shutting down other services
continue
}
s.Log.Debug().Str("service", sName).Msg("graceful ordered shutdown for service done")
}
delete(s.serviceToken, sName)
}
for sName := range s.serviceToken {
for i := range s.serviceToken[sName] {
wg.Add(1)

View File

@@ -50,11 +50,12 @@ type Mode int
// Runtime configures the OpenCloud runtime when running in supervised mode.
type Runtime struct {
Port string `yaml:"port" env:"OC_RUNTIME_PORT" desc:"The TCP port at which OpenCloud will be available" introductionVersion:"1.0.0"`
Host string `yaml:"host" env:"OC_RUNTIME_HOST" desc:"The host at which OpenCloud will be available" introductionVersion:"1.0.0"`
Services []string `yaml:"services" env:"OC_RUN_EXTENSIONS;OC_RUN_SERVICES" desc:"A comma-separated list of service names. Will start only the listed services." introductionVersion:"1.0.0"`
Disabled []string `yaml:"disabled_services" env:"OC_EXCLUDE_RUN_SERVICES" desc:"A comma-separated list of service names. Will start all default services except of the ones listed. Has no effect when OC_RUN_SERVICES is set." introductionVersion:"1.0.0"`
Additional []string `yaml:"add_services" env:"OC_ADD_RUN_SERVICES" desc:"A comma-separated list of service names. Will add the listed services to the default configuration. Has no effect when OC_RUN_SERVICES is set. Note that one can add services not started by the default list and exclude services from the default list by using both envvars at the same time." introductionVersion:"1.0.0"`
Port string `yaml:"port" env:"OC_RUNTIME_PORT" desc:"The TCP port at which OpenCloud will be available" introductionVersion:"1.0.0"`
Host string `yaml:"host" env:"OC_RUNTIME_HOST" desc:"The host at which OpenCloud will be available" introductionVersion:"1.0.0"`
Services []string `yaml:"services" env:"OC_RUN_EXTENSIONS;OC_RUN_SERVICES" desc:"A comma-separated list of service names. Will start only the listed services." introductionVersion:"1.0.0"`
Disabled []string `yaml:"disabled_services" env:"OC_EXCLUDE_RUN_SERVICES" desc:"A comma-separated list of service names. Will start all default services except of the ones listed. Has no effect when OC_RUN_SERVICES is set." introductionVersion:"1.0.0"`
Additional []string `yaml:"add_services" env:"OC_ADD_RUN_SERVICES" desc:"A comma-separated list of service names. Will add the listed services to the default configuration. Has no effect when OC_RUN_SERVICES is set. Note that one can add services not started by the default list and exclude services from the default list by using both envvars at the same time." introductionVersion:"1.0.0"`
ShutdownOrder []string `yaml:"shutdown_order" env:"OC_SHUTDOWN_ORDER" desc:"A comma-separated list of service names defining the order in which services are shut down. Services not listed will be stopped after the listed ones in random order." introductionVersion:"%%NEXT%%"`
}
// Config combines all available configuration parts.
@@ -77,6 +78,7 @@ type Config struct {
TokenManager *shared.TokenManager `yaml:"token_manager"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"1.0.0"`
TransferSecret string `yaml:"transfer_secret" env:"OC_TRANSFER_SECRET" desc:"Transfer secret for signing file up- and download requests." introductionVersion:"1.0.0"`
URLSigningSecret string `yaml:"url_signing_secret" env:"OC_URL_SIGNING_SECRET" desc:"The shared secret used to sign URLs e.g. for image downloads by the web office suite." introductionVersion:"%%NEXT%%"`
SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID" desc:"ID of the OpenCloud storage-system system user. Admins need to set the ID for the storage-system system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"1.0.0"`
SystemUserAPIKey string `yaml:"system_user_api_key" env:"OC_SYSTEM_USER_API_KEY" desc:"API key for the storage-system system user." introductionVersion:"1.0.0"`
AdminUserID string `yaml:"admin_user_id" env:"OC_ADMIN_USER_ID" desc:"ID of a user, that should receive admin privileges. Consider that the UUID can be encoded in some LDAP deployment configurations like in .ldif files. These need to be decoded beforehand." introductionVersion:"1.0.0"`

View File

@@ -50,8 +50,9 @@ func DefaultConfig() *Config {
return &Config{
OpenCloudURL: "https://localhost:9200",
Runtime: Runtime{
Port: "9250",
Host: "localhost",
Port: "9250",
Host: "localhost",
ShutdownOrder: []string{"proxy"},
},
Reva: &shared.Reva{
Address: "eu.opencloud.api.gateway",

View File

@@ -100,6 +100,14 @@ func EnsureCommons(cfg *config.Config) {
cfg.Commons.TransferSecret = cfg.TransferSecret
}
// make sure url signing secret is set and copy it to the commons part
// fall back to transfer secret for url signing secret to avoid
// issues when upgrading from an older release
if cfg.URLSigningSecret == "" {
cfg.URLSigningSecret = cfg.TransferSecret
}
cfg.Commons.URLSigningSecret = cfg.URLSigningSecret
// copy metadata user id to the commons part if set
if cfg.SystemUserID != "" {
cfg.Commons.SystemUserID = cfg.SystemUserID
@@ -128,6 +136,10 @@ func Validate(cfg *config.Config) error {
return shared.MissingRevaTransferSecretError("opencloud")
}
if cfg.URLSigningSecret == "" {
return shared.MissingURLSigningSecret("opencloud")
}
if cfg.MachineAuthAPIKey == "" {
return shared.MissingMachineAuthApiKeyError("opencloud")
}

View File

@@ -93,3 +93,11 @@ func MissingWOPISecretError(service string) error {
"the config/corresponding environment variable).",
service, defaults.BaseConfigPath())
}
func MissingURLSigningSecret(service string) error {
return fmt.Errorf("The URL signing secret has not been set properly in your config for %s. "+
"Make sure your %s config contains the proper values "+
"(e.g. by using 'opencloud init --diff' and applying the patch or setting a value manually in "+
"the config/corresponding environment variable).",
service, defaults.BaseConfigPath())
}

View File

@@ -80,6 +80,7 @@ type Commons struct {
Reva *Reva `yaml:"reva"`
MachineAuthAPIKey string `mask:"password" yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"1.0.0"`
TransferSecret string `mask:"password" yaml:"transfer_secret,omitempty" env:"REVA_TRANSFER_SECRET" desc:"The secret used for signing the requests towards the data gateway for up- and downloads." introductionVersion:"1.0.0"`
URLSigningSecret string `yaml:"url_signing_secret" env:"OC_URL_SIGNING_SECRET" desc:"The shared secret used to sign URLs e.g. for image downloads by the web office suite." introductionVersion:"%%NEXT%%"`
SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID" desc:"ID of the OpenCloud storage-system system user. Admins need to set the ID for the storage-system system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"1.0.0"`
SystemUserAPIKey string `mask:"password" yaml:"system_user_api_key" env:"SYSTEM_USER_API_KEY" desc:"API key for all system users." introductionVersion:"1.0.0"`
AdminUserID string `yaml:"admin_user_id" env:"OC_ADMIN_USER_ID" desc:"ID of a user, that should receive admin privileges. Consider that the UUID can be encoded in some LDAP deployment configurations like in .ldif files. These need to be decoded beforehand." introductionVersion:"1.0.0"`

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.5.0+dev"
LatestTag = "3.7.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

@@ -0,0 +1,103 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jiri Grönroos <jiri.gronroos@iki.fi>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-11-06 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/opencloud-eu/teams/204053/fi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: pkg/service/response.go:44
msgid "description"
msgstr "kuvaus"
#: pkg/service/response.go:43
msgid "display name"
msgstr "näyttönimi"
#: pkg/service/response.go:42
msgid "expiration date"
msgstr "vanhenemispäivä"
#: pkg/service/response.go:41
msgid "password"
msgstr "salasana"
#: pkg/service/response.go:40
msgid "permission"
msgstr "käyttöoikeus"
#: pkg/service/response.go:39
msgid "some field"
msgstr "jokin kieltä"
#: pkg/service/response.go:26
msgid "{resource} was downloaded via public link {token}"
msgstr "{resource} ladattiin julkisen linkin {token} kautta"
#: pkg/service/response.go:24
msgid "{user} added {resource} to {folder}"
msgstr "{user} lisäsi {resource} kansioon {folder}"
#: pkg/service/response.go:36
msgid "{user} added {sharee} as member of {space}"
msgstr "{user} lisäsi {sharee} jäseneksi avaruuteen {space}"
#: pkg/service/response.go:27
msgid "{user} deleted {resource} from {folder}"
msgstr "{user} poisti {resource} kansiosta {folder}"
#: pkg/service/response.go:28
msgid "{user} moved {resource} to {folder}"
msgstr "{user} siirsi {resource} kansioon {folder}"
#: pkg/service/response.go:35
msgid "{user} removed link to {resource}"
msgstr "{user} poisti linkin resurssiin {resource}"
#: pkg/service/response.go:32
msgid "{user} removed {sharee} from {resource}"
msgstr "{user} poisti {sharee} resurssista {resource}"
#: pkg/service/response.go:37
msgid "{user} removed {sharee} from {space}"
msgstr "{user} poisti {sharee} avaruudesta {space}"
#: pkg/service/response.go:29
msgid "{user} renamed {oldResource} to {resource}"
msgstr "{user} muutti kohteen {oldResource} uudeksi nimeksi {resource}"
#: pkg/service/response.go:33
msgid "{user} shared {resource} via link"
msgstr "{user} jakoi {resource} linkin kautta"
#: pkg/service/response.go:30
msgid "{user} shared {resource} with {sharee}"
msgstr "{user} jakoi {resource} käyttäjän {sharee} kanssa"
#: pkg/service/response.go:34
msgid "{user} updated {field} for a link {token} on {resource}"
msgstr ""
"{user} päivitti kentän {field} linkkiin {token} resurssissa {resource}"
#: pkg/service/response.go:31
msgid "{user} updated {field} for the {resource}"
msgstr "{user} päivitti kentän {field} resurssille {resource}"
#: pkg/service/response.go:25
msgid "{user} updated {resource} in {folder}"
msgstr "{user} päivitti {resource} kansiossa {folder}"

View File

@@ -1198,6 +1198,7 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
isAnonymousUser := true
isPublicShare := false
isAdminUser := false
user := ctxpkg.ContextMustGetUser(ctx)
if user.String() != "" {
// if we have a wopiContext.User
@@ -1207,6 +1208,12 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
isAnonymousUser = false
userFriendlyName = user.GetDisplayName()
userId = hexEncodedWopiUserId
isAdminUser, err = utils.CheckPermission(ctx, "WebOffice.Manage", gwc)
if err != nil {
logger.Error().Err(err).Msg("CheckPermission failed")
isAdminUser = false
}
}
}
@@ -1259,6 +1266,7 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
fileinfo.KeyFileVersionURL: createVersionsUrl(privateLinkURL),
fileinfo.KeyEnableOwnerTermination: true, // only for collabora
fileinfo.KeyEnableInsertRemoteImage: true,
fileinfo.KeySupportsExtendedLockLength: true,
fileinfo.KeySupportsGetLock: true,
fileinfo.KeySupportsLocks: true,
@@ -1267,6 +1275,7 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
fileinfo.KeySupportsRename: true,
fileinfo.KeyIsAnonymousUser: isAnonymousUser,
fileinfo.KeyIsAdminUser: isAdminUser,
fileinfo.KeyUserFriendlyName: userFriendlyName,
fileinfo.KeyUserID: userId,

View File

@@ -13,6 +13,7 @@ import (
auth "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
@@ -1671,6 +1672,13 @@ var _ = Describe("FileConnector", func() {
}
ctx = ctxpkg.ContextSetUser(ctx, u)
gatewayClient.On("CheckPermission", mock.Anything, mock.Anything).Return(
&permissions.CheckPermissionResponse{
Status: status.NewOK(ctx),
},
nil,
)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
Status: status.NewOK(ctx),
Info: &providerv1beta1.ResourceInfo{
@@ -1792,6 +1800,8 @@ var _ = Describe("FileConnector", func() {
UserCanRename: false,
BreadcrumbDocName: "test.txt",
PostMessageOrigin: "https://cloud.opencloud.test",
EnableInsertRemoteImage: true,
IsAnonymousUser: true,
}
response, err := fc.CheckFileInfo(ctx)
@@ -1889,7 +1899,7 @@ var _ = Describe("FileConnector", func() {
UserCanRename: false,
UserCanReview: false,
UserCanWrite: false,
EnableInsertRemoteImage: false,
EnableInsertRemoteImage: true,
UserID: "guest-zzz000",
UserFriendlyName: "guest zzz000",
FileSharingURL: "https://cloud.opencloud.test/f/storageid$spaceid%21opaqueid?details=sharing",
@@ -1928,6 +1938,13 @@ var _ = Describe("FileConnector", func() {
}
ctx = ctxpkg.ContextSetUser(ctx, u)
gatewayClient.On("CheckPermission", mock.Anything, mock.Anything).Return(
&permissions.CheckPermissionResponse{
Status: status.NewOK(ctx),
},
nil,
)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
Status: status.NewOK(ctx),
Info: &providerv1beta1.ResourceInfo{
@@ -1975,6 +1992,8 @@ var _ = Describe("FileConnector", func() {
UserCanRename: false,
BreadcrumbDocName: "test.txt",
PostMessageOrigin: "https://cloud.opencloud.test",
EnableInsertRemoteImage: true,
IsAdminUser: true,
}
response, err := fc.CheckFileInfo(ctx)
@@ -2001,6 +2020,13 @@ var _ = Describe("FileConnector", func() {
}
ctx = ctxpkg.ContextSetUser(ctx, u)
gatewayClient.On("CheckPermission", mock.Anything, mock.Anything).Return(
&permissions.CheckPermissionResponse{
Status: status.NewOK(ctx),
},
nil,
)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
Status: status.NewOK(ctx),
Info: &providerv1beta1.ResourceInfo{
@@ -2045,6 +2071,7 @@ var _ = Describe("FileConnector", func() {
FileVersionURL: "https://cloud.opencloud.test/f/storageid$spaceid%21opaqueid?details=versions",
HostEditURL: "https://cloud.opencloud.test/external-onlyoffice/path/to/test.txt?fileId=storageid%24spaceid%21opaqueid&view_mode=write",
PostMessageOrigin: "https://cloud.opencloud.test",
EnableInsertRemoteImage: true,
}
// change wopi app provider

View File

@@ -56,6 +56,10 @@ type Collabora struct {
SaveAsPostmessage bool `json:"SaveAsPostmessage,omitempty"`
// If set to true, it allows the document owner (the one with OwnerId =UserId) to send a closedocument message (see protocol.txt)
EnableOwnerTermination bool `json:"EnableOwnerTermination,omitempty"`
// If set to true, the user has administrator rights in the integration. Some functionality of Collabora Online, such as update check and server audit are supposed to be shown to administrators only.
IsAdminUser bool `json:"IsAdminUser"`
// If set to true, some functionality of Collabora which is supposed to be shown to authenticated users only is hidden
IsAnonymousUser bool `json:"IsAnonymousUser,omitempty"`
// JSON object that contains additional info about the user, namely the avatar image.
//UserExtraInfo -> requires definition, currently not used
@@ -131,6 +135,10 @@ func (cinfo *Collabora) SetProperties(props map[string]interface{}) {
//UserPrivateInfo -> requires definition, currently not used
case KeyWatermarkText:
cinfo.WatermarkText = value.(string)
case KeyIsAdminUser:
cinfo.IsAdminUser = value.(bool)
case KeyIsAnonymousUser:
cinfo.IsAnonymousUser = value.(bool)
case KeyEnableShare:
cinfo.EnableShare = value.(bool)

View File

@@ -50,6 +50,7 @@ const (
KeyIsAnonymousUser = "IsAnonymousUser"
KeyIsEduUser = "IsEduUser"
KeyIsAdminUser = "IsAdminUser"
KeyLicenseCheckForEditIsEnabled = "LicenseCheckForEditIsEnabled"
KeyUserFriendlyName = "UserFriendlyName"
KeyUserInfo = "UserInfo"

View File

@@ -8,10 +8,10 @@ import (
"github.com/go-ldap/ldap/v3"
"github.com/gofrs/uuid"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"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"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
type educationConfig struct {
@@ -119,16 +119,18 @@ func (i *LDAP) CreateEducationSchool(ctx context.Context, school libregraph.Educ
}
// Check that the school number is not already used
_, err := i.getSchoolByNumber(school.GetSchoolNumber())
switch err {
case nil:
logger.Debug().Err(errSchoolNumberExists).Str("schoolNumber", school.GetSchoolNumber()).Msg("duplicate school number")
return nil, errSchoolNumberExists
case ErrNotFound:
break
default:
logger.Error().Err(err).Str("schoolNumber", school.GetSchoolNumber()).Msg("error looking up school by number")
return nil, errorcode.New(errorcode.GeneralException, "error looking up school by number")
if school.HasSchoolNumber() {
_, err := i.getSchoolByNumber(school.GetSchoolNumber())
switch err {
case nil:
logger.Debug().Err(errSchoolNumberExists).Str("schoolNumber", school.GetSchoolNumber()).Msg("duplicate school number")
return nil, errSchoolNumberExists
case ErrNotFound:
break
default:
logger.Error().Err(err).Str("schoolNumber", school.GetSchoolNumber()).Msg("error looking up school by number")
return nil, errorcode.New(errorcode.GeneralException, "error looking up school by number")
}
}
attributeTypeAndValue := ldap.AttributeTypeAndValue{
@@ -142,7 +144,9 @@ func (i *LDAP) CreateEducationSchool(ctx context.Context, school libregraph.Educ
)
ar := ldap.NewAddRequest(dn, nil)
ar.Attribute(i.educationConfig.schoolAttributeMap.displayName, []string{school.GetDisplayName()})
ar.Attribute(i.educationConfig.schoolAttributeMap.schoolNumber, []string{school.GetSchoolNumber()})
if school.HasSchoolNumber() {
ar.Attribute(i.educationConfig.schoolAttributeMap.schoolNumber, []string{school.GetSchoolNumber()})
}
if !i.useServerUUID {
ar.Attribute(i.educationConfig.schoolAttributeMap.id, []string{uuid.Must(uuid.NewV4()).String()})
}
@@ -723,18 +727,22 @@ func (i *LDAP) createSchoolModelFromLDAP(e *ldap.Entry) *libregraph.EducationSch
if err != nil && !errors.Is(err, errNotSet) {
i.logger.Error().Err(err).Str("dn", e.DN).Msg("Error reading termination date for LDAP entry")
}
if id != "" && displayName != "" && schoolNumber != "" {
school := libregraph.NewEducationSchool()
school.SetDisplayName(displayName)
school.SetSchoolNumber(schoolNumber)
school.SetId(id)
if t != nil {
school.SetTerminationDate(*t)
}
return school
if id == "" || displayName == "" {
i.logger.Warn().Str("dn", e.DN).Str("id", id).Str("displayName", displayName).Msg("Invalid School. Missing required attribute")
return nil
}
i.logger.Warn().Str("dn", e.DN).Str("id", id).Str("displayName", displayName).Str("schoolNumber", schoolNumber).Msg("Invalid School. Missing required attribute")
return nil
school := libregraph.NewEducationSchool()
school.SetDisplayName(displayName)
school.SetId(id)
if schoolNumber != "" {
school.SetSchoolNumber(schoolNumber)
}
if t != nil {
school.SetTerminationDate(*t)
}
return school
}
func (i *LDAP) getSchoolNumber(e *ldap.Entry) string {

View File

@@ -83,9 +83,8 @@ func (i *LDAP) GetGroups(ctx context.Context, oreq *godata.GoDataRequest) ([]*li
if search != "" {
search = ldap.EscapeFilter(search)
groupFilter = fmt.Sprintf(
"(|(%s=*%s*)(%s=*%s*))",
"(%s=*%s*)",
i.groupAttributeMap.name, search,
i.groupAttributeMap.id, search,
)
}
groupFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.groupFilter, i.groupObjectClass, groupFilter)

View File

@@ -305,7 +305,7 @@ func TestGetGroupsSearch(t *testing.T) {
// only match if the filter contains the search term unquoted
lm.On("Search", mock.MatchedBy(
func(req *ldap.SearchRequest) bool {
return req.Filter == "(&(objectClass=groupOfNames)(|(cn=*term*)(entryUUID=*term*)))"
return req.Filter == "(&(objectClass=groupOfNames)(cn=*term*))"
})).
Return(&ldap.SearchResult{}, nil)
b, _ := getMockedBackend(lm, lconfig, &logger)

View File

@@ -0,0 +1,132 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jiri Grönroos <jiri.gronroos@iki.fi>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-11-06 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/opencloud-eu/teams/204053/fi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Voi muokata"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Voi muokata ilman versioita"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Voi hallita"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Voi lähettää"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Voi nähdä"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Voi nähdä (turvallinen)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Ei pääsyä"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Estä kaikki pääsy."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Täällä voit lisätä kuvauksen avaruudelle."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Näytä ja lataa."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr "Näytä vain asiakirjat, kuvat ja PDF:t. Vesileimat lisätään."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Näytä, lataa ja muokkaa."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Näytä, lataa ja näytä kaikki kutsutut henkilöt."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Näytä, lataa ja lähetä."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "Näytä, lataa, muokkaa ja näytä kaikki kaikki kutsutut henkilöt."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Näytä, lataa lähetä, muokkaa, lisää ja poista."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr "Näytä, lataa, lähetä, muokkaa, lisää, poista ja hallitse jäseniä."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Näytä, lataa, lähetä, muokkaa, lisää, poista ja näytä kaikki kutsutut "
"henkilöt."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr "Näytä, lataa, lähetä, muokkaa, lisää, poista mukaan lukien historia."

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-10-05 00:01+0000\n"
"POT-Creation-Date: 2025-10-26 00:00+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/opencloud-eu/teams/204053/nl/)\n"

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-10-12 00:00+0000\n"
"POT-Creation-Date: 2025-11-01 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Lulufox, 2025\n"
"Language-Team: Russian (https://app.transifex.com/opencloud-eu/teams/204053/ru/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-10-16 08:04+0000\n"
"POT-Creation-Date: 2025-11-06 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: LinkinWires <darkinsonic13@gmail.com>, 2025\n"
"Language-Team: Ukrainian (https://app.transifex.com/opencloud-eu/teams/204053/uk/)\n"

View File

@@ -74,12 +74,6 @@ func (g Graph) PostEducationSchool(w http.ResponseWriter, r *http.Request) {
return
}
if _, ok := school.GetSchoolNumberOk(); !ok {
logger.Debug().Interface("school", school).Msg("could not create school: missing required attribute")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Missing Required Attribute")
return
}
// validate terminationDate attribute, needs to be "far enough" in the future, terminationDate can be nil (means
// termination date is to be deleted
if terminationDate, ok := school.GetTerminationDateOk(); ok && terminationDate != nil {

View File

@@ -232,18 +232,6 @@ var _ = Describe("Schools", func() {
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles missing school number", func() {
newSchool = libregraph.NewEducationSchool()
newSchool.SetDisplayName("New School")
newSchoolJson, err := json.Marshal(newSchool)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBuffer(newSchoolJson))
svc.PostEducationSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("disallows school create ids", func() {
newSchool = libregraph.NewEducationSchool()
newSchool.SetId("disallowed")

View File

@@ -76,18 +76,7 @@ func (g Graph) PostEducationUser(w http.ResponseWriter, r *http.Request) {
return
}
identities, ok := u.GetIdentitiesOk()
if !ok {
logger.Debug().Err(err).Interface("user", u).Msg("could not create education user: missing required Collection: 'identities'")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing required Attribute: 'identities'")
return
}
if len(identities) < 1 {
logger.Debug().Err(err).Interface("user", u).Msg("could not create education user: missing entry in Collection: 'identities'")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing required Collection: 'identities'")
return
}
for i, identity := range identities {
for i, identity := range u.GetIdentities() {
if _, ok := identity.GetIssuerOk(); !ok {
logger.Debug().Err(err).Interface("user", u).Msgf("could not create education user: missing Attribute in 'identities' Collection Entry %d: 'issuer'", i)
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("missing Attribute in 'identities' Collection Entry %d: 'issuer'", i))
@@ -130,12 +119,6 @@ func (g Graph) PostEducationUser(w http.ResponseWriter, r *http.Request) {
u.SetUserType("Member")
}
if _, ok := u.GetPrimaryRoleOk(); !ok {
logger.Debug().Err(err).Interface("user", u).Msg("could not create education user: missing required Attribute: 'primaryRole'")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing required Attribute: 'primaryRole'")
return
}
logger.Debug().Interface("user", u).Msg("calling create education user on backend")
if u, err = g.identityEducationBackend.CreateEducationUser(r.Context(), *u); err != nil {
logger.Debug().Err(err).Msg("could not create education user: backend error")

View File

@@ -3,9 +3,12 @@ package channels
import (
"context"
"crypto/rand"
"crypto/tls"
"fmt"
stdmail "net/mail"
"strings"
"time"
"github.com/pkg/errors"
mail "github.com/xhit/go-simple-mail/v2"
@@ -118,6 +121,7 @@ func (m Mail) SendMessage(_ context.Context, message *Message) error {
email := mail.NewMSG()
email.SetFrom(appendSender(message.Sender, m.smtpAddress)).AddTo(message.Recipient...)
email.SetSubject(message.Subject)
email.AddHeader("Message-ID", generateMessageID(m.smtpAddress.Address))
email.SetBody(mail.TextPlain, message.TextBody)
if message.HTMLBody != "" {
email.AddAlternative(mail.TextHTML, message.HTMLBody)
@@ -135,3 +139,22 @@ func appendSender(sender string, a stdmail.Address) string {
}
return a.String()
}
// generateMessageID generates a unique Message-ID header value according to RFC 5322
func generateMessageID(domain string) string {
// Extract domain from email address if it contains @
if idx := strings.LastIndex(domain, "@"); idx != -1 {
domain = domain[idx+1:]
}
// Generate random bytes for uniqueness
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
// Fallback to timestamp-based ID if random fails
return fmt.Sprintf("<%d@%s>", time.Now().UnixNano(), domain)
}
// Create Message-ID: <timestamp.random@domain>
timestamp := time.Now().Unix()
return fmt.Sprintf("<%d.%x@%s>", timestamp, b, domain)
}

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-10-13 00:01+0000\n"
"POT-Creation-Date: 2025-11-02 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: miguel tapias, 2025\n"
"Language-Team: Spanish (https://app.transifex.com/opencloud-eu/teams/204053/es/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-10-05 00:01+0000\n"
"POT-Creation-Date: 2025-10-26 00:00+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/opencloud-eu/teams/204053/nl/)\n"

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-10-07 00:00+0000\n"
"POT-Creation-Date: 2025-10-27 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Lulufox, 2025\n"
"Language-Team: Russian (https://app.transifex.com/opencloud-eu/teams/204053/ru/)\n"

View File

@@ -95,7 +95,7 @@ func Server(cfg *config.Config) *cli.Command {
ocdav.WithTraceProvider(traceProvider),
ocdav.RegisterTTL(registry.GetRegisterTTL()),
ocdav.RegisterInterval(registry.GetRegisterInterval()),
ocdav.URLSigningSharedSecret(cfg.URLSigningSharedSecret),
ocdav.URLSigningSharedSecret(cfg.Commons.URLSigningSecret),
}
s, err := ocdav.Service(opts...)

View File

@@ -34,9 +34,8 @@ type Config struct {
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;OCDAV_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"1.0.0"`
URLSigningSharedSecret string `yaml:"url_signing_shared_secret" env:"OC_URL_SIGNING_SHARED_SECRET" desc:"The shared secret used to sign URLs." introductionVersion:"4.0.0"`
Context context.Context `yaml:"-"`
Status Status `yaml:"-"`
Context context.Context `yaml:"-"`
Status Status `yaml:"-"`
AllowPropfindDepthInfinity bool `yaml:"allow_propfind_depth_infinity" env:"OCDAV_ALLOW_PROPFIND_DEPTH_INFINITY" desc:"Allow the use of depth infinity in PROPFINDS. When enabled, a propfind will traverse through all subfolders. If many subfolders are expected, depth infinity can cause heavy server load and/or delayed response times." introductionVersion:"1.0.0"`
}

View File

@@ -37,9 +37,14 @@ func Validate(cfg *config.Config) error {
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
if cfg.MachineAuthAPIKey == "" {
return shared.MissingMachineAuthApiKeyError(cfg.Service.Name)
}
if cfg.Commons.URLSigningSecret == "" {
return shared.MissingURLSigningSecret(cfg.Service.Name)
}
return nil
}

View File

@@ -311,15 +311,11 @@ func loadMiddlewares(logger log.Logger, cfg *config.Config,
RevaGatewaySelector: gatewaySelector,
})
var signURLVerifier signedurl.Verifier
if cfg.PreSignedURL.JWTSigningSharedSecret != "" {
var err error
signURLVerifier, err = signedurl.NewJWTSignedURL(signedurl.WithSecret(cfg.PreSignedURL.JWTSigningSharedSecret))
if err != nil {
logger.Fatal().Err(err).Msg("Failed to initialize signed URL configuration.")
}
signURLVerifier, err := signedurl.NewJWTSignedURL(signedurl.WithSecret(cfg.Commons.URLSigningSecret))
if err != nil {
logger.Fatal().Err(err).Msg("Failed to initialize signed URL configuration.")
}
authenticators = append(authenticators, middleware.SignedURLAuthenticator{
Logger: logger,
PreSignedURLConfig: cfg.PreSignedURL,

View File

@@ -180,10 +180,9 @@ type StaticSelectorConf struct {
// PreSignedURL is the config for the pre-signed url middleware
type PreSignedURL struct {
AllowedHTTPMethods []string `yaml:"allowed_http_methods"`
Enabled bool `yaml:"enabled" env:"PROXY_ENABLE_PRESIGNEDURLS" desc:"Allow OCS to get a signing key to sign requests." introductionVersion:"1.0.0"`
SigningKeys *SigningKeys `yaml:"signing_keys"`
JWTSigningSharedSecret string `yaml:"url_signing_shared_secret" env:"OC_URL_SIGNING_SHARED_SECRET" desc:"The shared secret used to sign URLs." introductionVersion:"4.0.0"`
AllowedHTTPMethods []string `yaml:"allowed_http_methods"`
Enabled bool `yaml:"enabled" env:"PROXY_ENABLE_PRESIGNEDURLS" desc:"Allow OCS to get a signing key to sign requests." introductionVersion:"1.0.0"`
SigningKeys *SigningKeys `yaml:"signing_keys"`
}
// SigningKeys is a store configuration.

View File

@@ -56,9 +56,14 @@ func Validate(cfg *config.Config) error {
if cfg.ServiceAccount.ServiceAccountID == "" {
return shared.MissingServiceAccountID(cfg.Service.Name)
}
if cfg.ServiceAccount.ServiceAccountSecret == "" {
return shared.MissingServiceAccountSecret(cfg.Service.Name)
}
if cfg.Commons.URLSigningSecret == "" {
return shared.MissingURLSigningSecret(cfg.Service.Name)
}
return nil
}

View File

@@ -0,0 +1,144 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jiri Grönroos <jiri.gronroos@iki.fi>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-11-06 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/opencloud-eu/teams/204053/fi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Lisätty avaruuden jäseneksi"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Päivittäin"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Sähköpostin lähetyksen aikaväli"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Tiedosto hylätty"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Välittömästi"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Ei koskaan"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Ilmoita kun minut lisätty jäseneksi avaruuteen"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Ilmoita kun jäsenyyteni avaruudesta on poistettu"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Ilmoita kun olen vastaanottanut jaon"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Ilmoita kun vastaanotettu jako on poistettu"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Ilmoita kun vastaanotettu jako on vanhentunut"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr ""
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr ""
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr ""
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valittu arvo:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Jako vanheni"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Jako vastaanotettu"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Jako poistettu"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Avaruus poistettu"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Avaruus poistettu käytöstä"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Avaruuden jäsenyys vanhentunut"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Viikottain"

View File

@@ -0,0 +1,150 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# idoet <idoet@protonmail.ch>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-10-29 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: idoet <idoet@protonmail.ch>, 2025\n"
"Language-Team: Indonesian (https://app.transifex.com/opencloud-eu/teams/204053/id/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: id\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Ditambahkan menjadi anggota ruang penyimpanan"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Harian"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Interval pengiriman email"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Berkas ditolak"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instan"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Tidak pernah"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr ""
"Beri tahu ketika saya telah ditambahkan menjadi anggota dari suatu ruang "
"penyimpanan"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr ""
"Beri tahu ketika saya telah dihapus dari keanggotaan suatu ruang penyimpanan"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Beri tahu ketika saya telah menerima berbagi"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Beri tahu jika file yang saya unggah ditolak karena infeksi virus atau "
"pelanggaran kebijakan"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Beri tahu saat berbagi yang diterima telah dihapus"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Beri tahu ketika berbagi yang diterima telah berakhir"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Beri tahu ketika ruang penyimpanan yang saya ikuti telah dihapus"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Beri tahu ketika ruang penyimpanan yang saya ikuti telah dinonaktifkan"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Beri tahu saat keanggotaan ruang penyimpanan telah berakhir"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Dihapus dari anggota ruang penyimpanan"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Yang dipilih:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Berbagi Berakhir"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Berbagi Diterima"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Berbagi Dihapus"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Ruang penyimpanan dihapus"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Ruang penyimpanan dinonaktifkan"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Keanggotaan ruang penyimpanan telah berakhir"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Mingguan"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-10-16 08:04+0000\n"
"POT-Creation-Date: 2025-11-06 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: LinkinWires <darkinsonic13@gmail.com>, 2025\n"
"Language-Team: Ukrainian (https://app.transifex.com/opencloud-eu/teams/204053/uk/)\n"

View File

@@ -140,6 +140,7 @@ func generateBundleAdminRole() *settingsmsg.Bundle {
SetProjectSpaceQuotaPermission(All),
SettingsManagementPermission(All),
SpaceAbilityPermission(All),
WebOfficeManagementPermssion(All),
WriteFavoritesPermission(Own),
},
}
@@ -659,9 +660,9 @@ func DefaultRoleAssignments(cfg *config.Config) []*settingsmsg.UserRoleAssignmen
RoleId: BundleUUIDRoleUser,
},
{
AccountUuid: "60708dda-e897-11ef-919f-bbb7437d6ec2",
RoleId: BundleUUIDRoleUser,
},
AccountUuid: "60708dda-e897-11ef-919f-bbb7437d6ec2",
RoleId: BundleUUIDRoleUser,
},
{
// additional admin user
AccountUuid: "cd88bf9a-dd7f-11ef-a609-7f78deb2345f", // demo user "dennis"

View File

@@ -621,3 +621,22 @@ func WriteFavoritesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.
},
}
}
// WebOfficManagementPermssion is the permission to mark/unmark files as favorites
func WebOfficeManagementPermssion(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "27a29046-a816-424f-bd71-2ffb9029162f",
Name: "WebOffice.Manage",
DisplayName: "Manage WebOffice",
Description: "This permission gives access to the admin features in the WebOffice suite.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}

View File

@@ -0,0 +1,117 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jiri Grönroos <jiri.gronroos@iki.fi>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-11-06 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/opencloud-eu/teams/204053/fi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: pkg/service/templates.go:39
msgid "Access to Space {space} lost"
msgstr "Pääsy avaruuteen {space} menetetty"
#: pkg/service/templates.go:54
msgid "Access to {resource} expired"
msgstr "Pääsy resurssiin {resource} vanheni"
#: pkg/service/templates.go:59
msgid ""
"Attention! The instance will be shut down and deprovisioned on {date}. "
"Download all your data before that date as no access past that date is "
"possible."
msgstr ""
"Huomio! Tämä instanssi sammutetaan ja poistetaan {date}. Lataa datasi ennen "
"kyseistä päivää, sillä pääsy sen jälkeen ei ole mahdollista."
#: pkg/service/templates.go:14
msgid "File {resource} was deleted because it violates the policies"
msgstr "Tiedosto {resource} poistettiin, koska loukkaa käytäntöjä"
#: pkg/service/templates.go:58
msgid "Instance will be shut down and deprovisioned"
msgstr "Instanssi sammutetaan ja poistetaan käytöstä"
#: pkg/service/templates.go:38
msgid "Membership expired"
msgstr "Jäsenyys vanhentunut"
#: pkg/service/templates.go:13
msgid "Policies enforced"
msgstr "Käytännöt pakotettu"
#: pkg/service/templates.go:23
msgid "Removed from Space"
msgstr "Poistettu avaruudesta"
#: pkg/service/templates.go:43
msgid "Resource shared"
msgstr "Resurssi jaettu"
#: pkg/service/templates.go:48
msgid "Resource unshared"
msgstr "Resurssin jako lopetettu"
#: pkg/service/templates.go:53
msgid "Share expired"
msgstr "Jako vanhentunut"
#: pkg/service/templates.go:33
msgid "Space deleted"
msgstr "Jako poistettu"
#: pkg/service/templates.go:28
msgid "Space disabled"
msgstr "Avaruus poistettu käytöstä"
#: pkg/service/templates.go:18
msgid "Space shared"
msgstr "Avaruus jaettu"
#: pkg/service/templates.go:8
msgid "Virus found"
msgstr "Virus löydetty"
#: pkg/service/templates.go:9
msgid "Virus found in {resource}. Upload not possible. Virus: {virus}"
msgstr ""
"Virus paikannettu resurssiin {resource}. Lähetys ei ole mahdollista. Virus: "
"{virus}"
#: pkg/service/templates.go:19
msgid "{user} added you to Space {space}"
msgstr "{user} lisäsi sinut avaruuteen {space}"
#: pkg/service/templates.go:34
msgid "{user} deleted Space {space}"
msgstr "{user} poisti avaruuden {space}"
#: pkg/service/templates.go:29
msgid "{user} disabled Space {space}"
msgstr "{user} poisti käytöstä avaruuden {space}"
#: pkg/service/templates.go:24
msgid "{user} removed you from Space {space}"
msgstr "{user} poisti sinut avaruudesta {space}"
#: pkg/service/templates.go:44
msgid "{user} shared {resource} with you"
msgstr "{user} jakoi {resource} kanssasi"
#: pkg/service/templates.go:49
msgid "{user} unshared {resource} with you"
msgstr "{user} lopettu resurssin {resource} jakamisen kanssasi"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-10-05 00:01+0000\n"
"POT-Creation-Date: 2025-10-26 00:00+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/opencloud-eu/teams/204053/nl/)\n"

View File

@@ -1,6 +1,6 @@
SHELL := bash
NAME := web
WEB_ASSETS_VERSION = v4.1.0
WEB_ASSETS_VERSION = v4.2.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

@@ -197,34 +197,6 @@ class GraphHelper {
return $baseUrl . '/graph/v1beta1/' . $path;
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $method
* @param string $path
* @param string|null $body
* @param array|null $headers
*
* @return RequestInterface
*/
public static function createRequest(
string $baseUrl,
string $xRequestId,
string $method,
string $path,
?string $body = null,
?array $headers = []
): RequestInterface {
$fullUrl = self::getFullUrl($baseUrl, $path);
return HttpRequestHelper::createRequest(
$fullUrl,
$xRequestId,
$method,
$headers,
$body
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
@@ -1908,7 +1880,7 @@ class GraphHelper {
string $permissionsId
): ResponseInterface {
$url = self::getBetaFullUrl($baseUrl, "drives/$spaceId/items/$itemId/permissions/$permissionsId");
return HttpRequestHelper::sendRequestOnce(
return HttpRequestHelper::sendRequest(
$url,
$xRequestId,
'PATCH',
@@ -2264,7 +2236,7 @@ class GraphHelper {
): ResponseInterface {
$url = self::getBetaFullUrl($baseUrl, "drives/$spaceId/root/permissions/$permissionsId");
return HttpRequestHelper::sendRequestOnce(
return HttpRequestHelper::sendRequest(
$url,
$xRequestId,
'PATCH',

View File

@@ -74,6 +74,7 @@ class HttpRequestHelper {
* than download it all up-front.
* @param int|null $timeout
* @param Client|null $client
* @param string|null $bearerToken
*
* @return ResponseInterface
* @throws GuzzleException
@@ -90,7 +91,8 @@ class HttpRequestHelper {
?CookieJar $cookies = null,
bool $stream = false,
?int $timeout = 0,
?Client $client = null
?Client $client = null,
?string $bearerToken = null
): ResponseInterface {
if ($client === null) {
$client = self::createClient(
@@ -99,7 +101,8 @@ class HttpRequestHelper {
$config,
$cookies,
$stream,
$timeout
$timeout,
$bearerToken
);
}
@@ -200,6 +203,13 @@ class HttpRequestHelper {
} else {
$debugResponses = false;
}
// use basic auth for 'public' user or no user
if ($user === 'public' || $user === null || $user === '') {
$bearerToken = null;
} else {
$useBearerToken = TokenHelper::useBearerToken();
$bearerToken = $useBearerToken ? TokenHelper::getTokens($user, $password, $url)['access_token'] : null;
}
$sendRetryLimit = self::numRetriesOnHttpTooEarly();
$sendCount = 0;
@@ -217,7 +227,8 @@ class HttpRequestHelper {
$cookies,
$stream,
$timeout,
$client
$client,
$bearerToken,
);
if ($response->getStatusCode() >= 400
@@ -348,6 +359,7 @@ class HttpRequestHelper {
* @param bool $stream Set to true to stream a response rather
* than download it all up-front.
* @param int|null $timeout
* @param string|null $bearerToken
*
* @return Client
*/
@@ -357,10 +369,13 @@ class HttpRequestHelper {
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false,
?int $timeout = 0
?int $timeout = 0,
?string $bearerToken = null
): Client {
$options = [];
if ($user !== null) {
if ($bearerToken !== null) {
$options['headers']['Authorization'] = 'Bearer ' . $bearerToken;
} elseif ($user !== null) {
$options['auth'] = [$user, $password];
}
if ($config !== null) {

View File

@@ -0,0 +1,403 @@
<?php
/**
* @author Viktor Scharf <v.scharf@opencloud.eu>
* @copyright Copyright (c) 2025 Viktor Scharf <v.scharf@opencloud.eu>
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\GuzzleException;
use Exception;
/**
* Helper for obtaining bearer tokens for users
*/
class TokenHelper {
private const LOGON_URL = '/signin/v1/identifier/_/logon';
private const REDIRECT_URL = '/oidc-callback.html';
private const TOKEN_URL = '/konnect/v1/token';
// Static cache [username => token_data]
private static array $tokenCache = [];
/**
* @return bool
*/
public static function useBearerToken(): bool {
return \getenv('USE_BEARER_TOKEN') === 'true';
}
/**
* Extracts base URL from a full URL
*
* @param string $url
*
* @return string the base URL
*/
private static function extractBaseUrl(string $url): string {
return preg_replace('#(https?://[^/]+).*#', '$1', $url);
}
/**
* Get access and refresh tokens for a user
* Uses cache to avoid unnecessary requests
*
* @param string $username
* @param string $password
* @param string $url
*
* @return array ['access_token' => string, 'refresh_token' => string, 'expires_at' => int]
* @throws GuzzleException
* @throws Exception
*/
public static function getTokens(string $username, string $password, string $url): array {
// Extract base URL. I need to send $url to get correct server in case of multiple servers (ocm suite)
$baseUrl = self::extractBaseUrl($url);
$cacheKey = $username . '|' . $baseUrl;
// Check cache
if (isset(self::$tokenCache[$cacheKey])) {
$cachedToken = self::$tokenCache[$cacheKey];
// Check if access token has expired
if (time() < $cachedToken['expires_at']) {
return $cachedToken;
}
$refreshedToken = self::refreshToken($cachedToken['refresh_token'], $baseUrl);
$tokenData = [
'access_token' => $refreshedToken['access_token'],
'refresh_token' => $refreshedToken['refresh_token'],
'expires_at' => time() + 300 // 5 minutes
];
self::$tokenCache[$cacheKey] = $tokenData;
return $tokenData;
}
// Get new tokens
$cookieJar = new CookieJar();
$continueUrl = self::getAuthorizedEndPoint($username, $password, $baseUrl, $cookieJar);
$code = self::getCode($continueUrl, $baseUrl, $cookieJar);
$tokens = self::getToken($code, $baseUrl, $cookieJar);
$tokenData = [
'access_token' => $tokens['access_token'],
'refresh_token' => $tokens['refresh_token'],
'expires_at' => time() + 290 // set expiry to 290 seconds to allow for some buffer
];
// Save to cache
self::$tokenCache[$cacheKey] = $tokenData;
return $tokenData;
}
/**
* Refresh token
*
* @param string $refreshToken
* @param string $baseUrl
*
* @return array
* @throws GuzzleException
* @throws Exception
*/
private static function refreshToken(string $refreshToken, string $baseUrl): array {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false
]
);
$response = $client->post(
$baseUrl . self::TOKEN_URL,
[
'form_params' => [
'client_id' => 'web',
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
]
]
);
if ($response->getStatusCode() !== 200) {
throw new Exception(
\sprintf(
'Token refresh failed: Expected status code 200 but received %d. Message: %s',
$response->getStatusCode(),
$response->getReasonPhrase()
)
);
}
$data = json_decode($response->getBody()->getContents(), true);
if (!isset($data['access_token']) || !isset($data['refresh_token'])) {
throw new Exception('Missing tokens in refresh response');
}
return [
'access_token' => $data['access_token'],
'refresh_token' => $data['refresh_token']
];
}
/**
* Clear cached tokens for a specific user
*
* @param string $username
* @param string $url
*
* @return void
*/
public static function clearUserTokens(string $username, string $url): void {
$baseUrl = self::extractBaseUrl($url);
$cacheKey = $username . '|' . $baseUrl;
unset(self::$tokenCache[$cacheKey]);
}
/**
* Clear all cached tokens
*
* @return void
*/
public static function clearAllTokens(): void {
self::$tokenCache = [];
}
/**
* @param string $username
* @param string $password
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return \Psr\Http\Message\ResponseInterface
* @throws GuzzleException
*/
public static function makeLoginRequest(
string $username,
string $password,
string $baseUrl,
CookieJar $cookieJar
): \Psr\Http\Message\ResponseInterface {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false,
'cookies' => $cookieJar
]
);
return $client->post(
$baseUrl . self::LOGON_URL,
[
'headers' => [
'Kopano-Konnect-XSRF' => '1',
'Referer' => $baseUrl,
'Content-Type' => 'application/json'
],
'json' => [
'params' => [$username, $password, '1'],
'hello' => [
'scope' => 'openid profile offline_access email',
'client_id' => 'web',
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
'flow' => 'oidc'
]
]
]
);
}
/**
* Step 1: Login and get continue_uri
*
* @param string $username
* @param string $password
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
private static function getAuthorizedEndPoint(
string $username,
string $password,
string $baseUrl,
CookieJar $cookieJar
): string {
$response = self::makeLoginRequest($username, $password, $baseUrl, $cookieJar);
if ($response->getStatusCode() !== 200) {
throw new Exception(
\sprintf(
'Logon failed: Expected status code 200 but received %d. Message: %s',
$response->getStatusCode(),
$response->getReasonPhrase()
)
);
}
$data = json_decode($response->getBody()->getContents(), true);
if (!isset($data['hello']['continue_uri'])) {
throw new Exception('Missing continue_uri in logon response');
}
return $data['hello']['continue_uri'];
}
/**
* Step 2: Authorization and get code
*
* @param string $continueUrl
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
private static function getCode(string $continueUrl, string $baseUrl, CookieJar $cookieJar): string {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false, // Disable automatic redirects
'cookies' => $cookieJar
]
);
$params = [
'client_id' => 'web',
'prompt' => 'none',
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
'response_mode' => 'query',
'response_type' => 'code',
'scope' => 'openid profile offline_access email'
];
$response = $client->get(
$continueUrl,
[
'query' => $params
]
);
if ($response->getStatusCode() !== 302) {
// Add debugging to understand what is happening
$body = $response->getBody()->getContents();
throw new Exception(
\sprintf(
'Authorization failed: Expected status code 302 but received %d. Message: %s. Body: %s',
$response->getStatusCode(),
$response->getReasonPhrase(),
$body
)
);
}
$location = $response->getHeader('Location')[0] ?? '';
if (empty($location)) {
throw new Exception('Missing Location header in authorization response');
}
parse_str(parse_url($location, PHP_URL_QUERY), $queryParams);
// Check for errors
if (isset($queryParams['error'])) {
throw new Exception(
\sprintf(
'Authorization error: %s - %s',
$queryParams['error'],
urldecode($queryParams['error_description'] ?? 'No description')
)
);
}
if (!isset($queryParams['code'])) {
throw new Exception('Missing auth code in redirect URL. Location: ' . $location);
}
return $queryParams['code'];
}
/**
* Step 3: Get token
*
* @param string $code
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return array
*
* @throws GuzzleException
* @throws Exception
*
*/
private static function getToken(string $code, string $baseUrl, CookieJar $cookieJar): array {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false,
'cookies' => $cookieJar
]
);
$response = $client->post(
$baseUrl . self::TOKEN_URL,
[
'form_params' => [
'client_id' => 'web',
'code' => $code,
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
'grant_type' => 'authorization_code'
]
]
);
if ($response->getStatusCode() !== 200) {
throw new Exception(
\sprintf(
'Token request failed: Expected status code 200 but received %d. Message: %s',
$response->getStatusCode(),
$response->getReasonPhrase()
)
);
}
$data = json_decode($response->getBody()->getContents(), true);
if (!isset($data['access_token']) || !isset($data['refresh_token'])) {
throw new Exception('Missing tokens in response');
}
return [
'access_token' => $data['access_token'],
'refresh_token' => $data['refresh_token']
];
}
}

View File

@@ -25,6 +25,7 @@ use Behat\Behat\Context\Context;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\HttpRequestHelper;
use TestHelpers\BehatHelper;
use TestHelpers\TokenHelper;
use TestHelpers\WebDavHelper;
/**
@@ -714,4 +715,27 @@ class AuthContext implements Context {
);
$this->featureContext->setResponse($response);
}
/**
* @When user :user should not be able to log in with wrong password :password
*
* @param string $user
* @param string $password
*
* @return void
*/
public function userShouldNotBeAbleToLogInWithWrongPassword(
string $user,
string $password
): void {
TokenHelper::clearUserTokens($user, $this->featureContext->getBaseUrl());
$response = TokenHelper::makeLoginRequest(
$user,
$password,
$this->featureContext->getBaseUrl(),
new \GuzzleHttp\Cookie\CookieJar()
);
// why is not 401 returned?
$this->featureContext->theHTTPStatusCodeShouldBe(204, 'should not be able to log in', $response);
}
}

View File

@@ -696,6 +696,10 @@ class CliContext implements Context {
* @return void
*/
public function theAdministratorReadsTheFileContent(string $user, string $file): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [
@@ -715,6 +719,10 @@ class CliContext implements Context {
* @return void
*/
public function theAdministratorCopiesFileToFolder(string $user, string $file, string $folder): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
@@ -739,6 +747,10 @@ class CliContext implements Context {
* @return void
*/
public function theAdministratorRenamesFile(string $user, string $file, string $newName): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
@@ -763,6 +775,10 @@ class CliContext implements Context {
* @return void
*/
public function theAdministratorMovesFileToFolder(string $user, string $file, string $folder): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
@@ -827,6 +843,10 @@ class CliContext implements Context {
* @return void
*/
public function theAdministratorCopiesFileToSpace(string $user, string $file, string $space): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$usersStoragePath = $this->getUsersStoragePath();
$projectsStoragePath = $this->getProjectsStoragePath();
@@ -874,6 +894,10 @@ class CliContext implements Context {
* @return void
*/
public function theAdminChecksTheAttributeOfFileForUser(string $attribute, string $file, string $user): void {
// this downloads the file using WebDAV and by that checks if it's still in
// postprocessing. So its effectively a check for finished postprocessing
$this->featureContext->userDownloadsFileUsingTheAPI($user, $file);
$userUuid = $this->featureContext->getAttributeOfCreatedUser($user, 'id');
$storagePath = $this->getUsersStoragePath();
$body = [

View File

@@ -17,6 +17,7 @@ use TestHelpers\GraphHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\HttpRequestHelper;
use TestHelpers\BehatHelper;
use TestHelpers\TokenHelper;
require_once 'bootstrap.php';
@@ -2864,6 +2865,7 @@ class GraphContext implements Context {
);
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
$this->featureContext->updateUsernameInCreatedUserList($byUser, $userName);
TokenHelper::clearUserTokens($byUser, $this->featureContext->getBaseUrl());
}
/**

View File

@@ -31,6 +31,7 @@ use TestHelpers\WebDavHelper;
use TestHelpers\GraphHelper;
use Laminas\Ldap\Exception\LdapException;
use Laminas\Ldap\Ldap;
use TestHelpers\TokenHelper;
/**
* Functions for provisioning of users and groups
@@ -558,110 +559,65 @@ trait Provisioning {
*/
public function usersHaveBeenCreated(
TableNode $table,
bool $useDefault=true,
bool $initialize=true
bool $useDefault = true,
bool $initialize = true
) {
$this->verifyTableNodeColumns($table, ['username'], ['displayname', 'email', 'password']);
$table = $table->getColumnsHash();
$users = $this->buildUsersAttributesArray($useDefault, $table);
$requests = [];
$client = HttpRequestHelper::createClient(
$this->getAdminUsername(),
$this->getAdminPassword()
);
foreach ($users as $userAttributes) {
$userName = $userAttributes['userid'];
$password = $userAttributes['password'];
$displayName = $userAttributes['displayName'];
$email = $userAttributes['email'];
if ($this->isTestingWithLdap()) {
$this->createLdapUser($userAttributes);
} else {
$attributesToCreateUser['userid'] = $userAttributes['userid'];
$attributesToCreateUser['password'] = $userAttributes['password'];
$attributesToCreateUser['displayname'] = $userAttributes['displayName'];
if ($userAttributes['email'] === null) {
Assert::assertArrayHasKey(
'userid',
$userAttributes,
__METHOD__ . " userAttributes array does not have key 'userid'"
try {
$this->createLdapUser($userAttributes);
} catch (LdapException $exception) {
throw new Exception(
__METHOD__ . " cannot create a LDAP user with provided data. Error: $exception"
);
$attributesToCreateUser['email'] = $userAttributes['userid'] . '@opencloud.eu';
} else {
$attributesToCreateUser['email'] = $userAttributes['email'];
}
$body = GraphHelper::prepareCreateUserPayload(
$attributesToCreateUser['userid'],
$attributesToCreateUser['password'],
$attributesToCreateUser['email'],
$attributesToCreateUser['displayname']
);
$request = GraphHelper::createRequest(
} else {
// Use the same logic as userHasBeenCreated for email generation
if ($email === null) {
$email = $this->getEmailAddressForUser($userName);
if ($email === null) {
// escape @ & space if present in userId
$email = \str_replace(["@", " "], "", $userName) . '@opencloud.eu';
}
}
$userName = $this->getActualUsername($userName);
$userName = \trim($userName);
$response = GraphHelper::createUser(
$this->getBaseUrl(),
$this->getStepLineRef(),
"POST",
'users',
$body,
$this->getAdminUsername(),
$this->getAdminPassword(),
$userName,
$password,
$email,
$displayName,
);
// Add the request to the $requests array so that they can be sent in parallel.
$requests[] = $request;
Assert::assertEquals(
201,
$response->getStatusCode(),
__METHOD__ . " cannot create user '$userName' using Graph API.\nResponse:" .
json_encode($this->getJsonDecodedResponse($response))
);
$userId = $this->getJsonDecodedResponse($response)['id'];
}
}
$exceptionToThrow = null;
if (!$this->isTestingWithLdap()) {
$results = HttpRequestHelper::sendBatchRequest($requests, $client);
// Check all requests to inspect failures.
foreach ($results as $key => $e) {
if ($e instanceof ClientException) {
$responseBody = $this->getJsonDecodedResponse($e->getResponse());
$httpStatusCode = $e->getResponse()->getStatusCode();
$graphStatusCode = $responseBody['error']['code'];
$messageText = $responseBody['error']['message'];
$exceptionToThrow = new Exception(
__METHOD__ .
" Unexpected failure when creating the user '" .
$users[$key]['userid'] . "'" .
"\nHTTP status $httpStatusCode " .
"\nGraph status $graphStatusCode " .
"\nError message $messageText"
);
}
}
}
$this->addUserToCreatedUsersList($userName, $password, $displayName, $email, $userId ?? null);
// Create requests for setting displayname and email for the newly created users.
// These values cannot be set while creating the user, so we have to edit the newly created user to set these values.
foreach ($users as $userAttributes) {
if (!$this->isTestingWithLdap()) {
// for graph api, we need to save the user id to be able to add it in some group
// can be fetched with the "onPremisesSamAccountName" i.e. userid
$response = $this->graphContext->adminHasRetrievedUserUsingTheGraphApi($userAttributes['userid']);
$userAttributes['id'] = $this->getJsonDecodedResponse($response)['id'];
} else {
$userAttributes['id'] = null;
}
$this->addUserToCreatedUsersList(
$userAttributes['userid'],
$userAttributes['password'],
$userAttributes['displayName'],
$userAttributes['email'],
$userAttributes['id']
);
}
if (isset($exceptionToThrow)) {
throw $exceptionToThrow;
}
foreach ($users as $user) {
Assert::assertTrue(
$this->userExists($user["userid"]),
"User '" . $user["userid"] . "' should exist but does not exist"
);
}
if ($initialize) {
foreach ($users as $user) {
$this->initializeUser($user['userid'], $user['password']);
if ($initialize) {
$this->initializeUser($userName, $password);
}
}
}
@@ -841,45 +797,16 @@ trait Provisioning {
*/
public function userHasBeenDeleted(string $user): void {
$user = $this->getActualUsername($user);
if ($this->userExists($user)) {
if ($this->isTestingWithLdap() && \in_array($user, $this->ldapCreatedUsers)) {
$this->deleteLdapUser($user);
} else {
$response = $this->deleteUser($user);
$this->theHTTPStatusCodeShouldBe(204, "", $response);
WebDavHelper::removeSpaceIdReferenceForUser($user);
}
if ($this->isTestingWithLdap() && \in_array($user, $this->ldapCreatedUsers)) {
$this->deleteLdapUser($user);
} else {
$response = $this->deleteUser($user);
$this->theHTTPStatusCodeShouldBe(204, "", $response);
WebDavHelper::removeSpaceIdReferenceForUser($user);
}
Assert::assertFalse(
$this->userExists($user),
"User '$user' should not exist but does exist"
);
$this->rememberThatUserIsNotExpectedToExist($user);
}
/**
* @Given these users have been initialized:
* expects a table of users with the heading
* "|username|password|"
*
* @param TableNode $table
*
* @return void
*/
public function theseUsersHaveBeenInitialized(TableNode $table): void {
foreach ($table as $row) {
if (!isset($row ['password'])) {
$password = $this->getPasswordForUser($row ['username']);
} else {
$password = $row ['password'];
}
$this->initializeUser(
$row ['username'],
$password
);
}
}
/**
* get all the existing groups
*
@@ -961,13 +888,14 @@ trait Provisioning {
$url = $this->getBaseUrl()
. "/ocs/v$this->ocsApiVersion.php/cloud/users/$user";
}
HttpRequestHelper::get(
$url,
$this->getStepLineRef(),
$user,
$password
);
if ($password !== '') {
HttpRequestHelper::get(
$url,
$this->getStepLineRef(),
$user,
$password
);
}
}
/**
@@ -1162,12 +1090,6 @@ trait Provisioning {
}
$this->addUserToCreatedUsersList($user, $password, $displayName, $email, $userId);
Assert::assertTrue(
$this->userExists($user),
"User '$user' should exist but does not exist"
);
$this->initializeUser($user, $password);
}
@@ -1999,21 +1921,15 @@ trait Provisioning {
$this->usingServer('LOCAL');
foreach ($this->createdUsers as $userData) {
$user = $userData['actualUsername'];
TokenHelper::clearUserTokens($user, $this->getBaseUrl());
$this->deleteUser($user);
Assert::assertFalse(
$this->userExists($user),
"User '$user' should not exist but does exist"
);
$this->rememberThatUserIsNotExpectedToExist($user);
}
$this->usingServer('REMOTE');
foreach ($this->createdRemoteUsers as $userData) {
$user = $userData['actualUsername'];
TokenHelper::clearUserTokens($user, $this->getBaseUrl());
$this->deleteUser($user);
Assert::assertFalse(
$this->userExists($user),
"User '$user' should not exist but does exist"
);
$this->rememberThatUserIsNotExpectedToExist($user);
}
$this->usingServer($previousServer);

View File

@@ -741,6 +741,17 @@ trait WebDav {
$this->setResponse($this->downloadFileWithRange($user, $fileSource, $range));
}
/**
* @When the user waits for :time seconds for postprocessing to finish
*
* @param int $time
*
* @return void
*/
public function waitForCertainSeconds(int $time): void {
\sleep($time);
}
/**
* @Then /^user "([^"]*)" using password "([^"]*)" should not be able to download file "([^"]*)"$/
*

View File

@@ -116,8 +116,8 @@ _ocdav: api compatibility, return correct status code_
- [coreApiWebdavUploadTUS/uploadToShare.feature:256](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L256)
- [coreApiWebdavUploadTUS/uploadToShare.feature:279](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L279)
- [coreApiWebdavUploadTUS/uploadToShare.feature:280](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L280)
- [coreApiWebdavUploadTUS/uploadToShare.feature:375](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L375)
- [coreApiWebdavUploadTUS/uploadToShare.feature:376](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L376)
- [coreApiWebdavUploadTUS/uploadToShare.feature:377](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L377)
#### [Renaming resource to banned name is allowed in spaces webdav](https://github.com/owncloud/ocis/issues/3099)

View File

@@ -116,8 +116,8 @@ _ocdav: api compatibility, return correct status code_
- [coreApiWebdavUploadTUS/uploadToShare.feature:256](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L256)
- [coreApiWebdavUploadTUS/uploadToShare.feature:279](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L279)
- [coreApiWebdavUploadTUS/uploadToShare.feature:280](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L280)
- [coreApiWebdavUploadTUS/uploadToShare.feature:375](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L375)
- [coreApiWebdavUploadTUS/uploadToShare.feature:376](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L376)
- [coreApiWebdavUploadTUS/uploadToShare.feature:377](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavUploadTUS/uploadToShare.feature#L377)
#### [Renaming resource to banned name is allowed in spaces webdav](https://github.com/owncloud/ocis/issues/3099)

View File

@@ -120,3 +120,26 @@ Feature: Propfind test
| Manager | RDNVWZP |
| Space Editor | DNVW |
| Space Viewer | |
@issue-1523
Scenario: propfind response contains a restored folder with correct name
Given user "Alice" has created a folder "folderMain" in space "Personal"
And user "Alice" has deleted folder "folderMain"
And user "Alice" has created a folder "folderMain" in space "Personal"
When user "Alice" restores the folder with original path "/folderMain" to "/folderMain (1)" using the trashbin API
And user "Alice" sends PROPFIND request to space "Personal" using the WebDAV API
Then the HTTP status code should be "207"
And as user "Alice" the PROPFIND 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 |
| oc:permissions | RDNVCKZP |
| oc:size | 0 |
And as user "Alice" the PROPFIND response should contain a resource "folderMain (1)" with these key and value pairs:
| key | value |
| oc:fileid | %file_id_pattern% |
| oc:file-parent | %file_id_pattern% |
| oc:name | folderMain (1) |
| oc:permissions | RDNVCKZP |
| oc:size | 0 |

View File

@@ -219,7 +219,7 @@ Feature: edit user
When the user "Brian" resets the password of user "Carol" to "newpassword" using the Graph API
Then the HTTP status code should be "403"
And the content of file "resetpassword.txt" for user "Carol" using password "1234" should be "test file for reset password"
But user "Carol" using password "newpassword" should not be able to download file "resetpassword.txt"
And user "Carol" should not be able to log in with wrong password "newpassword"
Examples:
| user-role | user-role-2 |
| Space Admin | Space Admin |

View File

@@ -542,6 +542,7 @@ Feature: enable or disable sync of incoming shares
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "textfile0.txt" synced
And the user "Admin" has deleted a user "Alice"
When user "Brian" disables sync of share "textfile0.txt" using the Graph API
Then the HTTP status code should be "204"
@@ -820,6 +821,7 @@ Feature: enable or disable sync of incoming shares
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "<resource>" synced
And user "Brian" has disabled sync of last shared resource
When user "Brian" disables sync of share "<resource>" using the Graph API
Then the HTTP status code should be "409"

View File

@@ -14,7 +14,7 @@ Feature: reset user password via CLI command
But the command output should not contain "Failed to update user password: entry does not exist"
And the administrator has started the server
And user "Alice" should be able to create folder "newFolder" using password "newpass"
But user "Alice" should not be able to create folder "anotherFolder" using password "%alt1%"
But user "Alice" should not be able to log in with wrong password "%alt1%"
Scenario: try to reset password of non-existing user

View File

@@ -23,6 +23,7 @@ Feature: sharing
| sharee | grp1 |
| shareType | group |
| permissionsRole | File Editor |
And user "Brian" has a share "textfile0.txt" synced
And user "Brian" has moved file "/Shares/textfile0.txt" to "/Shares/anotherName.txt"
When user "Alice" deletes the last share using the sharing API
Then the OCS status code should be "<ocs-status-code>"
@@ -89,6 +90,7 @@ Feature: sharing
| sharee | Brian |
| shareType | user |
| permissionsRole | Editor |
And user "Brian" has a share "shared" synced
When user "Brian" deletes file "/Shares/shared/shared_file.txt" using the WebDAV API
Then the HTTP status code should be "204"
And as "Brian" file "/Shares/shared/shared_file.txt" should not exist
@@ -114,6 +116,7 @@ Feature: sharing
| sharee | Brian |
| shareType | user |
| permissionsRole | Editor |
And user "Brian" has a share "shared" synced
When user "Brian" deletes folder "/Shares/shared/sub" using the WebDAV API
Then the HTTP status code should be "204"
And as "Brian" folder "/Shares/shared/sub" should not exist
@@ -165,6 +168,7 @@ Feature: sharing
| sharee | Brian |
| shareType | user |
| permissionsRole | Viewer |
And user "Brian" has a share "shared" synced
When user "Brian" deletes file "/Shares/shared/shared_file.txt" using the WebDAV API
Then the HTTP status code should be "403"
And as "Alice" file "/shared/shared_file.txt" should exist
@@ -187,6 +191,7 @@ Feature: sharing
| sharee | Brian |
| shareType | user |
| permissionsRole | Uploader |
And user "Brian" has a share "shared" synced
When user "Brian" deletes file "/Shares/shared/shared_file.txt" using the WebDAV API
Then the HTTP status code should be "403"
And as "Alice" file "/shared/shared_file.txt" should exist
@@ -208,6 +213,7 @@ Feature: sharing
| sharee | Brian |
| shareType | user |
| permissionsRole | Uploader |
And user "Brian" has a share "shared" synced
And user "Brian" has uploaded file "filesForUpload/textfile.txt" to "/Shares/shared/textfile.txt"
When user "Brian" deletes file "/Shares/shared/textfile.txt" using the WebDAV API
Then the HTTP status code should be "403"
@@ -237,6 +243,7 @@ Feature: sharing
| sharee | grp1 |
| shareType | group |
| permissionsRole | <permission-role> |
And user "Brian" has a share "<sync-entry>" synced
When user "Brian" deletes the last share of user "Alice" using the sharing API
Then the OCS status code should be "996"
And the HTTP status code should be "<http-status-code>"
@@ -244,11 +251,11 @@ Feature: sharing
And as "Brian" entry "<received-entry>" should exist
And as "Carol" entry "<received-entry>" should exist
Examples:
| entry-to-share | permission-role | ocs-api-version | http-status-code | received-entry |
| /shared/shared_file.txt | File Editor | 1 | 200 | /Shares/shared_file.txt |
| /shared/shared_file.txt | File Editor | 2 | 500 | /Shares/shared_file.txt |
| /shared | Editor | 1 | 200 | /Shares/shared |
| /shared | Editor | 2 | 500 | /Shares/shared |
| entry-to-share | permission-role | ocs-api-version | http-status-code | received-entry | sync-entry |
| /shared/shared_file.txt | File Editor | 1 | 200 | /Shares/shared_file.txt | shared_file.txt |
| /shared/shared_file.txt | File Editor | 2 | 500 | /Shares/shared_file.txt | shared_file.txt |
| /shared | Editor | 1 | 200 | /Shares/shared | shared |
| /shared | Editor | 2 | 500 | /Shares/shared | shared |
Scenario Outline: individual share recipient tries to delete the share
@@ -262,17 +269,18 @@ Feature: sharing
| sharee | Brian |
| shareType | user |
| permissionsRole | <permission-role> |
And user "Brian" has a share "<sync-entry>" synced
When user "Brian" deletes the last share of user "Alice" using the sharing API
Then the OCS status code should be "996"
And the HTTP status code should be "<http-status-code>"
And as "Alice" entry "<entry-to-share>" should exist
And as "Brian" entry "<received-entry>" should exist
Examples:
| entry-to-share | permission-role | ocs-api-version | http-status-code | received-entry |
| /shared/shared_file.txt | File Editor | 1 | 200 | /Shares/shared_file.txt |
| /shared/shared_file.txt | File Editor | 2 | 500 | /Shares/shared_file.txt |
| /shared | Editor | 1 | 200 | /Shares/shared |
| /shared | Editor | 2 | 500 | /Shares/shared |
| entry-to-share | permission-role | ocs-api-version | http-status-code | received-entry | sync-entry |
| /shared/shared_file.txt | File Editor | 1 | 200 | /Shares/shared_file.txt | shared_file.txt |
| /shared/shared_file.txt | File Editor | 2 | 500 | /Shares/shared_file.txt | shared_file.txt |
| /shared | Editor | 1 | 200 | /Shares/shared | shared |
| /shared | Editor | 2 | 500 | /Shares/shared | shared |
@issue-720
Scenario Outline: request PROPFIND after sharer deletes the collaborator
@@ -284,6 +292,7 @@ Feature: sharing
| sharee | Brian |
| shareType | user |
| permissionsRole | File Editor |
And user "Brian" has a share "textfile0.txt" synced
When user "Alice" deletes the last share using the sharing API
Then the OCS status code should be "<ocs-status-code>"
And the HTTP status code should be "200"
@@ -304,6 +313,7 @@ Feature: sharing
| sharee | Brian |
| shareType | user |
| permissionsRole | File Editor |
And user "Brian" has a share "textfile0.txt" synced
When user "Brian" tries to delete the last share of user "Alice" using the sharing API
Then the HTTP status code should be "<http-status-code>"
And the OCS status code should be "996"

View File

@@ -567,3 +567,52 @@ Feature: restore deleted files/folders
| dav-path-version |
| spaces |
| new |
@issue-1523
Scenario Outline: restore deleted folder when folder with same name exists
Given using <dav-path-version> DAV path
And user "Alice" has created folder "new"
And user "Alice" has uploaded file with content "content" to "new/test.txt"
And user "Alice" has deleted folder "new"
And user "Alice" has created folder "new"
And user "Alice" has uploaded file with content "new content" to "new/new-file.txt"
When user "Alice" restores the folder with original path "/new" to "/new (1)" using the trashbin API
Then the HTTP status code should be "201"
And as "Alice" the following folders should exist
| path |
| /new |
| /new (1) |
And as "Alice" the following files should exist
| path |
| /new/new-file.txt |
| /new (1)/test.txt |
Examples:
| dav-path-version |
| spaces |
| new |
@issue-1523
Scenario Outline: restore deleted folder with files when folder with same name exists
Given using <dav-path-version> DAV path
And user "Alice" has created folder "folder-a"
And user "Alice" has uploaded file with content "content b" to "folder-a/b.txt"
And user "Alice" has uploaded file with content "content c" to "folder-a/c.txt"
And user "Alice" has deleted file "folder-a/b.txt"
And user "Alice" has deleted folder "folder-a"
And user "Alice" has created folder "folder-a"
When user "Alice" restores the file with original path "folder-a/b.txt" using the trashbin API
Then the HTTP status code should be "201"
When user "Alice" restores the folder with original path "/folder-a" to "/folder-a (1)" using the trashbin API
Then the HTTP status code should be "201"
And as "Alice" the following folders should exist
| path |
| /folder-a |
| /folder-a (1) |
And as "Alice" the following files should exist
| path |
| /folder-a/b.txt |
| /folder-a (1)/c.txt |
Examples:
| dav-path-version |
| spaces |
| new |

View File

@@ -50,6 +50,7 @@ Feature: low level tests for upload of chunks
| Upload-Metadata | filename ZmlsZS50eHQ= |
When user "Alice" sends a chunk to the last created TUS Location with offset "0" and data "123" using the WebDAV API
And user "Alice" sends a chunk to the last created TUS Location with offset "3" and data "4567890" using the WebDAV API
And the user waits for "2" seconds for postprocessing to finish
And user "Alice" sends a chunk to the last created TUS Location with offset "3" and data "0000000" using the WebDAV API
Then the HTTP status code should be "404"
And the content of file "/file.txt" for user "Alice" should be "1234567890"

View File

@@ -290,6 +290,7 @@ Feature: upload file to shared folder
And user "Alice" sends a chunk to the last created TUS Location with offset "5" and data "56789" with checksum "MD5 099ebea48ea9666a7da2177267983138" using the TUS protocol on the WebDAV API
And user "Alice" shares file "textFile.txt" with user "Brian" using the sharing API
Then the HTTP status code should be "200"
And user "Brian" has a share "textFile.txt" synced
And the content of file "/Shares/textFile.txt" for user "Brian" should be "0123456789"
Examples:
| dav-path-version |

View File

@@ -4,7 +4,6 @@
[![Coverage Status](https://coveralls.io/repos/github/blevesearch/bleve/badge.svg?branch=master)](https://coveralls.io/github/blevesearch/bleve?branch=master)
[![Go Reference](https://pkg.go.dev/badge/github.com/blevesearch/bleve/v2.svg)](https://pkg.go.dev/github.com/blevesearch/bleve/v2)
[![Join the chat](https://badges.gitter.im/join_chat.svg)](https://app.gitter.im/#/room/#blevesearch_bleve:gitter.im)
[![codebeat](https://codebeat.co/badges/38a7cbc9-9cf5-41c0-a315-0746178230f4)](https://codebeat.co/projects/github-com-blevesearch-bleve)
[![Go Report Card](https://goreportcard.com/badge/github.com/blevesearch/bleve/v2)](https://goreportcard.com/report/github.com/blevesearch/bleve/v2)
[![Sourcegraph](https://sourcegraph.com/github.com/blevesearch/bleve/-/badge.svg)](https://sourcegraph.com/github.com/blevesearch/bleve?badge)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
@@ -27,6 +26,8 @@ A modern indexing + search library in GO
* [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
* Supports [RRF (Reciprocal Rank Fusion) and RSF (Relative Score Fusion)](docs/score_fusion.md)
* [Result pagination](https://github.com/blevesearch/bleve/blob/master/docs/pagination.md)
* Query time boosting
* Search result match highlighting with document fragments
* Aggregations/faceting support:

View File

@@ -68,7 +68,7 @@ func newBuilder(path string, mapping mapping.IndexMapping, config map[string]int
return nil, err
}
config["internal"] = map[string][]byte{
string(mappingInternalKey): mappingBytes,
string(util.MappingInternalKey): mappingBytes,
}
// do not use real config, as these are options for the builder,

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2025 Couchbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fusion
import (
"github.com/blevesearch/bleve/v2/search"
)
type FusionResult struct {
Hits search.DocumentMatchCollection
Total uint64
MaxScore float64
}

131
vendor/github.com/blevesearch/bleve/v2/fusion/rrf.go generated vendored Normal file
View File

@@ -0,0 +1,131 @@
// Copyright (c) 2025 Couchbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fusion
import (
"fmt"
"sort"
"github.com/blevesearch/bleve/v2/search"
)
func formatRRFMessage(weight float64, rank int, rankConstant int) string {
return fmt.Sprintf("rrf score (weight=%.3f, rank=%d, rank_constant=%d), normalized score of", weight, rank, rankConstant)
}
// ReciprocalRankFusion performs a reciprocal rank fusion on the search results.
func ReciprocalRankFusion(hits search.DocumentMatchCollection, weights []float64, rankConstant int, windowSize int, numKNNQueries int, explain bool) FusionResult {
if len(hits) == 0 {
return FusionResult{
Hits: hits,
Total: 0,
MaxScore: 0.0,
}
}
// Create a map of document ID to a slice of ranks.
// The first element of the slice is the rank from the FTS search,
// and the subsequent elements are the ranks from the KNN searches.
docRanks := make(map[string][]int)
// Pre-assign rank lists to each candidate document
for _, hit := range hits {
docRanks[hit.ID] = make([]int, numKNNQueries+1)
}
// Only a max of `window_size` elements need to be counted for. Stop
// calculating rank once this threshold is hit.
sort.Slice(hits, func(a, b int) bool {
return scoreSortFunc()(hits[a], hits[b]) < 0
})
// Only consider top windowSize docs for rescoring
for i := range min(windowSize, len(hits)) {
if hits[i].Score != 0.0 {
// Skip if Score is 0, since that means the document was not
// found as part of FTS, and only in KNN.
docRanks[hits[i].ID][0] = i + 1
}
}
// Allocate knnDocs and reuse it within the loop
knnDocs := make([]*search.DocumentMatch, 0, len(hits))
// For each KNN query, rank the documents based on their KNN score.
for i := range numKNNQueries {
knnDocs = knnDocs[:0]
for _, hit := range hits {
if _, ok := hit.ScoreBreakdown[i]; ok {
knnDocs = append(knnDocs, hit)
}
}
// Sort the documents based on their score for this KNN query.
sort.Slice(knnDocs, func(a, b int) bool {
return scoreBreakdownSortFunc(i)(knnDocs[a], knnDocs[b]) < 0
})
// Update the ranks of the documents in the docRanks map.
// Only consider top windowSize docs for rescoring.
for j := range min(windowSize, len(knnDocs)) {
docRanks[knnDocs[j].ID][i+1] = j + 1
}
}
// Calculate the RRF score for each document.
var maxScore float64
for _, hit := range hits {
var rrfScore float64
var explChildren []*search.Explanation
if explain {
explChildren = make([]*search.Explanation, 0, numKNNQueries+1)
}
for i, rank := range docRanks[hit.ID] {
if rank > 0 {
partialRrfScore := weights[i] * 1.0 / float64(rankConstant+rank)
if explain {
expl := getFusionExplAt(
hit,
i,
partialRrfScore,
formatRRFMessage(weights[i], rank, rankConstant),
)
explChildren = append(explChildren, expl)
}
rrfScore += partialRrfScore
}
}
hit.Score = rrfScore
hit.ScoreBreakdown = nil
if rrfScore > maxScore {
maxScore = rrfScore
}
if explain {
finalizeFusionExpl(hit, explChildren)
}
}
sort.Sort(hits)
if len(hits) > windowSize {
hits = hits[:windowSize]
}
return FusionResult{
Hits: hits,
Total: uint64(len(hits)),
MaxScore: maxScore,
}
}

162
vendor/github.com/blevesearch/bleve/v2/fusion/rsf.go generated vendored Normal file
View File

@@ -0,0 +1,162 @@
// Copyright (c) 2025 Couchbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fusion
import (
"fmt"
"sort"
"github.com/blevesearch/bleve/v2/search"
)
func formatRSFMessage(weight float64, normalizedScore float64, minScore float64, maxScore float64) string {
return fmt.Sprintf("rsf score (weight=%.3f, normalized=%.6f, min=%.6f, max=%.6f), normalized score of",
weight, normalizedScore, minScore, maxScore)
}
// RelativeScoreFusion normalizes scores based on min/max values for FTS and each KNN query, then applies weights.
func RelativeScoreFusion(hits search.DocumentMatchCollection, weights []float64, windowSize int, numKNNQueries int, explain bool) FusionResult {
if len(hits) == 0 {
return FusionResult{
Hits: hits,
Total: 0,
MaxScore: 0.0,
}
}
rsfScores := make(map[string]float64)
// contains the docs under consideration for scoring.
// Reused for fts and knn hits
scoringDocs := make([]*search.DocumentMatch, 0, len(hits))
var explMap map[string][]*search.Explanation
if explain {
explMap = make(map[string][]*search.Explanation)
}
// remove non-fts hits
for _, hit := range hits {
if hit.Score != 0.0 {
scoringDocs = append(scoringDocs, hit)
}
}
// sort hits by fts score
sort.Slice(scoringDocs, func(a, b int) bool {
return scoreSortFunc()(scoringDocs[a], scoringDocs[b]) < 0
})
// Reslice to correct size
if len(scoringDocs) > windowSize {
scoringDocs = scoringDocs[:windowSize]
}
var min, max float64
if len(scoringDocs) > 0 {
min, max = scoringDocs[len(scoringDocs)-1].Score, scoringDocs[0].Score
}
for _, hit := range scoringDocs {
var tempRsfScore float64
if max > min {
tempRsfScore = (hit.Score - min) / (max - min)
} else {
tempRsfScore = 1.0
}
if explain {
// create and replace new explanation
expl := getFusionExplAt(
hit,
0,
tempRsfScore,
formatRSFMessage(weights[0], tempRsfScore, min, max),
)
explMap[hit.ID] = append(explMap[hit.ID], expl)
}
rsfScores[hit.ID] = weights[0] * tempRsfScore
}
for i := range numKNNQueries {
scoringDocs = scoringDocs[:0]
for _, hit := range hits {
if _, exists := hit.ScoreBreakdown[i]; exists {
scoringDocs = append(scoringDocs, hit)
}
}
sort.Slice(scoringDocs, func(a, b int) bool {
return scoreBreakdownSortFunc(i)(scoringDocs[a], scoringDocs[b]) < 0
})
if len(scoringDocs) > windowSize {
scoringDocs = scoringDocs[:windowSize]
}
if len(scoringDocs) > 0 {
min, max = scoringDocs[len(scoringDocs)-1].ScoreBreakdown[i], scoringDocs[0].ScoreBreakdown[i]
} else {
min, max = 0.0, 0.0
}
for _, hit := range scoringDocs {
var tempRsfScore float64
if max > min {
tempRsfScore = (hit.ScoreBreakdown[i] - min) / (max - min)
} else {
tempRsfScore = 1.0
}
if explain {
expl := getFusionExplAt(
hit,
i+1,
tempRsfScore,
formatRSFMessage(weights[i+1], tempRsfScore, min, max),
)
explMap[hit.ID] = append(explMap[hit.ID], expl)
}
rsfScores[hit.ID] += weights[i+1] * tempRsfScore
}
}
var maxScore float64
for _, hit := range hits {
if rsfScore, exists := rsfScores[hit.ID]; exists {
hit.Score = rsfScore
if rsfScore > maxScore {
maxScore = rsfScore
}
if explain {
finalizeFusionExpl(hit, explMap[hit.ID])
}
} else {
hit.Score = 0.0
}
hit.ScoreBreakdown = nil
}
sort.Sort(hits)
if len(hits) > windowSize {
hits = hits[:windowSize]
}
return FusionResult{
Hits: hits,
Total: uint64(len(hits)),
MaxScore: maxScore,
}
}

96
vendor/github.com/blevesearch/bleve/v2/fusion/util.go generated vendored Normal file
View File

@@ -0,0 +1,96 @@
// Copyright (c) 2025 Couchbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fusion
import (
"github.com/blevesearch/bleve/v2/search"
)
// scoreBreakdownSortFunc returns a comparison function for sorting DocumentMatch objects
// by their ScoreBreakdown at the specified index in descending order.
// In case of ties, documents with lower HitNumber (earlier hits) are preferred.
// If either document is missing the ScoreBreakdown for the specified index,
// it's treated as having a score of 0.0.
func scoreBreakdownSortFunc(idx int) func(i, j *search.DocumentMatch) int {
return func(i, j *search.DocumentMatch) int {
// Safely extract scores, defaulting to 0.0 if missing
iScore := 0.0
jScore := 0.0
if i.ScoreBreakdown != nil {
if score, ok := i.ScoreBreakdown[idx]; ok {
iScore = score
}
}
if j.ScoreBreakdown != nil {
if score, ok := j.ScoreBreakdown[idx]; ok {
jScore = score
}
}
// Sort by score in descending order (higher scores first)
if iScore > jScore {
return -1
} else if iScore < jScore {
return 1
}
// Break ties by HitNumber in ascending order (lower HitNumber wins)
if i.HitNumber < j.HitNumber {
return -1
} else if i.HitNumber > j.HitNumber {
return 1
}
return 0 // Equal scores and HitNumbers
}
}
func scoreSortFunc() func(i, j *search.DocumentMatch) int {
return func(i, j *search.DocumentMatch) int {
// Sort by score in descending order
if i.Score > j.Score {
return -1
} else if i.Score < j.Score {
return 1
}
// Break ties by HitNumber
if i.HitNumber < j.HitNumber {
return -1
} else if i.HitNumber > j.HitNumber {
return 1
}
return 0
}
}
func getFusionExplAt(hit *search.DocumentMatch, i int, value float64, message string) *search.Explanation {
return &search.Explanation{
Value: value,
Message: message,
Children: []*search.Explanation{hit.Expl.Children[i]},
}
}
func finalizeFusionExpl(hit *search.DocumentMatch, explChildren []*search.Explanation) {
hit.Expl.Children = explChildren
hit.Expl.Value = hit.Score
hit.Expl.Message = "sum of"
}

View File

@@ -308,5 +308,5 @@ First, all of this geo code is a Go adaptation of the [Lucene 5.3.2 sandbox geo
- LineStrings and MultiLineStrings may only contain Points and MultiPoints.
- Polygons or MultiPolygons intersecting Polygons and MultiPolygons may return arbitrary results when the overlap is only an edge or a vertex.
- Circles containing polygon will return a false positive result if all of the vertices of the polygon are within the circle, but the orientation of those points are clock-wise.
- The edges of an Envelope follows the latitude and logitude lines instead of the shortest path on a globe.
- The edges of an Envelope follows the latitude and longitude lines instead of the shortest path on a globe.
- Envelope intersecting queries with LineStrings, MultiLineStrings, Polygons and MultiPolygons implicitly converts the Envelope into a Polygon which changes the curvature of the edges causing inaccurate results for few edge cases.

View File

@@ -114,7 +114,7 @@ func DegreesToRadians(d float64) float64 {
return d * degreesToRadian
}
// RadiansToDegrees converts an angle in radians to degress
// RadiansToDegrees converts an angle in radians to degrees
func RadiansToDegrees(r float64) float64 {
return r * radiansToDegrees
}

View File

@@ -83,7 +83,7 @@ func ParseDistanceUnit(u string) (float64, error) {
}
// Haversin computes the distance between two points.
// This implemenation uses the sloppy math implemenations which trade off
// This implementation uses the sloppy math implementations which trade off
// accuracy for performance. The distance returned is in kilometers.
func Haversin(lon1, lat1, lon2, lat2 float64) float64 {
x1 := lat1 * degreesToRadian

View File

@@ -149,7 +149,7 @@ func (b *Batch) String() string {
}
// Reset returns a Batch to the empty state so that it can
// be re-used in the future.
// be reused in the future.
func (b *Batch) Reset() {
b.internal.Reset()
b.lastDocSize = 0
@@ -325,6 +325,8 @@ func Open(path string) (Index, error) {
// The mapping used when it was created will be used for all Index/Search operations.
// The provided runtimeConfig can override settings
// persisted when the kvstore was created.
// If runtimeConfig has updated mapping, then an index update is attempted
// Throws an error without any changes to the index if an unupdatable mapping is provided
func OpenUsing(path string, runtimeConfig map[string]interface{}) (Index, error) {
return openIndexUsing(path, runtimeConfig)
}

View File

@@ -293,7 +293,7 @@ func (s *Scorch) introducePersist(persist *persistIntroduction) {
newIndexSnapshot.segment[i] = newSegmentSnapshot
delete(persist.persisted, segmentSnapshot.id)
// update items persisted incase of a new segment snapshot
// update items persisted in case of a new segment snapshot
atomic.AddUint64(&s.stats.TotPersistedItems, newSegmentSnapshot.Count())
atomic.AddUint64(&s.stats.TotPersistedSegments, 1)
fileSegments++

View File

@@ -295,7 +295,7 @@ 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
// create tasks with valid merges - i.e. there should be at least 2 non-empty segments
if len(bestRoster) > 1 {
rv.Tasks = append(rv.Tasks, &MergeTask{Segments: bestRoster})
}

View File

@@ -79,6 +79,12 @@ func (o *OptimizeVR) Finish() error {
wg.Done()
}()
for field, vrs := range o.vrs {
// Early exit if the field is supposed to be completely deleted or
// if it's index data has been deleted
if info, ok := o.snapshot.updatedFields[field]; ok && (info.Deleted || info.Index) {
continue
}
vecIndex, err := segment.InterpretVectorIndex(field,
o.requiresFiltering, origSeg.deleted)
if err != nil {
@@ -185,7 +191,7 @@ func (s *IndexSnapshotVectorReader) VectorOptimize(ctx context.Context,
err := cbF(sumVectorIndexSize)
if err != nil {
// it's important to invoke the end callback at this point since
// if the earlier searchers of this optimze struct were successful
// if the earlier searchers of this optimize struct were successful
// the cost corresponding to it would be incremented and if the
// current searcher fails the check then we end up erroring out
// the overall optimized searcher creation, the cost needs to be

View File

@@ -386,7 +386,7 @@ type flushable struct {
totDocs uint64
}
// number workers which parallely perform an in-memory merge of the segments
// number workers which parallelly perform an in-memory merge of the segments
// followed by a flush operation.
var DefaultNumPersisterWorkers = 1
@@ -395,7 +395,7 @@ var DefaultNumPersisterWorkers = 1
var DefaultMaxSizeInMemoryMergePerWorker = 0
func legacyFlushBehaviour(maxSizeInMemoryMergePerWorker, numPersisterWorkers int) bool {
// DefaultMaxSizeInMemoryMergePerWorker = 0 is a special value to preserve the leagcy
// DefaultMaxSizeInMemoryMergePerWorker = 0 is a special value to preserve the legacy
// one-shot in-memory merge + flush behaviour.
return maxSizeInMemoryMergePerWorker == 0 && numPersisterWorkers == 1
}
@@ -608,7 +608,7 @@ func persistToDirectory(seg segment.UnpersistedSegment, d index.Directory,
func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
segPlugin SegmentPlugin, exclude map[uint64]struct{}, d index.Directory) (
[]string, map[uint64]string, error) {
snapshotsBucket, err := tx.CreateBucketIfNotExists(boltSnapshotsBucket)
snapshotsBucket, err := tx.CreateBucketIfNotExists(util.BoltSnapshotsBucket)
if err != nil {
return nil, nil, err
}
@@ -619,17 +619,17 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
}
// persist meta values
metaBucket, err := snapshotBucket.CreateBucketIfNotExists(boltMetaDataKey)
metaBucket, err := snapshotBucket.CreateBucketIfNotExists(util.BoltMetaDataKey)
if err != nil {
return nil, nil, err
}
err = metaBucket.Put(boltMetaDataSegmentTypeKey, []byte(segPlugin.Type()))
err = metaBucket.Put(util.BoltMetaDataSegmentTypeKey, []byte(segPlugin.Type()))
if err != nil {
return nil, nil, err
}
buf := make([]byte, binary.MaxVarintLen32)
binary.BigEndian.PutUint32(buf, segPlugin.Version())
err = metaBucket.Put(boltMetaDataSegmentVersionKey, buf)
err = metaBucket.Put(util.BoltMetaDataSegmentVersionKey, buf)
if err != nil {
return nil, nil, err
}
@@ -643,13 +643,13 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
if err != nil {
return nil, nil, err
}
err = metaBucket.Put(boltMetaDataTimeStamp, timeStampBinary)
err = metaBucket.Put(util.BoltMetaDataTimeStamp, timeStampBinary)
if err != nil {
return nil, nil, err
}
// persist internal values
internalBucket, err := snapshotBucket.CreateBucketIfNotExists(boltInternalKey)
internalBucket, err := snapshotBucket.CreateBucketIfNotExists(util.BoltInternalKey)
if err != nil {
return nil, nil, err
}
@@ -665,7 +665,7 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
val := make([]byte, 8)
bytesWritten := atomic.LoadUint64(&snapshot.parent.stats.TotBytesWrittenAtIndexTime)
binary.LittleEndian.PutUint64(val, bytesWritten)
err = internalBucket.Put(TotBytesWrittenKey, val)
err = internalBucket.Put(util.TotBytesWrittenKey, val)
if err != nil {
return nil, nil, err
}
@@ -689,7 +689,7 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
return nil, nil, fmt.Errorf("segment: %s copy err: %v", segPath, err)
}
filename := filepath.Base(segPath)
err = snapshotSegmentBucket.Put(boltPathKey, []byte(filename))
err = snapshotSegmentBucket.Put(util.BoltPathKey, []byte(filename))
if err != nil {
return nil, nil, err
}
@@ -705,7 +705,7 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
return nil, nil, fmt.Errorf("segment: %s persist err: %v", path, err)
}
newSegmentPaths[segmentSnapshot.id] = path
err = snapshotSegmentBucket.Put(boltPathKey, []byte(filename))
err = snapshotSegmentBucket.Put(util.BoltPathKey, []byte(filename))
if err != nil {
return nil, nil, err
}
@@ -721,7 +721,7 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
if err != nil {
return nil, nil, fmt.Errorf("error persisting roaring bytes: %v", err)
}
err = snapshotSegmentBucket.Put(boltDeletedKey, roaringBuf.Bytes())
err = snapshotSegmentBucket.Put(util.BoltDeletedKey, roaringBuf.Bytes())
if err != nil {
return nil, nil, err
}
@@ -733,7 +733,19 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
if err != nil {
return nil, nil, err
}
err = snapshotSegmentBucket.Put(boltStatsKey, b)
err = snapshotSegmentBucket.Put(util.BoltStatsKey, b)
if err != nil {
return nil, nil, err
}
}
// store updated field info
if segmentSnapshot.updatedFields != nil {
b, err := json.Marshal(segmentSnapshot.updatedFields)
if err != nil {
return nil, nil, err
}
err = snapshotSegmentBucket.Put(util.BoltUpdatedFieldsKey, b)
if err != nil {
return nil, nil, err
}
@@ -832,22 +844,9 @@ func zapFileName(epoch uint64) string {
// bolt snapshot code
var (
boltSnapshotsBucket = []byte{'s'}
boltPathKey = []byte{'p'}
boltDeletedKey = []byte{'d'}
boltInternalKey = []byte{'i'}
boltMetaDataKey = []byte{'m'}
boltMetaDataSegmentTypeKey = []byte("type")
boltMetaDataSegmentVersionKey = []byte("version")
boltMetaDataTimeStamp = []byte("timeStamp")
boltStatsKey = []byte("stats")
TotBytesWrittenKey = []byte("TotBytesWritten")
)
func (s *Scorch) loadFromBolt() error {
err := s.rootBolt.View(func(tx *bolt.Tx) error {
snapshots := tx.Bucket(boltSnapshotsBucket)
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
@@ -912,7 +911,7 @@ func (s *Scorch) loadFromBolt() error {
// NOTE: this is currently ONLY intended to be used by the command-line tool
func (s *Scorch) LoadSnapshot(epoch uint64) (rv *IndexSnapshot, err error) {
err = s.rootBolt.View(func(tx *bolt.Tx) error {
snapshots := tx.Bucket(boltSnapshotsBucket)
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
@@ -940,14 +939,14 @@ func (s *Scorch) loadSnapshot(snapshot *bolt.Bucket) (*IndexSnapshot, error) {
// first we look for the meta-data bucket, this will tell us
// which segment type/version was used for this snapshot
// all operations for this scorch will use this type/version
metaBucket := snapshot.Bucket(boltMetaDataKey)
metaBucket := snapshot.Bucket(util.BoltMetaDataKey)
if metaBucket == nil {
_ = rv.DecRef()
return nil, fmt.Errorf("meta-data bucket missing")
}
segmentType := string(metaBucket.Get(boltMetaDataSegmentTypeKey))
segmentType := string(metaBucket.Get(util.BoltMetaDataSegmentTypeKey))
segmentVersion := binary.BigEndian.Uint32(
metaBucket.Get(boltMetaDataSegmentVersionKey))
metaBucket.Get(util.BoltMetaDataSegmentVersionKey))
err := s.loadSegmentPlugin(segmentType, segmentVersion)
if err != nil {
_ = rv.DecRef()
@@ -957,7 +956,7 @@ func (s *Scorch) loadSnapshot(snapshot *bolt.Bucket) (*IndexSnapshot, error) {
var running uint64
c := snapshot.Cursor()
for k, _ := c.First(); k != nil; k, _ = c.Next() {
if k[0] == boltInternalKey[0] {
if k[0] == util.BoltInternalKey[0] {
internalBucket := snapshot.Bucket(k)
if internalBucket == nil {
_ = rv.DecRef()
@@ -972,11 +971,11 @@ func (s *Scorch) loadSnapshot(snapshot *bolt.Bucket) (*IndexSnapshot, error) {
_ = rv.DecRef()
return nil, err
}
} else if k[0] != boltMetaDataKey[0] {
} else if k[0] != util.BoltMetaDataKey[0] {
segmentBucket := snapshot.Bucket(k)
if segmentBucket == nil {
_ = rv.DecRef()
return nil, fmt.Errorf("segment key, but bucket missing % x", k)
return nil, fmt.Errorf("segment key, but bucket missing %x", k)
}
segmentSnapshot, err := s.loadSegment(segmentBucket)
if err != nil {
@@ -990,6 +989,10 @@ func (s *Scorch) loadSnapshot(snapshot *bolt.Bucket) (*IndexSnapshot, error) {
}
rv.segment = append(rv.segment, segmentSnapshot)
rv.offsets = append(rv.offsets, running)
// Merge all segment level updated field info for use during queries
if segmentSnapshot.updatedFields != nil {
rv.MergeUpdateFieldsInfo(segmentSnapshot.updatedFields)
}
running += segmentSnapshot.segment.Count()
}
}
@@ -997,46 +1000,59 @@ func (s *Scorch) loadSnapshot(snapshot *bolt.Bucket) (*IndexSnapshot, error) {
}
func (s *Scorch) loadSegment(segmentBucket *bolt.Bucket) (*SegmentSnapshot, error) {
pathBytes := segmentBucket.Get(boltPathKey)
pathBytes := segmentBucket.Get(util.BoltPathKey)
if pathBytes == nil {
return nil, fmt.Errorf("segment path missing")
}
segmentPath := s.path + string(os.PathSeparator) + string(pathBytes)
segment, err := s.segPlugin.Open(segmentPath)
seg, err := s.segPlugin.Open(segmentPath)
if err != nil {
return nil, fmt.Errorf("error opening bolt segment: %v", err)
}
rv := &SegmentSnapshot{
segment: segment,
segment: seg,
cachedDocs: &cachedDocs{cache: nil},
cachedMeta: &cachedMeta{meta: nil},
}
deletedBytes := segmentBucket.Get(boltDeletedKey)
deletedBytes := segmentBucket.Get(util.BoltDeletedKey)
if deletedBytes != nil {
deletedBitmap := roaring.NewBitmap()
r := bytes.NewReader(deletedBytes)
_, err := deletedBitmap.ReadFrom(r)
if err != nil {
_ = segment.Close()
_ = seg.Close()
return nil, fmt.Errorf("error reading deleted bytes: %v", err)
}
if !deletedBitmap.IsEmpty() {
rv.deleted = deletedBitmap
}
}
statBytes := segmentBucket.Get(boltStatsKey)
statBytes := segmentBucket.Get(util.BoltStatsKey)
if statBytes != nil {
var statsMap map[string]map[string]uint64
err := json.Unmarshal(statBytes, &statsMap)
stats := &fieldStats{statMap: statsMap}
if err != nil {
_ = segment.Close()
_ = seg.Close()
return nil, fmt.Errorf("error reading stat bytes: %v", err)
}
rv.stats = stats
}
updatedFieldBytes := segmentBucket.Get(util.BoltUpdatedFieldsKey)
if updatedFieldBytes != nil {
var updatedFields map[string]*index.UpdateFieldInfo
err := json.Unmarshal(updatedFieldBytes, &updatedFields)
if err != nil {
_ = seg.Close()
return nil, fmt.Errorf("error reading updated field bytes: %v", err)
}
rv.updatedFields = updatedFields
// Set the value within the segment base for use during merge
rv.UpdateFieldsInfo(rv.updatedFields)
}
return rv, nil
}
@@ -1215,7 +1231,7 @@ func (s *Scorch) removeOldBoltSnapshots() (numRemoved int, err error) {
}
}()
snapshots := tx.Bucket(boltSnapshotsBucket)
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return 0, nil
}
@@ -1293,7 +1309,7 @@ func (s *Scorch) removeOldZapFiles() error {
// duration. This results in all of them being purged from the boltDB
// and the next iteration of the removeOldData() would end up protecting
// latest contiguous snapshot which is a poor pattern in the rollback checkpoints.
// Hence we try to retain atmost retentionFactor portion worth of old snapshots
// Hence we try to retain at most retentionFactor portion worth of old snapshots
// in such a scenario using the following function
func getBoundaryCheckPoint(retentionFactor float64,
checkPoints []*snapshotMetaData, timeStamp time.Time,
@@ -1325,7 +1341,7 @@ func (s *Scorch) rootBoltSnapshotMetaData() ([]*snapshotMetaData, error) {
expirationDuration := time.Duration(s.numSnapshotsToKeep-1) * s.rollbackSamplingInterval
err := s.rootBolt.View(func(tx *bolt.Tx) error {
snapshots := tx.Bucket(boltSnapshotsBucket)
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
@@ -1349,11 +1365,11 @@ func (s *Scorch) rootBoltSnapshotMetaData() ([]*snapshotMetaData, error) {
if snapshot == nil {
continue
}
metaBucket := snapshot.Bucket(boltMetaDataKey)
metaBucket := snapshot.Bucket(util.BoltMetaDataKey)
if metaBucket == nil {
continue
}
timeStampBytes := metaBucket.Get(boltMetaDataTimeStamp)
timeStampBytes := metaBucket.Get(util.BoltMetaDataTimeStamp)
var timeStamp time.Time
err = timeStamp.UnmarshalText(timeStampBytes)
if err != nil {
@@ -1390,7 +1406,7 @@ func (s *Scorch) rootBoltSnapshotMetaData() ([]*snapshotMetaData, error) {
func (s *Scorch) RootBoltSnapshotEpochs() ([]uint64, error) {
var rv []uint64
err := s.rootBolt.View(func(tx *bolt.Tx) error {
snapshots := tx.Bucket(boltSnapshotsBucket)
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
@@ -1411,7 +1427,7 @@ func (s *Scorch) RootBoltSnapshotEpochs() ([]uint64, error) {
func (s *Scorch) loadZapFileNames() (map[string]struct{}, error) {
rv := map[string]struct{}{}
err := s.rootBolt.View(func(tx *bolt.Tx) error {
snapshots := tx.Bucket(boltSnapshotsBucket)
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
@@ -1423,14 +1439,14 @@ func (s *Scorch) loadZapFileNames() (map[string]struct{}, error) {
}
segc := snapshot.Cursor()
for segk, _ := segc.First(); segk != nil; segk, _ = segc.Next() {
if segk[0] == boltInternalKey[0] {
if segk[0] == util.BoltInternalKey[0] {
continue
}
segmentBucket := snapshot.Bucket(segk)
if segmentBucket == nil {
continue
}
pathBytes := segmentBucket.Get(boltPathKey)
pathBytes := segmentBucket.Get(util.BoltPathKey)
if pathBytes == nil {
continue
}

View File

@@ -19,6 +19,7 @@ import (
"log"
"os"
"github.com/blevesearch/bleve/v2/util"
bolt "go.etcd.io/bbolt"
)
@@ -61,7 +62,7 @@ func RollbackPoints(path string) ([]*RollbackPoint, error) {
_ = rootBolt.Close()
}()
snapshots := tx.Bucket(boltSnapshotsBucket)
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil, nil
}
@@ -87,7 +88,7 @@ func RollbackPoints(path string) ([]*RollbackPoint, error) {
meta := map[string][]byte{}
c2 := snapshot.Cursor()
for j, _ := c2.First(); j != nil; j, _ = c2.Next() {
if j[0] == boltInternalKey[0] {
if j[0] == util.BoltInternalKey[0] {
internalBucket := snapshot.Bucket(j)
if internalBucket == nil {
err = fmt.Errorf("internal bucket missing")
@@ -151,7 +152,7 @@ func Rollback(path string, to *RollbackPoint) error {
var found bool
var eligibleEpochs []uint64
err = rootBolt.View(func(tx *bolt.Tx) error {
snapshots := tx.Bucket(boltSnapshotsBucket)
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
@@ -193,7 +194,7 @@ func Rollback(path string, to *RollbackPoint) error {
}
}()
snapshots := tx.Bucket(boltSnapshotsBucket)
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}

View File

@@ -25,6 +25,7 @@ import (
"github.com/RoaringBitmap/roaring/v2"
"github.com/blevesearch/bleve/v2/registry"
"github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
segment "github.com/blevesearch/scorch_segment_api/v2"
bolt "go.etcd.io/bbolt"
@@ -217,9 +218,11 @@ func (s *Scorch) fireAsyncError(err error) {
}
func (s *Scorch) Open() error {
err := s.openBolt()
if err != nil {
return err
if s.rootBolt == nil {
err := s.openBolt()
if err != nil {
return err
}
}
s.asyncTasks.Add(1)
@@ -371,6 +374,7 @@ func (s *Scorch) Close() (err error) {
}
}
s.root = nil
s.rootBolt = nil
s.rootLock.Unlock()
}
@@ -940,3 +944,96 @@ func (s *Scorch) CopyReader() index.CopyReader {
func (s *Scorch) FireIndexEvent() {
s.fireEvent(EventKindIndexStart, 0)
}
// Updates bolt db with the given field info. Existing field info already in bolt
// will be merged before persisting. The index mapping is also overwritted both
// in bolt as well as the index snapshot
func (s *Scorch) UpdateFields(fieldInfo map[string]*index.UpdateFieldInfo, mappingBytes []byte) error {
err := s.updateBolt(fieldInfo, mappingBytes)
if err != nil {
return err
}
// Pass the update field info to all snapshots and segment bases
s.root.UpdateFieldsInfo(fieldInfo)
return nil
}
func (s *Scorch) OpenMeta() error {
if s.rootBolt == nil {
err := s.openBolt()
if err != nil {
return err
}
}
return nil
}
// Merge and update deleted field info and rewrite index mapping
func (s *Scorch) updateBolt(fieldInfo map[string]*index.UpdateFieldInfo, mappingBytes []byte) error {
return s.rootBolt.Update(func(tx *bolt.Tx) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
c := snapshots.Cursor()
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
_, _, err := decodeUvarintAscending(k)
if err != nil {
fmt.Printf("unable to parse segment epoch %x, continuing", k)
continue
}
snapshot := snapshots.Bucket(k)
cc := snapshot.Cursor()
for kk, _ := cc.First(); kk != nil; kk, _ = cc.Next() {
if kk[0] == util.BoltInternalKey[0] {
internalBucket := snapshot.Bucket(kk)
if internalBucket == nil {
return fmt.Errorf("segment key, but bucket missing %x", kk)
}
err = internalBucket.Put(util.MappingInternalKey, mappingBytes)
if err != nil {
return err
}
} else if kk[0] != util.BoltMetaDataKey[0] {
segmentBucket := snapshot.Bucket(kk)
if segmentBucket == nil {
return fmt.Errorf("segment key, but bucket missing %x", kk)
}
var updatedFields map[string]*index.UpdateFieldInfo
updatedFieldBytes := segmentBucket.Get(util.BoltUpdatedFieldsKey)
if updatedFieldBytes != nil {
err := json.Unmarshal(updatedFieldBytes, &updatedFields)
if err != nil {
return fmt.Errorf("error reading updated field bytes: %v", err)
}
for field, info := range fieldInfo {
if val, ok := updatedFields[field]; ok {
updatedFields[field] = &index.UpdateFieldInfo{
Deleted: info.Deleted || val.Deleted,
Store: info.Store || val.Store,
DocValues: info.DocValues || val.DocValues,
Index: info.Index || val.Index,
}
} else {
updatedFields[field] = info
}
}
} else {
updatedFields = fieldInfo
}
b, err := json.Marshal(updatedFields)
if err != nil {
return err
}
err = segmentBucket.Put(util.BoltUpdatedFieldsKey, b)
if err != nil {
return err
}
}
}
}
return nil
})
}

View File

@@ -84,6 +84,13 @@ type IndexSnapshot struct {
m3 sync.RWMutex // bm25 metrics specific - not to interfere with TFR creation
fieldCardinality map[string]int
// Stores information about zapx fields that have been
// fully deleted (indicated by UpdateFieldInfo.Deleted) or
// partially deleted index, store or docvalues (indicated by
// UpdateFieldInfo.Index or .Store or .DocValues).
// Used to short circuit queries trying to read stale data
updatedFields map[string]*index.UpdateFieldInfo
}
func (i *IndexSnapshot) Segments() []*SegmentSnapshot {
@@ -509,6 +516,13 @@ func (is *IndexSnapshot) Document(id string) (rv index.Document, err error) {
// Keeping that TODO for now until we have a cleaner way.
rvd.StoredFieldsSize += uint64(len(val))
// Skip fields that have been completely deleted or had their
// store data deleted
if info, ok := is.updatedFields[name]; ok &&
(info.Deleted || info.Store) {
return true
}
// copy value, array positions to preserve them beyond the scope of this callback
value := append([]byte(nil), val...)
arrayPos := append([]uint64(nil), pos...)
@@ -634,10 +648,22 @@ func (is *IndexSnapshot) TermFieldReader(ctx context.Context, term []byte, field
segBytesRead := s.segment.BytesRead()
rv.incrementBytesRead(segBytesRead)
}
dict, err := s.segment.Dictionary(field)
var dict segment.TermDictionary
var err error
// Skip fields that have been completely deleted or had their
// index data deleted
if info, ok := is.updatedFields[field]; ok &&
(info.Index || info.Deleted) {
dict, err = s.segment.Dictionary("")
} else {
dict, err = s.segment.Dictionary(field)
}
if err != nil {
return nil, err
}
if dictStats, ok := dict.(segment.DiskStatsReporter); ok {
bytesRead := dictStats.BytesRead()
rv.incrementBytesRead(bytesRead)
@@ -783,6 +809,23 @@ func (is *IndexSnapshot) documentVisitFieldTermsOnSegment(
}
}
// Filter out fields that have been completely deleted or had their
// docvalues data deleted from both visitable fields and required fields
filterUpdatedFields := func(fields []string) []string {
filteredFields := make([]string, 0)
for _, field := range fields {
if info, ok := is.updatedFields[field]; ok &&
(info.DocValues || info.Deleted) {
continue
}
filteredFields = append(filteredFields, field)
}
return filteredFields
}
fieldsFiltered := filterUpdatedFields(fields)
vFieldsFiltered := filterUpdatedFields(vFields)
var errCh chan error
// cFields represents the fields that we'll need from the
@@ -790,7 +833,7 @@ func (is *IndexSnapshot) documentVisitFieldTermsOnSegment(
// if the caller happens to know we're on the same segmentIndex
// from a previous invocation
if cFields == nil {
cFields = subtractStrings(fields, vFields)
cFields = subtractStrings(fieldsFiltered, vFieldsFiltered)
if !ss.cachedDocs.hasFields(cFields) {
errCh = make(chan error, 1)
@@ -805,8 +848,8 @@ func (is *IndexSnapshot) documentVisitFieldTermsOnSegment(
}
}
if ssvOk && ssv != nil && len(vFields) > 0 {
dvs, err = ssv.VisitDocValues(localDocNum, fields, visitor, dvs)
if ssvOk && ssv != nil && len(vFieldsFiltered) > 0 {
dvs, err = ssv.VisitDocValues(localDocNum, fieldsFiltered, visitor, dvs)
if err != nil {
return nil, nil, err
}
@@ -1161,3 +1204,33 @@ func (is *IndexSnapshot) ThesaurusKeysRegexp(name string,
func (is *IndexSnapshot) UpdateSynonymSearchCount(delta uint64) {
atomic.AddUint64(&is.parent.stats.TotSynonymSearches, delta)
}
// Update current snapshot updated field data as well as pass it on to all segments and segment bases
func (is *IndexSnapshot) UpdateFieldsInfo(updatedFields map[string]*index.UpdateFieldInfo) {
is.m.Lock()
defer is.m.Unlock()
is.MergeUpdateFieldsInfo(updatedFields)
for _, segmentSnapshot := range is.segment {
segmentSnapshot.UpdateFieldsInfo(is.updatedFields)
}
}
// Merge given updated field information with existing updated field information
func (is *IndexSnapshot) MergeUpdateFieldsInfo(updatedFields map[string]*index.UpdateFieldInfo) {
if is.updatedFields == nil {
is.updatedFields = updatedFields
} else {
for fieldName, info := range updatedFields {
if val, ok := is.updatedFields[fieldName]; ok {
val.Deleted = val.Deleted || info.Deleted
val.Index = val.Index || info.Index
val.DocValues = val.DocValues || info.DocValues
val.Store = val.Store || info.Store
} else {
is.updatedFields[fieldName] = info
}
}
}
}

View File

@@ -163,7 +163,7 @@ func (i *IndexSnapshotTermFieldReader) Advance(ID index.IndexInternalID, preAllo
// unadorned composite optimization
// we need to reset all the iterators
// back to the beginning, which effectively
// achives the same thing as the above
// achieves the same thing as the above
for _, iter := range i.iterators {
if optimizedIterator, ok := iter.(ResetablePostingsIterator); ok {
optimizedIterator.ResetIterator()

View File

@@ -83,6 +83,10 @@ func (i *IndexSnapshotVectorReader) Next(preAlloced *index.VectorDoc) (
}
for i.segmentOffset < len(i.iterators) {
if i.iterators[i.segmentOffset] == nil {
i.segmentOffset++
continue
}
next, err := i.iterators[i.segmentOffset].Next()
if err != nil {
return nil, err

View File

@@ -35,12 +35,13 @@ type SegmentSnapshot struct {
// segment was mmaped recently, in which case
// we consider the loading cost of the metadata
// as part of IO stats.
mmaped uint32
id uint64
segment segment.Segment
deleted *roaring.Bitmap
creator string
stats *fieldStats
mmaped uint32
id uint64
segment segment.Segment
deleted *roaring.Bitmap
creator string
stats *fieldStats
updatedFields map[string]*index.UpdateFieldInfo
cachedMeta *cachedMeta
@@ -146,6 +147,28 @@ func (s *SegmentSnapshot) Size() (rv int) {
return
}
// Merge given updated field information with existing and pass it on to the segment base
func (s *SegmentSnapshot) UpdateFieldsInfo(updatedFields map[string]*index.UpdateFieldInfo) {
if s.updatedFields == nil {
s.updatedFields = updatedFields
} else {
for fieldName, info := range updatedFields {
if val, ok := s.updatedFields[fieldName]; ok {
val.Deleted = val.Deleted || info.Deleted
val.Index = val.Index || info.Index
val.DocValues = val.DocValues || info.DocValues
val.Store = val.Store || info.Store
} else {
s.updatedFields[fieldName] = info
}
}
}
if segment, ok := s.segment.(segment.UpdatableSegment); ok {
segment.SetUpdatedFields(s.updatedFields)
}
}
type cachedFieldDocs struct {
m sync.Mutex
readyCh chan struct{} // closed when the cachedFieldDocs.docs is ready to be used.

View File

@@ -0,0 +1,13 @@
## Instructions for generating new go stubs using upsidedown.proto
1. Download latest of protoc-gen-go
```
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
```
2. To generate `upsidedown.pb.go` using upsdidedown.proto:
```
protoc --go_out=. --go_opt=Mindex/upsidedown/upsidedown.proto=index/upsidedown/ index/upsidedown/upsidedown.proto
```
3. Manually add back Size and MarshalTo methods for BackIndexRowValue, BackIndexTermsEntry, BackIndexStoreEntry to support upside_down.

View File

@@ -371,6 +371,6 @@ func (r *UpsideDownCouchDocIDReader) nextOnly() bool {
start = r.onlyPos
r.onlyPos++
}
// inidicate if we got to the end of the list
// indicate if we got to the end of the list
return r.onlyPos < len(r.only)
}

View File

@@ -23,7 +23,7 @@ import (
"reflect"
"github.com/blevesearch/bleve/v2/size"
"github.com/golang/protobuf/proto"
"google.golang.org/protobuf/proto"
)
var (
@@ -924,7 +924,7 @@ type backIndexFieldTermVisitor func(field uint32, term []byte)
//
// This code originates from:
// func (m *BackIndexRowValue) Unmarshal(data []byte) error
// the sections which create garbage or parse unintersting sections
// the sections which create garbage or parse uninteresting sections
// have been commented out. This was done by design to allow for easier
// merging in the future if that original function is regenerated
func visitBackIndexRow(data []byte, callback backIndexFieldTermVisitor) error {

View File

@@ -30,7 +30,7 @@ import (
index "github.com/blevesearch/bleve_index_api"
store "github.com/blevesearch/upsidedown_store_api"
"github.com/golang/protobuf/proto"
"google.golang.org/protobuf/proto"
)
const Name = "upside_down"

View File

@@ -1,382 +1,319 @@
// Code generated by protoc-gen-gogo.
// source: upsidedown.proto
// DO NOT EDIT!
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v5.29.3
// source: index/upsidedown/upsidedown.proto
/*
Package upsidedown is a generated protocol buffer package.
It is generated from these files:
upsidedown.proto
It has these top-level messages:
BackIndexTermsEntry
BackIndexStoreEntry
BackIndexRowValue
*/
package upsidedown
import proto "github.com/golang/protobuf/proto"
import math "math"
import (
fmt "fmt"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
import io "io"
import fmt "fmt"
import github_com_golang_protobuf_proto "github.com/golang/protobuf/proto"
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = math.Inf
var (
ErrInvalidLengthUpsidedown = fmt.Errorf("proto: negative length found during unmarshaling")
)
type BackIndexTermsEntry struct {
Field *uint32 `protobuf:"varint,1,req,name=field" json:"field,omitempty"`
Terms []string `protobuf:"bytes,2,rep,name=terms" json:"terms,omitempty"`
XXX_unrecognized []byte `json:"-"`
state protoimpl.MessageState `protogen:"open.v1"`
Field *uint32 `protobuf:"varint,1,req,name=field" json:"field,omitempty"`
Terms []string `protobuf:"bytes,2,rep,name=terms" json:"terms,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (m *BackIndexTermsEntry) Reset() { *m = BackIndexTermsEntry{} }
func (m *BackIndexTermsEntry) String() string { return proto.CompactTextString(m) }
func (*BackIndexTermsEntry) ProtoMessage() {}
func (x *BackIndexTermsEntry) Reset() {
*x = BackIndexTermsEntry{}
mi := &file_index_upsidedown_upsidedown_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (m *BackIndexTermsEntry) GetField() uint32 {
if m != nil && m.Field != nil {
return *m.Field
func (x *BackIndexTermsEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BackIndexTermsEntry) ProtoMessage() {}
func (x *BackIndexTermsEntry) ProtoReflect() protoreflect.Message {
mi := &file_index_upsidedown_upsidedown_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BackIndexTermsEntry.ProtoReflect.Descriptor instead.
func (*BackIndexTermsEntry) Descriptor() ([]byte, []int) {
return file_index_upsidedown_upsidedown_proto_rawDescGZIP(), []int{0}
}
func (x *BackIndexTermsEntry) GetField() uint32 {
if x != nil && x.Field != nil {
return *x.Field
}
return 0
}
func (m *BackIndexTermsEntry) GetTerms() []string {
if m != nil {
return m.Terms
func (x *BackIndexTermsEntry) GetTerms() []string {
if x != nil {
return x.Terms
}
return nil
}
func (x *BackIndexTermsEntry) MarshalTo(data []byte) (n int, err error) {
var i int
_ = i
var l int
_ = l
if x.Field == nil {
return 0, fmt.Errorf("missing required `Field`")
} else {
data[i] = 0x8
i++
i = encodeVarintUpsidedown(data, i, uint64(*x.Field))
}
if len(x.Terms) > 0 {
for _, s := range x.Terms {
data[i] = 0x12
i++
l = len(s)
for l >= 1<<7 {
data[i] = uint8(uint64(l)&0x7f | 0x80)
l >>= 7
i++
}
data[i] = uint8(l)
i++
i += copy(data[i:], s)
}
}
return i, nil
}
func (x *BackIndexTermsEntry) Size() (n int) {
var l int
_ = l
if x.Field != nil {
n += 1 + sovUpsidedown(uint64(*x.Field))
}
if len(x.Terms) > 0 {
for _, s := range x.Terms {
l = len(s)
n += 1 + l + sovUpsidedown(uint64(l))
}
}
return n
}
type BackIndexStoreEntry struct {
Field *uint32 `protobuf:"varint,1,req,name=field" json:"field,omitempty"`
ArrayPositions []uint64 `protobuf:"varint,2,rep,name=arrayPositions" json:"arrayPositions,omitempty"`
XXX_unrecognized []byte `json:"-"`
state protoimpl.MessageState `protogen:"open.v1"`
Field *uint32 `protobuf:"varint,1,req,name=field" json:"field,omitempty"`
ArrayPositions []uint64 `protobuf:"varint,2,rep,name=arrayPositions" json:"arrayPositions,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (m *BackIndexStoreEntry) Reset() { *m = BackIndexStoreEntry{} }
func (m *BackIndexStoreEntry) String() string { return proto.CompactTextString(m) }
func (*BackIndexStoreEntry) ProtoMessage() {}
func (x *BackIndexStoreEntry) Reset() {
*x = BackIndexStoreEntry{}
mi := &file_index_upsidedown_upsidedown_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (m *BackIndexStoreEntry) GetField() uint32 {
if m != nil && m.Field != nil {
return *m.Field
func (x *BackIndexStoreEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BackIndexStoreEntry) ProtoMessage() {}
func (x *BackIndexStoreEntry) ProtoReflect() protoreflect.Message {
mi := &file_index_upsidedown_upsidedown_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BackIndexStoreEntry.ProtoReflect.Descriptor instead.
func (*BackIndexStoreEntry) Descriptor() ([]byte, []int) {
return file_index_upsidedown_upsidedown_proto_rawDescGZIP(), []int{1}
}
func (x *BackIndexStoreEntry) GetField() uint32 {
if x != nil && x.Field != nil {
return *x.Field
}
return 0
}
func (m *BackIndexStoreEntry) GetArrayPositions() []uint64 {
if m != nil {
return m.ArrayPositions
func (x *BackIndexStoreEntry) GetArrayPositions() []uint64 {
if x != nil {
return x.ArrayPositions
}
return nil
}
func (x *BackIndexStoreEntry) MarshalTo(data []byte) (n int, err error) {
var i int
_ = i
var l int
_ = l
if x.Field == nil {
return 0, fmt.Errorf("missing required `Field`")
} else {
data[i] = 0x8
i++
i = encodeVarintUpsidedown(data, i, uint64(*x.Field))
}
if len(x.ArrayPositions) > 0 {
for _, num := range x.ArrayPositions {
data[i] = 0x10
i++
i = encodeVarintUpsidedown(data, i, uint64(num))
}
}
return i, nil
}
func (x *BackIndexStoreEntry) Size() (n int) {
var l int
_ = l
if x.Field != nil {
n += 1 + sovUpsidedown(uint64(*x.Field))
}
if len(x.ArrayPositions) > 0 {
for _, e := range x.ArrayPositions {
n += 1 + sovUpsidedown(uint64(e))
}
}
return n
}
type BackIndexRowValue struct {
TermsEntries []*BackIndexTermsEntry `protobuf:"bytes,1,rep,name=termsEntries" json:"termsEntries,omitempty"`
StoredEntries []*BackIndexStoreEntry `protobuf:"bytes,2,rep,name=storedEntries" json:"storedEntries,omitempty"`
XXX_unrecognized []byte `json:"-"`
state protoimpl.MessageState `protogen:"open.v1"`
TermsEntries []*BackIndexTermsEntry `protobuf:"bytes,1,rep,name=termsEntries" json:"termsEntries,omitempty"`
StoredEntries []*BackIndexStoreEntry `protobuf:"bytes,2,rep,name=storedEntries" json:"storedEntries,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (m *BackIndexRowValue) Reset() { *m = BackIndexRowValue{} }
func (m *BackIndexRowValue) String() string { return proto.CompactTextString(m) }
func (*BackIndexRowValue) ProtoMessage() {}
func (m *BackIndexRowValue) GetTermsEntries() []*BackIndexTermsEntry {
if m != nil {
return m.TermsEntries
}
return nil
func (x *BackIndexRowValue) Reset() {
*x = BackIndexRowValue{}
mi := &file_index_upsidedown_upsidedown_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (m *BackIndexRowValue) GetStoredEntries() []*BackIndexStoreEntry {
if m != nil {
return m.StoredEntries
}
return nil
func (x *BackIndexRowValue) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (m *BackIndexTermsEntry) Unmarshal(data []byte) error {
var hasFields [1]uint64
l := len(data)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
func (*BackIndexRowValue) ProtoMessage() {}
func (x *BackIndexRowValue) ProtoReflect() protoreflect.Message {
mi := &file_index_upsidedown_upsidedown_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Field", wireType)
}
var v uint32
for shift := uint(0); ; shift += 7 {
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
v |= (uint32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
m.Field = &v
hasFields[0] |= uint64(0x00000001)
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Terms", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
postIndex := iNdEx + int(stringLen)
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Terms = append(m.Terms, string(data[iNdEx:postIndex]))
iNdEx = postIndex
default:
var sizeOfWire int
for {
sizeOfWire++
wire >>= 7
if wire == 0 {
break
}
}
iNdEx -= sizeOfWire
skippy, err := skipUpsidedown(data[iNdEx:])
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BackIndexRowValue.ProtoReflect.Descriptor instead.
func (*BackIndexRowValue) Descriptor() ([]byte, []int) {
return file_index_upsidedown_upsidedown_proto_rawDescGZIP(), []int{2}
}
func (x *BackIndexRowValue) GetTermsEntries() []*BackIndexTermsEntry {
if x != nil {
return x.TermsEntries
}
return nil
}
func (x *BackIndexRowValue) GetStoredEntries() []*BackIndexStoreEntry {
if x != nil {
return x.StoredEntries
}
return nil
}
func (x *BackIndexRowValue) MarshalTo(data []byte) (n int, err error) {
var i int
_ = i
var l int
_ = l
if len(x.TermsEntries) > 0 {
for _, msg := range x.TermsEntries {
data[i] = 0xa
i++
i = encodeVarintUpsidedown(data, i, uint64(msg.Size()))
n, err := msg.MarshalTo(data[i:])
if err != nil {
return err
return 0, err
}
if skippy < 0 {
return ErrInvalidLengthUpsidedown
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...)
iNdEx += skippy
i += n
}
}
if hasFields[0]&uint64(0x00000001) == 0 {
return new(github_com_golang_protobuf_proto.RequiredNotSetError)
}
return nil
}
func (m *BackIndexStoreEntry) Unmarshal(data []byte) error {
var hasFields [1]uint64
l := len(data)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Field", wireType)
}
var v uint32
for shift := uint(0); ; shift += 7 {
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
v |= (uint32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
m.Field = &v
hasFields[0] |= uint64(0x00000001)
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field ArrayPositions", wireType)
}
var v uint64
for shift := uint(0); ; shift += 7 {
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
v |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
m.ArrayPositions = append(m.ArrayPositions, v)
default:
var sizeOfWire int
for {
sizeOfWire++
wire >>= 7
if wire == 0 {
break
}
}
iNdEx -= sizeOfWire
skippy, err := skipUpsidedown(data[iNdEx:])
if len(x.StoredEntries) > 0 {
for _, msg := range x.StoredEntries {
data[i] = 0x12
i++
i = encodeVarintUpsidedown(data, i, uint64(msg.Size()))
n, err := msg.MarshalTo(data[i:])
if err != nil {
return err
return 0, err
}
if skippy < 0 {
return ErrInvalidLengthUpsidedown
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...)
iNdEx += skippy
i += n
}
}
if hasFields[0]&uint64(0x00000001) == 0 {
return new(github_com_golang_protobuf_proto.RequiredNotSetError)
}
return nil
return i, nil
}
func (m *BackIndexRowValue) Unmarshal(data []byte) error {
l := len(data)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field TermsEntries", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
postIndex := iNdEx + msglen
if msglen < 0 {
return ErrInvalidLengthUpsidedown
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.TermsEntries = append(m.TermsEntries, &BackIndexTermsEntry{})
if err := m.TermsEntries[len(m.TermsEntries)-1].Unmarshal(data[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field StoredEntries", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
postIndex := iNdEx + msglen
if msglen < 0 {
return ErrInvalidLengthUpsidedown
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.StoredEntries = append(m.StoredEntries, &BackIndexStoreEntry{})
if err := m.StoredEntries[len(m.StoredEntries)-1].Unmarshal(data[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
var sizeOfWire int
for {
sizeOfWire++
wire >>= 7
if wire == 0 {
break
}
}
iNdEx -= sizeOfWire
skippy, err := skipUpsidedown(data[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthUpsidedown
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...)
iNdEx += skippy
func (x *BackIndexRowValue) Size() (n int) {
var l int
_ = l
if len(x.TermsEntries) > 0 {
for _, e := range x.TermsEntries {
l = e.Size()
n += 1 + l + sovUpsidedown(uint64(l))
}
}
return nil
if len(x.StoredEntries) > 0 {
for _, e := range x.StoredEntries {
l = e.Size()
n += 1 + l + sovUpsidedown(uint64(l))
}
}
return n
}
func skipUpsidedown(data []byte) (n int, err error) {
l := len(data)
iNdEx := 0
@@ -465,66 +402,6 @@ func skipUpsidedown(data []byte) (n int, err error) {
panic("unreachable")
}
var (
ErrInvalidLengthUpsidedown = fmt.Errorf("proto: negative length found during unmarshaling")
)
func (m *BackIndexTermsEntry) Size() (n int) {
var l int
_ = l
if m.Field != nil {
n += 1 + sovUpsidedown(uint64(*m.Field))
}
if len(m.Terms) > 0 {
for _, s := range m.Terms {
l = len(s)
n += 1 + l + sovUpsidedown(uint64(l))
}
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
return n
}
func (m *BackIndexStoreEntry) Size() (n int) {
var l int
_ = l
if m.Field != nil {
n += 1 + sovUpsidedown(uint64(*m.Field))
}
if len(m.ArrayPositions) > 0 {
for _, e := range m.ArrayPositions {
n += 1 + sovUpsidedown(uint64(e))
}
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
return n
}
func (m *BackIndexRowValue) Size() (n int) {
var l int
_ = l
if len(m.TermsEntries) > 0 {
for _, e := range m.TermsEntries {
l = e.Size()
n += 1 + l + sovUpsidedown(uint64(l))
}
}
if len(m.StoredEntries) > 0 {
for _, e := range m.StoredEntries {
l = e.Size()
n += 1 + l + sovUpsidedown(uint64(l))
}
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
return n
}
func sovUpsidedown(x uint64) (n int) {
for {
n++
@@ -535,150 +412,7 @@ func sovUpsidedown(x uint64) (n int) {
}
return n
}
func sozUpsidedown(x uint64) (n int) {
return sovUpsidedown(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *BackIndexTermsEntry) Marshal() (data []byte, err error) {
size := m.Size()
data = make([]byte, size)
n, err := m.MarshalTo(data)
if err != nil {
return nil, err
}
return data[:n], nil
}
func (m *BackIndexTermsEntry) MarshalTo(data []byte) (n int, err error) {
var i int
_ = i
var l int
_ = l
if m.Field == nil {
return 0, new(github_com_golang_protobuf_proto.RequiredNotSetError)
} else {
data[i] = 0x8
i++
i = encodeVarintUpsidedown(data, i, uint64(*m.Field))
}
if len(m.Terms) > 0 {
for _, s := range m.Terms {
data[i] = 0x12
i++
l = len(s)
for l >= 1<<7 {
data[i] = uint8(uint64(l)&0x7f | 0x80)
l >>= 7
i++
}
data[i] = uint8(l)
i++
i += copy(data[i:], s)
}
}
if m.XXX_unrecognized != nil {
i += copy(data[i:], m.XXX_unrecognized)
}
return i, nil
}
func (m *BackIndexStoreEntry) Marshal() (data []byte, err error) {
size := m.Size()
data = make([]byte, size)
n, err := m.MarshalTo(data)
if err != nil {
return nil, err
}
return data[:n], nil
}
func (m *BackIndexStoreEntry) MarshalTo(data []byte) (n int, err error) {
var i int
_ = i
var l int
_ = l
if m.Field == nil {
return 0, new(github_com_golang_protobuf_proto.RequiredNotSetError)
} else {
data[i] = 0x8
i++
i = encodeVarintUpsidedown(data, i, uint64(*m.Field))
}
if len(m.ArrayPositions) > 0 {
for _, num := range m.ArrayPositions {
data[i] = 0x10
i++
i = encodeVarintUpsidedown(data, i, uint64(num))
}
}
if m.XXX_unrecognized != nil {
i += copy(data[i:], m.XXX_unrecognized)
}
return i, nil
}
func (m *BackIndexRowValue) Marshal() (data []byte, err error) {
size := m.Size()
data = make([]byte, size)
n, err := m.MarshalTo(data)
if err != nil {
return nil, err
}
return data[:n], nil
}
func (m *BackIndexRowValue) MarshalTo(data []byte) (n int, err error) {
var i int
_ = i
var l int
_ = l
if len(m.TermsEntries) > 0 {
for _, msg := range m.TermsEntries {
data[i] = 0xa
i++
i = encodeVarintUpsidedown(data, i, uint64(msg.Size()))
n, err := msg.MarshalTo(data[i:])
if err != nil {
return 0, err
}
i += n
}
}
if len(m.StoredEntries) > 0 {
for _, msg := range m.StoredEntries {
data[i] = 0x12
i++
i = encodeVarintUpsidedown(data, i, uint64(msg.Size()))
n, err := msg.MarshalTo(data[i:])
if err != nil {
return 0, err
}
i += n
}
}
if m.XXX_unrecognized != nil {
i += copy(data[i:], m.XXX_unrecognized)
}
return i, nil
}
func encodeFixed64Upsidedown(data []byte, offset int, v uint64) int {
data[offset] = uint8(v)
data[offset+1] = uint8(v >> 8)
data[offset+2] = uint8(v >> 16)
data[offset+3] = uint8(v >> 24)
data[offset+4] = uint8(v >> 32)
data[offset+5] = uint8(v >> 40)
data[offset+6] = uint8(v >> 48)
data[offset+7] = uint8(v >> 56)
return offset + 8
}
func encodeFixed32Upsidedown(data []byte, offset int, v uint32) int {
data[offset] = uint8(v)
data[offset+1] = uint8(v >> 8)
data[offset+2] = uint8(v >> 16)
data[offset+3] = uint8(v >> 24)
return offset + 4
}
func encodeVarintUpsidedown(data []byte, offset int, v uint64) int {
for v >= 1<<7 {
data[offset] = uint8(v&0x7f | 0x80)
@@ -688,3 +422,70 @@ func encodeVarintUpsidedown(data []byte, offset int, v uint64) int {
data[offset] = uint8(v)
return offset + 1
}
var File_index_upsidedown_upsidedown_proto protoreflect.FileDescriptor
const file_index_upsidedown_upsidedown_proto_rawDesc = "" +
"\n" +
"!index/upsidedown/upsidedown.proto\"A\n" +
"\x13BackIndexTermsEntry\x12\x14\n" +
"\x05field\x18\x01 \x02(\rR\x05field\x12\x14\n" +
"\x05terms\x18\x02 \x03(\tR\x05terms\"S\n" +
"\x13BackIndexStoreEntry\x12\x14\n" +
"\x05field\x18\x01 \x02(\rR\x05field\x12&\n" +
"\x0earrayPositions\x18\x02 \x03(\x04R\x0earrayPositions\"\x89\x01\n" +
"\x11BackIndexRowValue\x128\n" +
"\ftermsEntries\x18\x01 \x03(\v2\x14.BackIndexTermsEntryR\ftermsEntries\x12:\n" +
"\rstoredEntries\x18\x02 \x03(\v2\x14.BackIndexStoreEntryR\rstoredEntries"
var (
file_index_upsidedown_upsidedown_proto_rawDescOnce sync.Once
file_index_upsidedown_upsidedown_proto_rawDescData []byte
)
func file_index_upsidedown_upsidedown_proto_rawDescGZIP() []byte {
file_index_upsidedown_upsidedown_proto_rawDescOnce.Do(func() {
file_index_upsidedown_upsidedown_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_index_upsidedown_upsidedown_proto_rawDesc), len(file_index_upsidedown_upsidedown_proto_rawDesc)))
})
return file_index_upsidedown_upsidedown_proto_rawDescData
}
var file_index_upsidedown_upsidedown_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_index_upsidedown_upsidedown_proto_goTypes = []any{
(*BackIndexTermsEntry)(nil), // 0: BackIndexTermsEntry
(*BackIndexStoreEntry)(nil), // 1: BackIndexStoreEntry
(*BackIndexRowValue)(nil), // 2: BackIndexRowValue
}
var file_index_upsidedown_upsidedown_proto_depIdxs = []int32{
0, // 0: BackIndexRowValue.termsEntries:type_name -> BackIndexTermsEntry
1, // 1: BackIndexRowValue.storedEntries:type_name -> BackIndexStoreEntry
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_index_upsidedown_upsidedown_proto_init() }
func file_index_upsidedown_upsidedown_proto_init() {
if File_index_upsidedown_upsidedown_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_index_upsidedown_upsidedown_proto_rawDesc), len(file_index_upsidedown_upsidedown_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_index_upsidedown_upsidedown_proto_goTypes,
DependencyIndexes: file_index_upsidedown_upsidedown_proto_depIdxs,
MessageInfos: file_index_upsidedown_upsidedown_proto_msgTypes,
}.Build()
File_index_upsidedown_upsidedown_proto = out.File
file_index_upsidedown_upsidedown_proto_goTypes = nil
file_index_upsidedown_upsidedown_proto_depIdxs = nil
}

View File

@@ -32,7 +32,7 @@ type indexAliasImpl struct {
indexes []Index
mutex sync.RWMutex
open bool
// if all the indexes in tha alias have the same mapping
// if all the indexes in that alias have the same mapping
// then the user can set the mapping here to avoid
// checking the mapping of each index in the alias
mapping mapping.IndexMapping
@@ -186,6 +186,7 @@ func (i *indexAliasImpl) SearchInContext(ctx context.Context, req *SearchRequest
if len(i.indexes) < 1 {
return nil, ErrorAliasEmpty
}
if _, ok := ctx.Value(search.PreSearchKey).(bool); ok {
// since preSearchKey is set, it means that the request
// is being executed as part of a preSearch, which
@@ -227,6 +228,21 @@ func (i *indexAliasImpl) SearchInContext(ctx context.Context, req *SearchRequest
return i.indexes[0].SearchInContext(ctx, req)
}
// rescorer will be set if score fusion is supposed to happen
// at this alias (root alias), else will be nil
var rescorer *rescorer
if _, ok := ctx.Value(search.ScoreFusionKey).(bool); !ok {
// new context will be used in internal functions to collect data
// as suitable for fusion. Rescorer is used for rescoring
// using fusion algorithms.
if IsScoreFusionRequested(req) {
ctx = context.WithValue(ctx, search.ScoreFusionKey, true)
rescorer = newRescorer(req)
rescorer.prepareSearchRequest()
defer rescorer.restoreSearchRequest()
}
}
// at this stage we know we have multiple indexes
// check if preSearchData needs to be gathered from all indexes
// before executing the query
@@ -236,6 +252,14 @@ func (i *indexAliasImpl) SearchInContext(ctx context.Context, req *SearchRequest
// - the request requires preSearch
var preSearchDuration time.Duration
var sr *SearchResult
// fusionKnnHits stores the KnnHits at the root alias.
// This is used with score fusion in case there is no need to
// send the knn hits to the leaf indexes in search phase.
// Refer to constructPreSearchDataAndFusionKnnHits for more info.
// This variable is left nil if we have to send the knn hits to leaf
// indexes again, else contains the knn hits if not required.
var fusionKnnHits search.DocumentMatchCollection
flags, err := preSearchRequired(ctx, req, i.mapping)
if err != nil {
return nil, err
@@ -261,10 +285,10 @@ func (i *indexAliasImpl) SearchInContext(ctx context.Context, req *SearchRequest
// if the request is satisfied by the preSearch result, then we can
// directly return the preSearch result as the final result
if requestSatisfiedByPreSearch(req, flags) {
sr = finalizeSearchResult(req, preSearchResult)
sr = finalizeSearchResult(ctx, req, preSearchResult, rescorer)
// no need to run the 2nd phase MultiSearch(..)
} else {
preSearchData, err = constructPreSearchData(req, flags, preSearchResult, i.indexes)
preSearchData, fusionKnnHits, err = constructPreSearchDataAndFusionKnnHits(req, flags, preSearchResult, rescorer, i.indexes)
if err != nil {
return nil, err
}
@@ -274,7 +298,8 @@ func (i *indexAliasImpl) SearchInContext(ctx context.Context, req *SearchRequest
// check if search result was generated as part of preSearch itself
if sr == nil {
sr, err = MultiSearch(ctx, req, preSearchData, i.indexes...)
multiSearchParams := &multiSearchParams{preSearchData, rescorer, fusionKnnHits}
sr, err = MultiSearch(ctx, req, multiSearchParams, i.indexes...)
if err != nil {
return nil, err
}
@@ -653,7 +678,7 @@ func preSearch(ctx context.Context, req *SearchRequest, flags *preSearchFlags, i
// if the request is satisfied by just the preSearch result,
// finalize the result and return it directly without
// performing multi search
func finalizeSearchResult(req *SearchRequest, preSearchResult *SearchResult) *SearchResult {
func finalizeSearchResult(ctx context.Context, req *SearchRequest, preSearchResult *SearchResult, rescorer *rescorer) *SearchResult {
if preSearchResult == nil {
return nil
}
@@ -682,7 +707,16 @@ func finalizeSearchResult(req *SearchRequest, preSearchResult *SearchResult) *Se
if req.SearchAfter != nil {
preSearchResult.Hits = collector.FilterHitsBySearchAfter(preSearchResult.Hits, req.Sort, req.SearchAfter)
}
if rescorer != nil {
// rescore takes ftsHits and knnHits as first and second argument respectively
// since this is pure knn, set ftsHits to nil. preSearchResult.Hits contains knn results
preSearchResult.Hits, preSearchResult.Total, preSearchResult.MaxScore = rescorer.rescore(nil, preSearchResult.Hits)
rescorer.restoreSearchRequest()
}
preSearchResult.Hits = hitsInCurrentPage(req, preSearchResult.Hits)
if reverseQueryExecution {
// reverse the sort back to the original
req.Sort.Reverse()
@@ -759,6 +793,31 @@ func constructPreSearchData(req *SearchRequest, flags *preSearchFlags,
return mergedOut, nil
}
// Constructs the presearch data if required during the search phase.
// Also if we need to store knn hits at alias.
// If we need to store knn hits at alias: returns all the knn hits
// If we should send it to leaf indexes: includes in presearch data
func constructPreSearchDataAndFusionKnnHits(req *SearchRequest, flags *preSearchFlags,
preSearchResult *SearchResult, rescorer *rescorer, indexes []Index,
) (map[string]map[string]interface{}, search.DocumentMatchCollection, error) {
var fusionknnhits search.DocumentMatchCollection
// Checks if we need to send the KNN hits to the indexes in the
// search phase. If there is score fusion enabled, we do not
// send the KNN hits to the indexes.
if rescorer != nil && flags.knn {
fusionknnhits = preSearchResult.Hits
preSearchResult.Hits = nil
}
preSearchData, err := constructPreSearchData(req, flags, preSearchResult, indexes)
if err != nil {
return nil, nil, err
}
return preSearchData, fusionknnhits, nil
}
func preSearchDataSearch(ctx context.Context, req *SearchRequest, flags *preSearchFlags, indexes ...Index) (*SearchResult, error) {
asyncResults := make(chan *asyncSearchResult, len(indexes))
// run search on each index in separate go routine
@@ -912,9 +971,16 @@ func hitsInCurrentPage(req *SearchRequest, hits []*search.DocumentMatch) []*sear
return hits
}
// Extra parameters for MultiSearch
type multiSearchParams struct {
preSearchData map[string]map[string]interface{}
rescorer *rescorer
fusionKnnHits search.DocumentMatchCollection
}
// MultiSearch executes a SearchRequest across multiple Index objects,
// then merges the results. The indexes must honor any ctx deadline.
func MultiSearch(ctx context.Context, req *SearchRequest, preSearchData map[string]map[string]interface{}, indexes ...Index) (*SearchResult, error) {
func MultiSearch(ctx context.Context, req *SearchRequest, params *multiSearchParams, indexes ...Index) (*SearchResult, error) {
searchStart := time.Now()
asyncResults := make(chan *asyncSearchResult, len(indexes))
@@ -939,8 +1005,8 @@ func MultiSearch(ctx context.Context, req *SearchRequest, preSearchData map[stri
waitGroup.Add(len(indexes))
for _, in := range indexes {
var payload map[string]interface{}
if preSearchData != nil {
payload = preSearchData[in.Name()]
if params.preSearchData != nil {
payload = params.preSearchData[in.Name()]
}
go searchChildIndex(in, createChildSearchRequest(req, payload))
}
@@ -980,6 +1046,11 @@ func MultiSearch(ctx context.Context, req *SearchRequest, preSearchData map[stri
}
}
if params.rescorer != nil {
sr.Hits, sr.Total, sr.MaxScore = params.rescorer.rescore(sr.Hits, params.fusionKnnHits)
params.rescorer.restoreSearchRequest()
}
sr.Hits = hitsInCurrentPage(req, sr.Hits)
// fix up facets

View File

@@ -133,7 +133,7 @@ func newIndexUsing(path string, mapping mapping.IndexMapping, indexType string,
if err != nil {
return nil, err
}
err = rv.i.SetInternal(mappingInternalKey, mappingBytes)
err = rv.i.SetInternal(util.MappingInternalKey, mappingBytes)
if err != nil {
return nil, err
}
@@ -163,6 +163,9 @@ func openIndexUsing(path string, runtimeConfig map[string]interface{}) (rv *inde
rv.meta.IndexType = upsidedown.Name
}
var um *mapping.IndexMappingImpl
var umBytes []byte
storeConfig := rv.meta.Config
if storeConfig == nil {
storeConfig = map[string]interface{}{}
@@ -173,6 +176,21 @@ func openIndexUsing(path string, runtimeConfig map[string]interface{}) (rv *inde
storeConfig["error_if_exists"] = false
for rck, rcv := range runtimeConfig {
storeConfig[rck] = rcv
if rck == "updated_mapping" {
if val, ok := rcv.(string); ok {
if len(val) == 0 {
return nil, fmt.Errorf("updated_mapping is empty")
}
umBytes = []byte(val)
err = util.UnmarshalJSON(umBytes, &um)
if err != nil {
return nil, fmt.Errorf("error parsing updated_mapping into JSON: %v\nmapping contents:\n%v", err, rck)
}
} else {
return nil, fmt.Errorf("updated_mapping not of type string")
}
}
}
// open the index
@@ -185,15 +203,32 @@ func openIndexUsing(path string, runtimeConfig map[string]interface{}) (rv *inde
if err != nil {
return nil, err
}
err = rv.i.Open()
if err != nil {
return nil, err
}
defer func(rv *indexImpl) {
if !rv.open {
rv.i.Close()
var ui index.UpdateIndex
if um != nil {
var ok bool
ui, ok = rv.i.(index.UpdateIndex)
if !ok {
return nil, fmt.Errorf("updated mapping present for unupdatable index")
}
}(rv)
// Load the meta data from bolt so that we can read the current index
// mapping to compare with
err = ui.OpenMeta()
if err != nil {
return nil, err
}
} else {
err = rv.i.Open()
if err != nil {
return nil, err
}
defer func(rv *indexImpl) {
if !rv.open {
rv.i.Close()
}
}(rv)
}
// now load the mapping
indexReader, err := rv.i.Reader()
@@ -206,7 +241,7 @@ func openIndexUsing(path string, runtimeConfig map[string]interface{}) (rv *inde
}
}()
mappingBytes, err := indexReader.GetInternal(mappingInternalKey)
mappingBytes, err := indexReader.GetInternal(util.MappingInternalKey)
if err != nil {
return nil, err
}
@@ -217,19 +252,48 @@ func openIndexUsing(path string, runtimeConfig map[string]interface{}) (rv *inde
return nil, fmt.Errorf("error parsing mapping JSON: %v\nmapping contents:\n%s", err, string(mappingBytes))
}
// validate the mapping
err = im.Validate()
if err != nil {
// no longer return usable index on error because there
// is a chance the index is not open at this stage
return nil, err
}
// Validate and update the index with the new mapping
if um != nil && ui != nil {
err = um.Validate()
if err != nil {
return nil, err
}
fieldInfo, err := DeletedFields(im, um)
if err != nil {
return nil, err
}
err = ui.UpdateFields(fieldInfo, umBytes)
if err != nil {
return nil, err
}
im = um
err = rv.i.Open()
if err != nil {
return nil, err
}
defer func(rv *indexImpl) {
if !rv.open {
rv.i.Close()
}
}(rv)
}
// mark the index as open
rv.mutex.Lock()
defer rv.mutex.Unlock()
rv.open = true
// validate the mapping
err = im.Validate()
if err != nil {
// note even if the mapping is invalid
// we still return an open usable index
return rv, err
}
rv.m = im
indexStats.Register(rv)
return rv, err
@@ -562,6 +626,21 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
}
}()
// rescorer will be set if score fusion is supposed to happen
// at this alias (root alias), else will be nil
var rescorer *rescorer
if _, ok := ctx.Value(search.ScoreFusionKey).(bool); !ok {
// new context will be used in internal functions to collect data
// as suitable for hybrid search. Rescorer is used for rescoring
// using fusion algorithms.
if IsScoreFusionRequested(req) {
ctx = context.WithValue(ctx, search.ScoreFusionKey, true)
rescorer = newRescorer(req)
rescorer.prepareSearchRequest()
defer rescorer.restoreSearchRequest()
}
}
if _, ok := ctx.Value(search.PreSearchKey).(bool); ok {
preSearchResult, err := i.preSearch(ctx, req, indexReader)
if err != nil {
@@ -632,10 +711,21 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
}
}
}
if !skipKNNCollector && requestHasKNN(req) {
knnHits, err = i.runKnnCollector(ctx, req, indexReader, false)
if err != nil {
return nil, err
_, contextScoreFusionKeyExists := ctx.Value(search.ScoreFusionKey).(bool)
if !contextScoreFusionKeyExists {
// if no score fusion, default behaviour
if !skipKNNCollector && requestHasKNN(req) {
knnHits, err = i.runKnnCollector(ctx, req, indexReader, false)
if err != nil {
return nil, err
}
}
} else {
// if score fusion, run collect if rescorer is defined
if rescorer != nil && requestHasKNN(req) {
knnHits, err = i.runKnnCollector(ctx, req, indexReader, false)
}
}
@@ -650,7 +740,12 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
}
}
setKnnHitsInCollector(knnHits, req, coll)
// if score fusion, no faceting for knn hits is done
// hence we can skip setting the knn hits in the collector
if !contextScoreFusionKeyExists {
setKnnHitsInCollector(knnHits, req, coll)
}
if fts != nil {
if is, ok := indexReader.(*scorch.IndexSnapshot); ok {
@@ -859,6 +954,13 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
Facets: coll.FacetResults(),
}
// rescore if fusion flag is set
if rescorer != nil {
rv.Hits, rv.Total, rv.MaxScore = rescorer.rescore(rv.Hits, knnHits)
rescorer.restoreSearchRequest()
rv.Hits = hitsInCurrentPage(req, rv.Hits)
}
if req.Explain {
rv.Request = req
}

595
vendor/github.com/blevesearch/bleve/v2/index_update.go generated vendored Normal file
View File

@@ -0,0 +1,595 @@
// Copyright (c) 2025 Couchbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bleve
import (
"fmt"
"reflect"
"github.com/blevesearch/bleve/v2/mapping"
index "github.com/blevesearch/bleve_index_api"
)
// Store all the fields that interact with the data
// from a document path
type pathInfo struct {
fieldMapInfo []*fieldMapInfo
dynamic bool
path string
analyser string
parentPath string
}
// Store the field information with respect to the
// document paths
type fieldMapInfo struct {
fieldMapping *mapping.FieldMapping
analyzer string
datetimeParser string
rootName string
parent *pathInfo
}
// Compare two index mappings to identify all of the updatable changes
func DeletedFields(ori, upd *mapping.IndexMappingImpl) (map[string]*index.UpdateFieldInfo, error) {
// Compare all of the top level fields in an index mapping
err := compareMappings(ori, upd)
if err != nil {
return nil, err
}
// Check for new mappings present in the type mappings
// of the updated compared to the original
for name, updDMapping := range upd.TypeMapping {
err = checkUpdatedMapping(ori.TypeMapping[name], updDMapping)
if err != nil {
return nil, err
}
}
// Check for new mappings present in the default mappings
// of the updated compared to the original
err = checkUpdatedMapping(ori.DefaultMapping, upd.DefaultMapping)
if err != nil {
return nil, err
}
oriPaths := make(map[string]*pathInfo)
updPaths := make(map[string]*pathInfo)
// Go through each mapping present in the original
// and consolidate according to the document paths
for name, oriDMapping := range ori.TypeMapping {
addPathInfo(oriPaths, "", oriDMapping, ori, nil, name)
}
addPathInfo(oriPaths, "", ori.DefaultMapping, ori, nil, "")
// Go through each mapping present in the updated
// and consolidate according to the document paths
for name, updDMapping := range upd.TypeMapping {
addPathInfo(updPaths, "", updDMapping, upd, nil, name)
}
addPathInfo(updPaths, "", upd.DefaultMapping, upd, nil, "")
// Compare all components of custom analysis currently in use
err = compareCustomComponents(oriPaths, updPaths, ori, upd)
if err != nil {
return nil, err
}
// Compare both the mappings based on the document paths
// and create a list of index, docvalues, store differences
// for every single field possible
fieldInfo := make(map[string]*index.UpdateFieldInfo)
for path, info := range oriPaths {
err = addFieldInfo(fieldInfo, info, updPaths[path])
if err != nil {
return nil, err
}
}
// Remove entries from the list with no changes between the
// original and the updated mapping
for name, info := range fieldInfo {
if !info.Deleted && !info.Index && !info.DocValues && !info.Store {
delete(fieldInfo, name)
}
// A field cannot be completely deleted with any dynamic value turned on
if info.Deleted {
if upd.IndexDynamic {
return nil, fmt.Errorf("Mapping cannot be removed when index dynamic is true")
}
if upd.StoreDynamic {
return nil, fmt.Errorf("Mapping cannot be removed when store dynamic is true")
}
if upd.DocValuesDynamic {
return nil, fmt.Errorf("Mapping cannot be removed when docvalues dynamic is true")
}
}
}
return fieldInfo, nil
}
// Ensures none of the top level index mapping fields have changed
func compareMappings(ori, upd *mapping.IndexMappingImpl) error {
if ori.TypeField != upd.TypeField &&
(len(ori.TypeMapping) != 0 || len(upd.TypeMapping) != 0) {
return fmt.Errorf("type field cannot be changed when type mappings are present")
}
if ori.DefaultType != upd.DefaultType {
return fmt.Errorf("default type cannot be changed")
}
if ori.IndexDynamic != upd.IndexDynamic {
return fmt.Errorf("index dynamic cannot be changed")
}
if ori.StoreDynamic != upd.StoreDynamic {
return fmt.Errorf("store dynamic cannot be changed")
}
if ori.DocValuesDynamic != upd.DocValuesDynamic {
return fmt.Errorf("docvalues dynamic cannot be changed")
}
if ori.DefaultAnalyzer != upd.DefaultAnalyzer && upd.IndexDynamic {
return fmt.Errorf("default analyser cannot be changed if index dynamic is true")
}
if ori.DefaultDateTimeParser != upd.DefaultDateTimeParser && upd.IndexDynamic {
return fmt.Errorf("default datetime parser cannot be changed if index dynamic is true")
}
// Scoring model changes between "", "tf-idf" and "bm25" require no index changes to be made
if ori.ScoringModel != upd.ScoringModel {
if ori.ScoringModel != "" && ori.ScoringModel != index.TFIDFScoring && ori.ScoringModel != index.BM25Scoring ||
upd.ScoringModel != "" && upd.ScoringModel != index.TFIDFScoring && upd.ScoringModel != index.BM25Scoring {
return fmt.Errorf("scoring model can only be changed between \"\", %q and %q", index.TFIDFScoring, index.BM25Scoring)
}
}
return nil
}
// Ensures updated document mapping does not contain new
// field mappings or document mappings
func checkUpdatedMapping(ori, upd *mapping.DocumentMapping) error {
// Check to verify both original and updated are not nil
// and are enabled before proceeding
if ori == nil {
if upd == nil || !upd.Enabled {
return nil
}
return fmt.Errorf("updated index mapping contains new properties")
}
if upd == nil || !upd.Enabled {
return nil
}
var err error
// Recursively go through the child mappings
for name, updDMapping := range upd.Properties {
err = checkUpdatedMapping(ori.Properties[name], updDMapping)
if err != nil {
return err
}
}
// Simple checks to ensure no new field mappings present
// in updated
for _, updFMapping := range upd.Fields {
var oriFMapping *mapping.FieldMapping
for _, fMapping := range ori.Fields {
if updFMapping.Name == fMapping.Name {
oriFMapping = fMapping
}
}
if oriFMapping == nil {
return fmt.Errorf("updated index mapping contains new fields")
}
}
return nil
}
// Adds all of the field mappings while maintaining a tree of the document structure
// to ensure traversal and verification is possible incase of multiple mappings defined
// for a single field or multiple document fields' data getting written to a single zapx field
func addPathInfo(paths map[string]*pathInfo, name string, mp *mapping.DocumentMapping,
im *mapping.IndexMappingImpl, parent *pathInfo, rootName string) {
// Early exit if mapping has been disabled
// Comparisions later on will be done with a nil object
if !mp.Enabled {
return
}
// Consolidate path information like index dynamic across multiple
// mappings if path is the same
var pInfo *pathInfo
if val, ok := paths[name]; ok {
pInfo = val
} else {
pInfo = &pathInfo{
fieldMapInfo: make([]*fieldMapInfo, 0),
}
pInfo.dynamic = mp.Dynamic && im.IndexDynamic
pInfo.analyser = im.AnalyzerNameForPath(name)
}
pInfo.dynamic = (pInfo.dynamic || mp.Dynamic) && im.IndexDynamic
pInfo.path = name
if parent != nil {
pInfo.parentPath = parent.path
}
// Recursively add path information for all child mappings
for cName, cMapping := range mp.Properties {
var pathName string
if name == "" {
pathName = cName
} else {
pathName = name + "." + cName
}
addPathInfo(paths, pathName, cMapping, im, pInfo, rootName)
}
// Add field mapping information keeping the document structure intact
for _, fMap := range mp.Fields {
fieldMapInfo := &fieldMapInfo{
fieldMapping: fMap,
rootName: rootName,
parent: pInfo,
}
pInfo.fieldMapInfo = append(pInfo.fieldMapInfo, fieldMapInfo)
}
paths[name] = pInfo
}
// Compares all of the custom analysis components in use
func compareCustomComponents(oriPaths, updPaths map[string]*pathInfo, ori, upd *mapping.IndexMappingImpl) error {
// Compare all analysers currently in use
err := compareAnalysers(oriPaths, updPaths, ori, upd)
if err != nil {
return err
}
// Compare all datetime parsers currently in use
err = compareDateTimeParsers(oriPaths, updPaths, ori, upd)
if err != nil {
return err
}
// Compare all synonum sources
err = compareSynonymSources(ori, upd)
if err != nil {
return err
}
// Compare all char filters, tokenizers, token filters and token maps
err = compareAnalyserSubcomponents(ori, upd)
if err != nil {
return err
}
return nil
}
// Compares all analysers currently in use
// Standard analysers not in custom analysis are not compared
// Analysers in custom analysis but not in use are not compared
func compareAnalysers(oriPaths, updPaths map[string]*pathInfo, ori, upd *mapping.IndexMappingImpl) error {
oriAnalyzers := make(map[string]interface{})
updAnalyzers := make(map[string]interface{})
extractAnalyzers := func(paths map[string]*pathInfo, customAnalyzers map[string]map[string]interface{},
analyzers map[string]interface{}, indexMapping *mapping.IndexMappingImpl) {
for path, info := range paths {
for _, fInfo := range info.fieldMapInfo {
if fInfo.fieldMapping.Type == "text" {
analyzerName := indexMapping.AnalyzerNameForPath(path)
fInfo.analyzer = analyzerName
if val, ok := customAnalyzers[analyzerName]; ok {
analyzers[analyzerName] = val
}
}
}
}
}
extractAnalyzers(oriPaths, ori.CustomAnalysis.Analyzers, oriAnalyzers, ori)
extractAnalyzers(updPaths, upd.CustomAnalysis.Analyzers, updAnalyzers, upd)
for name, anUpd := range updAnalyzers {
if anOri, ok := oriAnalyzers[name]; ok {
if !reflect.DeepEqual(anUpd, anOri) {
return fmt.Errorf("analyser %s changed while being used by fields", name)
}
} else {
return fmt.Errorf("analyser %s newly added to an existing field", name)
}
}
return nil
}
// Compares all date time parsers currently in use
// Date time parsers in custom analysis but not in use are not compared
func compareDateTimeParsers(oriPaths, updPaths map[string]*pathInfo, ori, upd *mapping.IndexMappingImpl) error {
oriDateTimeParsers := make(map[string]interface{})
updDateTimeParsers := make(map[string]interface{})
extractDateTimeParsers := func(paths map[string]*pathInfo, customParsers map[string]map[string]interface{},
parsers map[string]interface{}, indexMapping *mapping.IndexMappingImpl) {
for _, info := range paths {
for _, fInfo := range info.fieldMapInfo {
if fInfo.fieldMapping.Type == "datetime" {
parserName := fInfo.fieldMapping.DateFormat
if parserName == "" {
parserName = indexMapping.DefaultDateTimeParser
}
fInfo.datetimeParser = parserName
if val, ok := customParsers[parserName]; ok {
parsers[parserName] = val
}
}
}
}
}
extractDateTimeParsers(oriPaths, ori.CustomAnalysis.DateTimeParsers, oriDateTimeParsers, ori)
extractDateTimeParsers(updPaths, upd.CustomAnalysis.DateTimeParsers, updDateTimeParsers, upd)
for name, dtUpd := range updDateTimeParsers {
if dtOri, ok := oriDateTimeParsers[name]; ok {
if !reflect.DeepEqual(dtUpd, dtOri) {
return fmt.Errorf("datetime parser %s changed while being used by fields", name)
}
} else {
return fmt.Errorf("datetime parser %s added to an existing field", name)
}
}
return nil
}
// Compares all synonym sources
// Synonym sources currently not in use are also compared
func compareSynonymSources(ori, upd *mapping.IndexMappingImpl) error {
if !reflect.DeepEqual(ori.CustomAnalysis.SynonymSources, upd.CustomAnalysis.SynonymSources) {
return fmt.Errorf("synonym sources cannot be changed")
}
return nil
}
// Compares all char filters, tokenizers, token filters and token maps
// Components not currently in use are also compared
func compareAnalyserSubcomponents(ori, upd *mapping.IndexMappingImpl) error {
if !reflect.DeepEqual(ori.CustomAnalysis.CharFilters, upd.CustomAnalysis.CharFilters) {
return fmt.Errorf("char filters cannot be changed")
}
if !reflect.DeepEqual(ori.CustomAnalysis.TokenFilters, upd.CustomAnalysis.TokenFilters) {
return fmt.Errorf("token filters cannot be changed")
}
if !reflect.DeepEqual(ori.CustomAnalysis.TokenMaps, upd.CustomAnalysis.TokenMaps) {
return fmt.Errorf("token maps cannot be changed")
}
if !reflect.DeepEqual(ori.CustomAnalysis.Tokenizers, upd.CustomAnalysis.Tokenizers) {
return fmt.Errorf("tokenizers cannot be changed")
}
return nil
}
// Compare all of the fields at a particular document path and add its field information
func addFieldInfo(fInfo map[string]*index.UpdateFieldInfo, ori, upd *pathInfo) error {
var info *index.UpdateFieldInfo
var err error
// Assume deleted or disabled mapping if upd is nil. Checks for ori being nil
// or upd having mappings not in orihave already been done before this stage
if upd == nil {
for _, oriFMapInfo := range ori.fieldMapInfo {
info, err = compareFieldMapping(oriFMapInfo.fieldMapping, nil)
if err != nil {
return err
}
err = validateFieldInfo(info, fInfo, ori, oriFMapInfo)
if err != nil {
return err
}
}
} else {
if upd.dynamic && ori.analyser != upd.analyser {
return fmt.Errorf("analyser has been changed for a dynamic mapping")
}
for _, oriFMapInfo := range ori.fieldMapInfo {
var updFMap *mapping.FieldMapping
var updAnalyser string
var updDatetimeParser string
// For multiple fields at a single document path, compare
// only with the matching ones
for _, updFMapInfo := range upd.fieldMapInfo {
if oriFMapInfo.rootName == updFMapInfo.rootName &&
oriFMapInfo.fieldMapping.Name == updFMapInfo.fieldMapping.Name {
updFMap = updFMapInfo.fieldMapping
if updFMap.Type == "text" {
updAnalyser = updFMapInfo.analyzer
} else if updFMap.Type == "datetime" {
updDatetimeParser = updFMapInfo.datetimeParser
}
}
}
// Compare analyser, datetime parser and synonym source before comparing
// the field mapping as it might not have this information
if updAnalyser != "" && oriFMapInfo.analyzer != updAnalyser {
return fmt.Errorf("analyser has been changed for a text field")
}
if updDatetimeParser != "" && oriFMapInfo.datetimeParser != updDatetimeParser {
return fmt.Errorf("datetime parser has been changed for a date time field")
}
info, err = compareFieldMapping(oriFMapInfo.fieldMapping, updFMap)
if err != nil {
return err
}
// Validate to ensure change is possible
// Needed if multiple mappings are aliased to the same field
err = validateFieldInfo(info, fInfo, ori, oriFMapInfo)
if err != nil {
return err
}
}
}
if err != nil {
return err
}
return nil
}
// Compares two field mappings against each other, checking for changes in index, store, doc values
// and complete deletiion of the mapping while noting that the changes made are doable based on
// other values like includeInAll and dynamic
// first return argument gives an empty fieldInfo if no changes detected
// second return argument gives a flag indicating whether any changes, if detected, are doable or if
// update is impossible
// third argument is an error explaining exactly why the change is not possible
func compareFieldMapping(original, updated *mapping.FieldMapping) (*index.UpdateFieldInfo, error) {
rv := &index.UpdateFieldInfo{}
if updated == nil {
if original != nil && !original.IncludeInAll {
rv.Deleted = true
return rv, nil
} else if original == nil {
return nil, fmt.Errorf("both field mappings cannot be nil")
}
return nil, fmt.Errorf("deleted field present in '_all' field")
} else if original == nil {
return nil, fmt.Errorf("matching field not found in original index mapping")
}
if original.Type != updated.Type {
return nil, fmt.Errorf("field type cannot be updated")
}
if original.Type == "text" {
if original.Analyzer != updated.Analyzer {
return nil, fmt.Errorf("analyzer cannot be updated for text fields")
}
}
if original.Type == "datetime" {
if original.DateFormat != updated.DateFormat {
return nil, fmt.Errorf("dateFormat cannot be updated for datetime fields")
}
}
if original.Type == "vector" || original.Type == "vector_base64" {
if original.Dims != updated.Dims {
return nil, fmt.Errorf("dimensions cannot be updated for vector and vector_base64 fields")
}
if original.Similarity != updated.Similarity {
return nil, fmt.Errorf("similarity cannot be updated for vector and vector_base64 fields")
}
if original.VectorIndexOptimizedFor != updated.VectorIndexOptimizedFor {
return nil, fmt.Errorf("vectorIndexOptimizedFor cannot be updated for vector and vector_base64 fields")
}
}
if original.IncludeInAll != updated.IncludeInAll {
return nil, fmt.Errorf("includeInAll cannot be changed")
}
if original.IncludeTermVectors != updated.IncludeTermVectors {
return nil, fmt.Errorf("includeTermVectors cannot be changed")
}
if original.SkipFreqNorm != updated.SkipFreqNorm {
return nil, fmt.Errorf("skipFreqNorm cannot be changed")
}
// Updating is not possible if store changes from true
// to false when the field is included in _all
if original.Store != updated.Store {
if updated.Store {
return nil, fmt.Errorf("store cannot be changed from false to true")
} else if updated.IncludeInAll {
return nil, fmt.Errorf("store cannot be changed if field present in `_all' field")
} else {
rv.Store = true
}
}
// Updating is not possible if index changes from true
// to false when the field is included in _all
if original.Index != updated.Index {
if updated.Index {
return nil, fmt.Errorf("index cannot be changed from false to true")
} else if updated.IncludeInAll {
return nil, fmt.Errorf("index cannot be changed if field present in `_all' field")
} else {
rv.Index = true
rv.DocValues = true
}
}
// Updating is not possible if docvalues changes from true
// to false when the field is included in _all
if original.DocValues != updated.DocValues {
if updated.DocValues {
return nil, fmt.Errorf("docvalues cannot be changed from false to true")
} else if updated.IncludeInAll {
return nil, fmt.Errorf("docvalues cannot be changed if field present in `_all' field")
} else {
rv.DocValues = true
}
}
return rv, nil
}
// After identifying changes, validate against the existing changes incase of duplicate fields.
// In such a situation, any conflicting changes found will abort the update process
func validateFieldInfo(newInfo *index.UpdateFieldInfo, fInfo map[string]*index.UpdateFieldInfo,
ori *pathInfo, oriFMapInfo *fieldMapInfo) error {
var name string
if oriFMapInfo.parent.parentPath == "" {
if oriFMapInfo.fieldMapping.Name == "" {
name = oriFMapInfo.parent.path
} else {
name = oriFMapInfo.fieldMapping.Name
}
} else {
if oriFMapInfo.fieldMapping.Name == "" {
name = oriFMapInfo.parent.parentPath + "." + oriFMapInfo.parent.path
} else {
name = oriFMapInfo.parent.parentPath + "." + oriFMapInfo.fieldMapping.Name
}
}
if (newInfo.Deleted || newInfo.Index || newInfo.DocValues || newInfo.Store) && ori.dynamic {
return fmt.Errorf("updated field is under a dynamic property")
}
if oldInfo, ok := fInfo[name]; ok {
if !reflect.DeepEqual(oldInfo, newInfo) {
return fmt.Errorf("updated field impossible to verify because multiple mappings point to the same field name")
}
} else {
fInfo[name] = newInfo
}
return nil
}

View File

@@ -13,7 +13,7 @@ var interleaveMagic = []uint64{
var interleaveShift = []uint{1, 2, 4, 8, 16}
// Interleave the first 32 bits of each uint64
// apdated from org.apache.lucene.util.BitUtil
// adapted from org.apache.lucene.util.BitUtil
// which was adapted from:
// http://graphics.stanford.edu/~seander/bithacks.html#InterleaveBMN
func Interleave(v1, v2 uint64) uint64 {

View File

@@ -30,7 +30,7 @@ func RegisterKVStore(name string, constructor KVStoreConstructor) error {
}
// KVStoreConstructor is used to build a KVStore of a specific type when
// specificied by the index configuration. In addition to meeting the
// specified by the index configuration. In addition to meeting the
// store.KVStore interface, KVStores must also support this constructor.
// Note that currently the values of config must
// be able to be marshaled and unmarshaled using the encoding/json library (used

162
vendor/github.com/blevesearch/bleve/v2/rescorer.go generated vendored Normal file
View File

@@ -0,0 +1,162 @@
// Copyright (c) 2025 Couchbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bleve
import (
"github.com/blevesearch/bleve/v2/fusion"
"github.com/blevesearch/bleve/v2/search"
"github.com/blevesearch/bleve/v2/search/query"
)
const (
DefaultScoreRankConstant = 60
)
// Rescorer is applied after all the query and knn results are obtained.
// The main use of Rescorer is in hybrid search; all the individual scores
// for query and knn are combined using Rescorer. Makes use of algorithms
// defined in `fusion`
type rescorer struct {
req *SearchRequest
// Stores the original From, Size and Boost parameters from the request
origFrom int
origSize int
origBoosts []float64
// Flag variable to make sure that restoreSearchRequest is only run once
// when it is deferred
restored bool
}
// Stores information about the hybrid search into FusionRescorer.
// Also mutates the SearchRequest by:
// - Setting boosts to 1: top level boosts only used for rescoring
// - Setting From and Size to 0 and ScoreWindowSize
func (r *rescorer) prepareSearchRequest() error {
if r.req.Params == nil {
r.req.Params = NewDefaultParams(r.req.From, r.req.Size)
}
r.origFrom = r.req.From
r.origSize = r.req.Size
r.req.From = 0
r.req.Size = r.req.Params.ScoreWindowSize
// req.Query's top level boost comes first, followed by the KNN queries
numQueries := numKNNQueries(r.req) + 1
r.origBoosts = make([]float64, numQueries)
// only modify queries if it is boostable. If not, ignore
if bQuery, ok := r.req.Query.(query.BoostableQuery); ok {
r.origBoosts[0] = bQuery.Boost()
bQuery.SetBoost(1.0)
} else {
r.origBoosts[0] = 1.0
}
// for all the knn queries, replace boost values
r.prepareKnnRequest()
return nil
}
func (r *rescorer) restoreSearchRequest() {
// Skip if already restored
if r.restored {
return
}
r.restored = true
r.req.From = r.origFrom
r.req.Size = r.origSize
if bQuery, ok := r.req.Query.(query.BoostableQuery); ok {
bQuery.SetBoost(r.origBoosts[0])
}
// for all the knn queries, restore boost values
r.restoreKnnRequest()
}
func (r *rescorer) rescore(ftsHits, knnHits search.DocumentMatchCollection) (search.DocumentMatchCollection, uint64, float64) {
mergedHits := r.mergeDocs(ftsHits, knnHits)
var fusionResult *fusion.FusionResult
switch r.req.Score {
case ScoreRRF:
res := fusion.ReciprocalRankFusion(
mergedHits,
r.origBoosts,
r.req.Params.ScoreRankConstant,
r.req.Params.ScoreWindowSize,
numKNNQueries(r.req),
r.req.Explain,
)
fusionResult = &res
case ScoreRSF:
res := fusion.RelativeScoreFusion(
mergedHits,
r.origBoosts,
r.req.Params.ScoreWindowSize,
numKNNQueries(r.req),
r.req.Explain,
)
fusionResult = &res
}
return fusionResult.Hits, fusionResult.Total, fusionResult.MaxScore
}
// Merge all the FTS and KNN docs along with explanations
func (r *rescorer) mergeDocs(ftsHits, knnHits search.DocumentMatchCollection) search.DocumentMatchCollection {
if len(knnHits) == 0 {
return ftsHits
}
knnHitMap := make(map[string]*search.DocumentMatch, len(knnHits))
for _, hit := range knnHits {
knnHitMap[hit.ID] = hit
}
for _, hit := range ftsHits {
if knnHit, ok := knnHitMap[hit.ID]; ok {
hit.ScoreBreakdown = knnHit.ScoreBreakdown
if r.req.Explain {
hit.Expl = &search.Explanation{Value: 0.0, Message: "", Children: append([]*search.Explanation{hit.Expl}, knnHit.Expl.Children...)}
}
delete(knnHitMap, hit.ID)
}
}
for _, hit := range knnHitMap {
hit.Score = 0
ftsHits = append(ftsHits, hit)
if r.req.Explain {
hit.Expl = &search.Explanation{Value: 0.0, Message: "", Children: append([]*search.Explanation{nil}, hit.Expl.Children...)}
}
}
return ftsHits
}
func newRescorer(req *SearchRequest) *rescorer {
return &rescorer{
req: req,
}
}

View File

@@ -18,6 +18,7 @@ import (
"fmt"
"reflect"
"sort"
"strconv"
"time"
"github.com/blevesearch/bleve/v2/analysis"
@@ -47,6 +48,15 @@ var cache = registry.NewCache()
const defaultDateTimeParser = optional.Name
const (
ScoreDefault = ""
ScoreNone = "none"
ScoreRRF = "rrf"
ScoreRSF = "rsf"
)
var AllowedFusionSort = search.SortOrder{&search.SortScore{Desc: true}}
type dateTimeRange struct {
Name string `json:"name,omitempty"`
Start time.Time `json:"start,omitempty"`
@@ -311,13 +321,71 @@ func (r *SearchRequest) Validate() error {
}
}
err := validateKNN(r)
err := r.validatePagination()
if err != nil {
return err
}
if IsScoreFusionRequested(r) {
if r.SearchAfter != nil || r.SearchBefore != nil {
return fmt.Errorf("cannot use search after or search before with score fusion")
}
if r.Sort != nil {
if !reflect.DeepEqual(r.Sort, AllowedFusionSort) {
return fmt.Errorf("sort must be empty or descending order of score for score fusion")
}
}
}
err = validateKNN(r)
if err != nil {
return err
}
return r.Facets.Validate()
}
// Validates SearchAfter/SearchBefore
func (r *SearchRequest) validatePagination() error {
var pagination []string
var afterOrBefore string
if r.SearchAfter != nil {
pagination = r.SearchAfter
afterOrBefore = "search after"
} else if r.SearchBefore != nil {
pagination = r.SearchBefore
afterOrBefore = "search before"
} else {
return nil
}
for i := range pagination {
switch ss := r.Sort[i].(type) {
case *search.SortGeoDistance:
_, err := strconv.ParseFloat(pagination[i], 64)
if err != nil {
return fmt.Errorf("invalid %s value for sort field '%s': '%s'. %s", afterOrBefore, ss.Field, pagination[i], err)
}
case *search.SortField:
switch ss.Type {
case search.SortFieldAsNumber:
_, err := strconv.ParseFloat(pagination[i], 64)
if err != nil {
return fmt.Errorf("invalid %s value for sort field '%s': '%s'. %s", afterOrBefore, ss.Field, pagination[i], err)
}
case search.SortFieldAsDate:
_, err := time.Parse(time.RFC3339Nano, pagination[i])
if err != nil {
return fmt.Errorf("invalid %s value for sort field '%s': '%s'. %s", afterOrBefore, ss.Field, pagination[i], err)
}
}
}
}
return nil
}
// AddFacet adds a FacetRequest to this SearchRequest
func (r *SearchRequest) AddFacet(facetName string, f *FacetRequest) {
if r.Facets == nil {
@@ -353,6 +421,11 @@ func (r *SearchRequest) SetSearchBefore(before []string) {
r.SearchBefore = before
}
// AddParams adds a RequestParams field to the search request
func (r *SearchRequest) AddParams(params RequestParams) {
r.Params = &params
}
// NewSearchRequest creates a new SearchRequest
// for the Query, using default values for all
// other search parameters.
@@ -377,7 +450,7 @@ func NewSearchRequestOptions(q query.Query, size, from int, explain bool) *Searc
// IndexErrMap tracks errors with the name of the index where it occurred
type IndexErrMap map[string]error
// MarshalJSON seralizes the error into a string for JSON consumption
// MarshalJSON serializes the error into a string for JSON consumption
func (iem IndexErrMap) MarshalJSON() ([]byte, error) {
tmp := make(map[string]string, len(iem))
for k, v := range iem {
@@ -398,7 +471,7 @@ func (iem IndexErrMap) UnmarshalJSON(data []byte) error {
return nil
}
// SearchStatus is a secion in the SearchResult reporting how many
// SearchStatus is a section in the SearchResult reporting how many
// underlying indexes were queried, how many were successful/failed
// and a map of any errors that were encountered
type SearchStatus struct {
@@ -433,7 +506,7 @@ func (ss *SearchStatus) Merge(other *SearchStatus) {
// scores, score explanation, location info and so on.
// Total - The total number of documents that matched the query.
// Cost - indicates how expensive was the query with respect to bytes read
// from the mmaped index files.
// from the mapped index files.
// MaxScore - The maximum score seen across all document hits seen for this query.
// Took - The time taken to execute the search.
// Facets - The facet results for the search.
@@ -607,3 +680,79 @@ func isMatchAllQuery(q query.Query) bool {
_, ok := q.(*query.MatchAllQuery)
return ok
}
// Checks if the request is hybrid search. Currently supports: RRF, RSF.
func IsScoreFusionRequested(req *SearchRequest) bool {
switch req.Score {
case ScoreRRF, ScoreRSF:
return true
default:
return false
}
}
// Additional parameters in the search request. Currently only being
// used for score fusion parameters.
type RequestParams struct {
ScoreRankConstant int `json:"score_rank_constant,omitempty"`
ScoreWindowSize int `json:"score_window_size,omitempty"`
}
func NewDefaultParams(from, size int) *RequestParams {
return &RequestParams{
ScoreRankConstant: DefaultScoreRankConstant,
ScoreWindowSize: from + size,
}
}
func (p *RequestParams) UnmarshalJSON(input []byte) error {
var temp struct {
ScoreRankConstant *int `json:"score_rank_constant,omitempty"`
ScoreWindowSize *int `json:"score_window_size,omitempty"`
}
if err := util.UnmarshalJSON(input, &temp); err != nil {
return err
}
if temp.ScoreRankConstant != nil {
p.ScoreRankConstant = *temp.ScoreRankConstant
}
if temp.ScoreWindowSize != nil {
p.ScoreWindowSize = *temp.ScoreWindowSize
}
return nil
}
func (p *RequestParams) Validate(size int) error {
if p.ScoreWindowSize < 1 {
return fmt.Errorf("score window size must be greater than 0")
} else if p.ScoreWindowSize < size {
return fmt.Errorf("score window size must be greater than or equal to Size (%d)", size)
}
return nil
}
func ParseParams(r *SearchRequest, input []byte) (*RequestParams, error) {
params := NewDefaultParams(r.From, r.Size)
if len(input) == 0 {
return params, nil
}
err := util.UnmarshalJSON(input, params)
if err != nil {
return nil, err
}
// validate params
err = params.Validate(r.Size)
if err != nil {
return nil, err
}
return params, nil
}

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