Compare commits

..
Author SHA1 Message Date
Dominik Schmidt be8cd461b4 feat(graph): add the driveItem versions API
Implements the MS Graph driveItemVersion operations on files:

- GET  .../items/{id}/versions lists the versions, newest first
- GET  .../versions/{version-id} returns one version, "current" describes the file itself
- GET  .../versions/{version-id}/content redirects to a signed download url for the version
- POST .../versions/{version-id}/restoreVersion makes the version the current content

The operations map onto ListFileVersions and RestoreFileVersion of the
gateway. Version downloads point at the WebDAV meta endpoint, signed the
same way as the driveItem download url. The download url is only added
when requested via $select.

The driveItemVersion models are vendored from the regenerated client of
opencloud-eu/libre-graph-api#73 until that spec change is merged and the
dependency can be bumped.
2026-09-07 13:00:01 +02:00
Dominik Schmidt 5807eea186 feat(graph): expose @microsoft.graph.downloadUrl and /content on driveItems
Populates @microsoft.graph.downloadUrl on file driveItems when requested
via $select and implements GET .../items/{item-id}/content as a 302 to
the same URL: a by-id WebDAV URL signed with OC_URL_SIGNING_SECRET,
verified by the proxy, valid for 30 minutes. Folders answer 404 on
/content and never carry the annotation.

The annotation is available on the driveItem stat, the children and
root children listings and the share jail item endpoint.
2026-09-07 12:59:50 +02:00
Dominik Schmidt 0459149e0c style(graph): gofmt import order 2026-09-07 12:59:08 +02:00
Dominik Schmidt 13da9331f2 refactor(graph): use BaseGraphService.publicBaseURL consistently
drive.WebUrl, driveItem.WebUrl and the public share link WebUrl all
derive from the same config value (graph.spaces.webdav_base), but only
driveItem.WebUrl used the pre-parsed BaseGraphService.publicBaseURL.
The other two re-parsed the config on every call.

Add a webURLForResource method on BaseGraphService for the /f/<id> URLs
(used twice, with a *string return matching the libregraph DriveItem
field shape), and inline g.publicBaseURL for the single /s/<token>
share-link case. Convert cs3ResourceToDriveItem and formatDriveItems
from free functions into BaseGraphService methods so they pick up
logger and publicBaseURL from the receiver. This also aligns them with
the surrounding code: BaseGraphService already exposes ~15 similar
methods, so the two free functions were the odd ones out.

Net: all three WebUrls are now constructed from a single pre-parsed
URL, and the (g.logger, g.publicBaseURL) plumbing at 7 call sites
disappears.
2026-09-07 12:59:08 +02:00
Dominik Schmidt 81536bbd0e Merge pull request #3471 from opencloud-eu/feat/graph-expand-thumbnails
feat(graph): expand thumbnails on driveItems
2026-09-07 12:50:53 +02:00
Jörn Friedrich Dreyer be226ebb4c Merge pull request #3478 from opencloud-eu/reva-bump-main-20260907
[full-ci] chore: bump reva to latest main
2026-09-07 12:48:12 +02:00
Dominik Schmidt c2b37668e4 chore: bump reva to latest main 2026-09-07 09:33:59 +02:00
opencloudeu 4b7c6908f0 [tx] updated from transifex 2026-09-06 23:16:18 +00:00
Dominik Schmidt 6231a870d5 feat(graph): expand thumbnails on driveItems
$expand=thumbnails was only honored by sharedByMe and sharedWithMe. The
driveItem stat, the children listing and the root children listing now
honor it as well, so a client that lists a folder learns which items have
a preview instead of guessing from the mime type.

The thumbnails are set from the resource info the listing already has, so
a later preview check that needs more than the mime type has a single
place to sit. The two share listings carry driveItems only, they keep
matching on the mime type but share the url building.
2026-09-06 23:51:35 +02:00
opencloudeu 17003fe34b [tx] updated from transifex 2026-09-05 23:17:58 +00:00
Dominik Schmidt 2ce9faec5a Merge pull request #3444 from opencloud-eu/feat/driveitem-lock
graph: expose lockInfo on driveItems
2026-09-04 09:51:13 +02:00
opencloudeu f5696d40f8 [tx] updated from transifex 2026-09-03 23:17:09 +00:00
Dominik Schmidt 9a769b8019 build(deps): bump github.com/opencloud-eu/libre-graph-api-go to 45af3945a067 2026-09-03 16:02:43 +00:00
Dominik Schmidt bafbc88a40 graph: expose lockInfo on driveItems 2026-09-03 17:52:13 +02:00
Dominik Schmidt addb32812a Merge pull request #3438 from opencloud-eu/feat/driveitem-share-types
graph: expose @libre.graph.shareTypes on driveItems
2026-09-03 17:52:10 +02:00
Dominik Schmidt aa968ec898 graph: expose @libre.graph.shareTypes on driveItems 2026-09-03 16:59:29 +02:00
Dominik Schmidt 61037cc22f Merge pull request #3202 from opencloud-eu/feat/search-live-photo-facet
feat(search): live photo facet
2026-09-03 11:42:28 +02:00
Jörn Friedrich Dreyer 993265b64e Merge pull request #3445 from opencloud-eu/feat/graph-expand-children
feat: support $expand=children on the driveItem endpoint
2026-09-03 09:35:05 +02:00
Dominik Schmidt 5995938c34 chore(search): trim the live photo comments 2026-09-03 03:34:33 +02:00
Dominik Schmidt 0a4f3ed786 fix(search): keep livePhoto.contentId out of the search siblings 2026-09-03 03:34:33 +02:00
Dominik Schmidt a7214b3d73 fix(search): drop the live photo facet on an empty pairing id 2026-09-03 03:34:33 +02:00
Dominik Schmidt 81b0df1ad4 feat(search): live photo facet 2026-09-03 03:34:33 +02:00
Dominik Schmidt e6102c0405 Merge pull request #3200 from opencloud-eu/feat/search-motion-photo-facet
feat(search): motion photo facet
2026-09-03 03:34:29 +02:00
opencloudeu ab0b3c58f1 [tx] updated from transifex 2026-09-02 23:18:41 +00:00
Dominik Schmidt 3515594787 fix(search): keep the video facet on the file itself 2026-09-03 01:09:41 +02:00
Dominik Schmidt a8318fe03b refactor(search): take the motion photo video size from the video tika extracted 2026-09-03 00:32:07 +02:00
Dominik Schmidt 706aaf02f2 refactor(search): decide the motion photo from the file xmp and its extracted video 2026-09-03 00:32:06 +02:00
Dominik Schmidt 88f21d804b refactor(search): restore cs3 Retrieve, the download split had only one caller left 2026-09-03 00:31:40 +02:00
Dominik Schmidt 8aebf80124 refactor(search): confirm the motion photo video by type, not by name 2026-09-03 00:31:40 +02:00
Dominik Schmidt ae9fd595d9 chore(search): drop the dead legacy tika key 2026-09-03 00:31:40 +02:00
Dominik Schmidt 709962b616 feat(search): confirm the motion photo video via tika instead of reading bytes 2026-09-03 00:31:40 +02:00
Dominik Schmidt a2050d1fb9 feat(search): index and expose the motion photo facet 2026-09-03 00:31:40 +02:00
Dominik Schmidt 0f4b95b9ae feat(search): add RetrieveRange to the content retriever 2026-09-03 00:27:04 +02:00
Dominik Schmidt 42876ca61c refactor(search): let getFirstValue try multiple metadata keys 2026-09-03 00:27:04 +02:00
Jörn Friedrich Dreyer f5a064395a Merge pull request #3201 from opencloud-eu/feat/search-video-facet
feat(search): video facet
2026-09-03 00:20:02 +02:00
dependabot[bot] 6688a037f7 build(deps): bump go.opentelemetry.io/contrib/zpages
Bumps [go.opentelemetry.io/contrib/zpages](https://github.com/open-telemetry/opentelemetry-go-contrib) from 0.70.0 to 0.71.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.70.0...zpages/v0.71.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/zpages
  dependency-version: 0.71.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 16:48:58 +02:00
Jörn Friedrich Dreyer 20a579e6ea Merge pull request #3257 from opencloud-eu/fix/userlog_notifications_handling
fix: notification handling for share removal and space membership expiry
2026-09-02 10:25:07 +02:00
Florian Schade a52b55461e Merge pull request #3432 from pbleser-oc/chore/muse-dynamic-go-version
chore(mise): determine go version in mise.toml by parsing go.mod
2026-09-01 22:16:50 +02:00
Dominik Schmidt cc73c95a14 Merge pull request #3420 from fredrikblau/fix/3410-upload-file-date
fix(search): ignore invalid photo exposure times
2026-09-01 21:59:31 +02:00
Pascal Bleser 1ab6710e4c chore(mise): determine go version by parsing go.mod
Instead of statically defining the version of Go in mise.toml as well as
in its native files (go.mod), use a small shell script to parse go.mod
and determine the version dynamically from there.

That version is then available as an env variable in the mise.toml file
and can be referenced for go, in order to avoid having to keep those
versions in sync, especially since dependabot will update go in the
go.mod file, but not in mise.toml.
2026-09-01 21:21:59 +02:00
Dominik Schmidt 0750884597 perf: check the resource type before parsing the query 2026-09-01 20:04:22 +02:00
Dominik Schmidt aff81159a5 chore: drop the listDriveItemChildren comment 2026-09-01 20:01:54 +02:00
Dominik Schmidt 701d6fea17 chore: trim comments 2026-09-01 20:01:06 +02:00
Dominik Schmidt 74dd57d500 refactor: let formatDriveItems return values so children needs no copy 2026-09-01 19:59:51 +02:00
Dominik Schmidt b95e341f9c feat: support $expand=children on the driveItem endpoint 2026-09-01 19:55:14 +02:00
amir 67124a6c37 fix(search): ignore invalid exposure denominators 2026-09-01 20:24:44 +03:30
amir db4cee156b fix(search): ignore invalid photo exposure times 2026-09-01 20:24:19 +03:30
Dominik Schmidt 727c986de8 feat(search): index and expose the video facet 2026-09-01 16:26:01 +02:00
Alex Ababii 8d407417fe granular notifications enabled by default in config 2026-09-01 15:48:03 +02:00
Alex Ababii 4a9b3f4002 upd test cases after intorduced fixes 2026-09-01 15:48:01 +02:00
Alex Ababii 71eb0716dc fix in app space memebership expired notification 2026-09-01 15:42:03 +02:00
Alex Ababii 59fd05f0a9 SpaceMembershipExpired notification email fix 2026-09-01 15:42:03 +02:00
Alex Ababii 3bd17299e3 send email when a share is removed (backport https://github.com/owncloud/ocis/pull/10915) 2026-09-01 15:42:03 +02:00
Alex Ababii cdc806d21c bump reva version with backport and fixes for SpaceMembershipExpired 2026-09-01 15:42:02 +02:00
Alex Ababii 8997fbc326 removed env vars for granular notifications from docker and vscode config 2026-09-01 15:40:31 +02:00
Alex Ababii df16f6d07f fix userlog policy violation/antivirus event handling, enabled granular notifications 2026-09-01 15:40:31 +02:00
Dominik Schmidt f60e2e52e5 Merge pull request #3437 from opencloud-eu/feat/driveitem-pending-operations
graph: expose pendingOperations on driveItems
2026-09-01 15:35:24 +02:00
Pascal Bleser 249d105d27 Merge pull request #3293 from pbleser-oc/chore/#1312-graph-event-handler-split
chore(graph): disable HTTP or eventhandlers by configuration
2026-09-01 14:31:24 +02:00
Alex Ababii 5a7afe0bf6 Merge pull request #3348 from opencloud-eu/fix/posix_cli_commands
fix: posix cli commands
2026-09-01 14:07:17 +02:00
Dominik Schmidt 000c99c63b graph: expose pendingOperations on driveItems 2026-09-01 14:05:18 +02:00
Dominik Schmidt df8d0e8fd3 Merge pull request #3198 from opencloud-eu/feat/search-tika-facets
feat(search): extract more data from tika 4 (if available)
2026-09-01 11:45:11 +02:00
Dominik Schmidt f7c5d3adb4 Merge pull request #3113 from opencloud-eu/feat/driveitem-following-tags
graph: expose following state, tags and allowed actions on driveItems
2026-09-01 11:34:53 +02:00
Pascal Bleser e4365320de chore(graph): use metrics tools introduced in 8821053ff8
* introduce a fix in the metrics tools introduced with 8821053ff8 in
    order to deal with a struct that has fields that are not exported
    (as is the case here)

  * introduce a func in the toplevel metrics package that registers a
    single metric, as that needs to be done explicitly for unexported
    ones

  * in the graph service, use RegisterAll and BuildInfo from the
    toplevel metrics package, as introduced with 8821053ff8
2026-09-01 10:57:54 +02:00
Pascal Bleser 5b72318493 chore(graph): add metrics for HTTP API and LDAP
Introducing gowrap as a build-time tool to generate interface delegate
structs from templates:

 * added as a 'make go-generate' target in services/graph,
 * added as a build-time dependency in .bingo/

Introduce an LDAP client abstraction interface to be able to wrap the
go-ldap client API with metrics transparently (and possibly hooks and
such in the future), in order to use delegation patterns to measure the
time LDAP (client) operations take to finish, as well as to track their
results (success, failure, not-found).

Has two implementations that are generated using gowrap:

* a go-ldap adapter implementation that directly delegates to a go-ldap
  connection
* a time measuring and metrics collecting implementation that delegates
  to another LdapClient

The metrics collecting one is disabled by default, can be enabled with
GRAPH_LDAP_METRICS_DISABLE=false

It collects durations of outbound LDAP client operations into a histogram, as
well as the number of concurrent outbound LDAP operations in a gauge (via an
atomic int and a gauge function, as that performs best).

Add an HTTP middleware that measures how long Graph HTTP API requests
take, storing taken time into a histogram along with labels for

 * method,
 * path pattern (from the chi routes),
 * Graph API version prefix,
 * Graph API resource name,
 * and the resulting status code.

It also tracks the number of concurrent inbound Graph API HTTP requests
using a gauge (also using an atomic int and a gauge function).

Disabled by default, can be enabled with
GRAPH_HTTP_METRICS_DISABLE=false

Add Backend and EducationBackend delegate implementations that measure
execution time on the level of the higher API call operations there
(CreateUser, DeleteUser, ..., CreateSchool, ...), generated using
gowrap.

Disabled by default, can be enabled with
GRAPH_IDENTITY_BACKEND_METRICS_DISABLE=false

Also added a small k6 script to produce some read-only load on the Graph
API, for a casual test of the metrics, as well as k6 in mise.toml.

Make an internal changes to how singular LDAP entry searches work in the LDAP
identity backends:

 * check whether searches for a singular entry returns more than one
   result, in which case a new error TooManyResults is returned, instead
   of leaving that undetected, blindly taking the first result, and
   potentially risking data inconsistencies

Improve the loggers in identity backends by adding attributes for their
request targets (Reva gateway address or LDAP URI, respectively).

Also add a "backend" attribute for all Graph API logs (set to "ldap" or
"cs3"), to help debug potential issues, and remove them from all the logger
debug calls at the beginning of each LDAP-related function as those should
really be part of the logger and set beforehand.

The LDAP identity backend logger also has two new attributes to help
debugging with logs:

 * write (bool): whether write operations are enabled
 * refint (bool): whether refint is enabled or not

Also adds a dedicated counter metric for user password change operations.

Minor campfire improvements:

 * add a constructor func for the CS3 backend

 * add a constructor func for the LDAP backend

 * in the LDAP identity backend, in searchLDAPEntryByFilter (used by all
   search/get public functions), errors that occur when performing LDAP
   SEARCH operations were blindly mapped to a ItemNotFound error,
   instead of being analyzed as it could be caused by a technical error

 * in the requireadmin middleware, add debug logging to explain why a
   request is denied

 * when an LDAP password change fails because the user entry was not
   found in LDAP, we now have a log message that tracks that
2026-09-01 10:57:47 +02:00
Pascal Bleser e8cf677353 chore(graph): disable HTTP or eventhandlers by configuration
In the scope of the broader issue #1312, this PR deals with performing
those changes for the `graph` service, namely to add the ability to
disable the HTTP API or to disable the events API handler by
configuration.

It also adds metrics for the events processing, and tests for the events
processing.

The previous implementation was combining the HTTP server service and
the events consumption, which is why this PR refactors the composition
of those services:

 * the event consumption has been moved into its own service
 * the identity.Backend is created beforehand, and then injected as a
   collaborator in both the HTTP service as well as the event consumer
   service

It also adds metrics, mainly for the event processing.

To encourage re-use in latter implementations and changes, it also
introduces two top-level package changes:

 * internal/eventstest/events_test_helpers: contains a TestBus
   implementation to unit-test event consumers without NATS
 * internal/metricstest/metrics_test_helpers: contains assertion
   functions to test Prometheus metrics
2026-09-01 10:57:26 +02:00
Dominik Schmidt d2bcd3cc9e test(search): comment the root-id assumption, pin the OR fan-out 2026-09-01 10:57:26 +02:00
Dominik Schmidt 71b3104e7b fix(search): keep wildcard drive ids out of completion and pinning 2026-09-01 10:57:26 +02:00
Dominik Schmidt 5f03dcf768 test(search): pin the driveId scope in the parity suite
SCOPE-06..09 cover the completed root id, negation and composition; the cross-space fixture root follows the space-root convention (opaque id = space id).
2026-09-01 10:57:26 +02:00
Dominik Schmidt 25043babd5 perf(search): prune the space fan-out for drive-pinned queries
When top-level AND conjuncts pin the query to a single root
(driveId/RootID restrictions), only that space's index is asked; the
restriction itself stays in the query, so this is purely an
optimization. Conservative by design: any top-level OR, negated or
group-nested restriction leaves the fan-out untouched, searching a
space too many is wasted work while skipping one would be wrong.
Mountpoints are kept for result path mapping.

Costs one extra parse of the query in the service; parsing once and
handing the AST to the engines (which currently re-parse per space) is
a follow-up that changes the engine interface.
2026-09-01 10:57:26 +02:00
Dominik Schmidt 8bd130cc41 feat(search): driveId as a query field
scope: takes an opaque resource id, which is hostile to hand-written
queries. Accept driveId:"<storage$space>" as a regular KQL field
instead: it resolves to the indexed RootID, and a bare drive id is
completed to the root resource id (a space root's opaque id is its
space id). Full root ids pass through untouched.

Combined with path: this gives a readable location scope without any
token stripping: both are plain fields, so they compose with groups,
OR and NOT like everything else.
2026-09-01 10:57:26 +02:00
Dominik Schmidt 394b0fdc45 graph: expose @libre.graph.permissions.actions.allowedValues on driveItems
Opt-in via $select, on GetDriveItem and the root children listing.
2026-09-01 10:21:50 +02:00
Dominik Schmidt 11d8b8a382 graph: expose @libre.graph.tags on driveItems 2026-09-01 10:21:50 +02:00
Dominik Schmidt f0515bb28c graph: expose @libre.graph.me.following on driveItems 2026-09-01 10:21:50 +02:00
Alex Ababii d923bfe1f6 cli: upd check for removing empty dir 2026-09-01 10:03:34 +02:00
Alex Ababii 9822c1ffd4 upd ci config for cliCommands suite 2026-09-01 10:03:34 +02:00
Alex Ababii c742ff6798 fix for trash purge empty dirs for posix 2026-09-01 10:03:34 +02:00
Alex Ababii a2d5ee2bc6 fix revisions purge cli command for posix 2026-09-01 10:03:34 +02:00
Alex Ababii 7aa5b467f6 cli: fix for cleanup call 2026-09-01 10:03:34 +02:00
Alex Ababii 983c836653 cli: restore single item from trash bin, listing on empty thrash bin 2026-09-01 10:03:34 +02:00
Dominik Schmidt 7d4c6d6524 Merge pull request #3424 from opencloud-eu/feat/search-scope
feat(search): scope searches to a drive via the driveId field
2026-09-01 09:36:20 +02:00
Jörn Friedrich Dreyer a249fa5f74 Merge pull request #2662 from dschmidt/docs/adr-reflection-based-search-mapping
docs(adr): unified search index mapping
2026-09-01 09:11:09 +02:00
Dominik Schmidt 5e4f124e78 fix(search): find the language endpoint on tika 4, keep the legacy path 2026-09-01 09:09:55 +02:00
Dominik Schmidt a1e8aa783d fix(search): reject NaN coordinates and non-positive bitrates 2026-09-01 09:09:55 +02:00
Dominik Schmidt ae9ec4ce70 fix(search): harden facet value parsing 2026-09-01 09:09:55 +02:00
Dominik Schmidt c03b9c7229 fix(search): gate the audio facet inside getAudio, keep facets across meta entries 2026-09-01 09:09:55 +02:00
Dominik Schmidt 77bb974f9b test(search): run acceptance and CI against the released tika 4.0.0
Both tika pins (local compose and the CI service) point at apache/tika:4.0.0-full; the search README links tika version-less.
2026-09-01 09:09:55 +02:00
Dominik Schmidt 1be01ae50c fix(search): read the tika 4 content key, keep the legacy one
Tika 4.0.0 renamed the meta prefix from X-TIKA: to tk:.
2026-09-01 09:09:55 +02:00
Dominik Schmidt 2b4a6fd739 feat(search): populate audio and gps-altitude facets from tika, drop the audio-only gate 2026-09-01 09:09:55 +02:00
Dominik Schmidt 7e0f536b83 refactor(search): split the tika extractor and its tests into per-facet files 2026-09-01 09:09:55 +02:00
Jörn Friedrich Dreyer 49ebe072f1 Merge pull request #3426 from opencloud-eu/dependabot/go_modules/go.opentelemetry.io/otel-1.46.0
build(deps): bump go.opentelemetry.io/otel from 1.45.0 to 1.46.0
2026-09-01 08:25:05 +02:00
Jörn Friedrich Dreyer 8028e3e310 Merge pull request #3435 from fschade/test-search-cjk-parity
test(search): cover chinese search in the parity suite
2026-09-01 08:24:34 +02:00
Dominik Schmidt 7d75b98c69 Merge pull request #3423 from opencloud-eu/test/parity-readme-staleness
test(search): fail the parity suite when the committed matrix is stale
2026-09-01 08:24:15 +02:00
Florian Schade 9e8403818c test(search): cover chinese search in the parity suite 2026-09-01 07:46:03 +02:00
Dominik Schmidt ef41429838 test(search): rename the update flag to UPDATE_SEARCH_PARITY_MATRIX 2026-09-01 07:35:15 +02:00
Dominik Schmidt 7bf64e329c docs(adr): apply review wording suggestion 2026-09-01 07:26:32 +02:00
Dominik Schmidt 268a4497b4 Merge pull request #3197 from opencloud-eu/feat/search-schema-change-handling
feat(search): check the index schema on startup and refuse breaking changes
2026-09-01 07:17:29 +02:00
Florian Schade b4893baa81 Merge pull request #3379 from opencloud-eu/ci/search-opensearch-pipelines
ci: run search API and e2e suites against OpenSearch
2026-09-01 06:56:07 +02:00
opencloudeu ebf9566886 [tx] updated from transifex 2026-08-31 23:20:06 +00:00
Dominik Schmidt b149316349 docs(adr): keep the facet reachability gap out of the problem list 2026-08-31 21:22:22 +02:00
Dominik Schmidt c6ddc13055 docs(adr): date the context section 2026-08-31 21:20:54 +02:00
Dominik Schmidt 65ea519099 docs(adr): update to the implemented state 2026-08-31 21:16:43 +02:00
Dominik Schmidt 2041fdc259 test(search): comment the root-id assumption, pin the OR fan-out 2026-08-31 21:14:58 +02:00
Dominik Schmidt 8e68a34b8f fix(search): keep wildcard drive ids out of completion and pinning 2026-08-31 21:14:04 +02:00
Jörn Friedrich Dreyer 8f44f6eb42 Merge pull request #3429 from opencloud-eu/dependabot/go_modules/google.golang.org/grpc-1.83.2
build(deps): bump google.golang.org/grpc from 1.83.1 to 1.83.2
2026-08-31 18:10:47 +02:00
Jörn Friedrich Dreyer 0f9c6dc1ec Merge pull request #3425 from pbleser-oc/chore/mise-go-1.25.9
chore(mise): upgrade go in mise to 1.25.9 after 578861b473
2026-08-31 18:07:57 +02:00
dependabot[bot] 5bfbfd8ff7 build(deps): bump google.golang.org/grpc from 1.83.1 to 1.83.2
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.1 to 1.83.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.83.1...v1.83.2)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 14:45:06 +00:00
dependabot[bot] fe1e7632fa build(deps): bump go.opentelemetry.io/otel from 1.45.0 to 1.46.0
Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.45.0 to 1.46.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.45.0...v1.46.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 14:44:13 +00:00
Pascal Bleser 15b7fad504 chore(mise): upgrade go in mise to 1.25.9 after 578861b473
* upgrade the go version in mise.toml to 1.25.9 because dependabot
   upgraded go to 1.25.9 in go.mod

 * mise: upgrade dlv from 1.27.0 to 1.27.1:
   https://github.com/go-delve/delve/releases/tag/v1.27.1
2026-08-31 15:52:30 +02:00
v.scharf 5c97014cd2 ci: run search API and e2e suites against OpenSearch 2026-08-31 15:29:37 +02:00
Dominik Schmidt 774c732f52 test(search): pin the driveId scope in the parity suite
SCOPE-06..09 cover the completed root id, negation and composition; the cross-space fixture root follows the space-root convention (opaque id = space id).
2026-08-31 15:23:25 +02:00
Dominik Schmidt 95f175fe10 perf(search): prune the space fan-out for drive-pinned queries
When top-level AND conjuncts pin the query to a single root
(driveId/RootID restrictions), only that space's index is asked; the
restriction itself stays in the query, so this is purely an
optimization. Conservative by design: any top-level OR, negated or
group-nested restriction leaves the fan-out untouched, searching a
space too many is wasted work while skipping one would be wrong.
Mountpoints are kept for result path mapping.

Costs one extra parse of the query in the service; parsing once and
handing the AST to the engines (which currently re-parse per space) is
a follow-up that changes the engine interface.
2026-08-31 15:23:25 +02:00
Dominik Schmidt 094baab7be feat(search): driveId as a query field
scope: takes an opaque resource id, which is hostile to hand-written
queries. Accept driveId:"<storage$space>" as a regular KQL field
instead: it resolves to the indexed RootID, and a bare drive id is
completed to the root resource id (a space root's opaque id is its
space id). Full root ids pass through untouched.

Combined with path: this gives a readable location scope without any
token stripping: both are plain fields, so they compose with groups,
OR and NOT like everything else.
2026-08-31 15:23:25 +02:00
Dominik Schmidt feb13d426b test(search): name the update flag in the generated header 2026-08-31 15:16:15 +02:00
Dominik Schmidt b1533a3135 test(search): write the matrix only with UPDATE_MATRIX
A plain run compares and fails on a stale README without touching the working tree; UPDATE_MATRIX=true regenerates it.
2026-08-31 15:16:15 +02:00
Dominik Schmidt 874d8fdac2 test(search): fail the parity suite when the committed matrix is stale
The suite already regenerates README.md on every run; now a run whose content differs from the committed file also fails, so CI catches a stale matrix. The fresh content is on disk after the run, committing it is the fix.
2026-08-31 15:16:15 +02:00
Dominik Schmidt 0e2e2876f4 test(search): a golden diff says whether it needs a SchemaVersion bump
The failure runs the classifier on golden vs generated: additive means regenerate only, breaking means bump too.
2026-08-31 15:15:18 +02:00
Dominik Schmidt e11ce95d95 test(search): golden for the generated OpenSearch index definition
Pins the shipped schema as a reviewable diff; regenerate with UPDATE_GOLDEN=1.
2026-08-31 15:12:54 +02:00
Dominik Schmidt 7493f189a0 chore(search): adapt tests and golden to the v4 base
The refuse specs use registered analyzers (fulltext is gone), the golden regenerates via UPDATE_GOLDEN, MappingGetResp grew an accessor, and the parity suite passes the new NewBackend signature.
2026-08-31 15:12:54 +02:00
Dominik Schmidt 53fa66f120 chore(search): follow the rebased base
The opensearch-go bump renamed the mapping-get accessor, the schema check
takes a context and a logger now, and the golden bleve mapping carries the
word-broken Name and Title.
2026-08-31 15:12:54 +02:00
Dominik Schmidt 519dd112ea review: note SchemaVersion bump on bleve marshaling drift in golden test 2026-08-31 15:12:54 +02:00
Dominik Schmidt 414247d6c4 review: use any over interface{}, clone slice in AddBreaking 2026-08-31 15:12:54 +02:00
Dominik Schmidt 7ff2643d23 refactor(search): share SortedUnionKeys + Classification.AddBreaking
- export mapping.SortedUnionKeys and reuse it in bleve.compareKeysExcept
  instead of a copied union-of-keys block
- add Classification.AddBreaking to fold engine-specific breaking reasons and
  force the verdict, replacing the identical block in the bleve and opensearch
  Classify paths
2026-08-31 15:12:54 +02:00
Dominik Schmidt a0521f0260 review: trim verbose comments, cover Reconcile, table-driven Classify tests
- shorten the multi-line doc comments flagged as too verbose
- add reconcile_test.go: direct unit tests for Reconcile incl. the
  persisted-but-errored and classify-error branches (previously only
  reached indirectly through the engine integration tests)
- convert the 11 near-identical Classify It blocks to a DescribeTable
2026-08-31 15:12:54 +02:00
Dominik Schmidt 6b07a0d9dc chore(search): tighten the schema-reconcile comments 2026-08-31 15:12:54 +02:00
Dominik Schmidt dc863c64c6 refactor(search): single-source the new-index log message 2026-08-31 15:12:54 +02:00
Dominik Schmidt a8ffe91c79 feat(search): log a reindex hint when a fresh search index is created 2026-08-31 15:12:54 +02:00
Dominik Schmidt fec7536617 refactor(search): warn on a persisted additive change even when the reopen fails 2026-08-31 15:12:54 +02:00
Dominik Schmidt e8c93359b1 refactor(search): route schema verdict handling through a shared mapping.Reconcile 2026-08-31 15:12:54 +02:00
Dominik Schmidt 04c37d12d2 refactor(search): extract opensearch.NewClient out of server startup 2026-08-31 15:12:54 +02:00
Dominik Schmidt 9755728d43 refactor(search): harden the index-diff helpers against unset input 2026-08-31 15:12:54 +02:00
Dominik Schmidt 37eed7c321 refactor(search): only enforce analysis settings, tolerate operational drift 2026-08-31 15:12:54 +02:00
Dominik Schmidt a6bfb61935 refactor(search): make the breaking-schema error developer-facing 2026-08-31 15:12:54 +02:00
Dominik Schmidt 5abe2ccb26 fix(search): list the schema mismatch reasons on separate lines 2026-08-31 15:12:54 +02:00
Dominik Schmidt e9347998b7 fix(search): name the service to stop in the schema mismatch error 2026-08-31 15:12:54 +02:00
Dominik Schmidt 33299afbaa fix(search): address max-review findings
- the additive warnings advertise --all-spaces --force-rescan; a plain
  walk skips unchanged documents and never backfills the new fields
- Apply checks index existence first again, so a pre-provisioned index
  needs no create privilege and odd create-error shapes (string error
  bodies, cluster blocks) cannot fail a healthy startup; Create on 404
  keeps the typed already-exists swallow as the creation-race backstop
- number_of_replicas drift is not breaking, it is runtime-tunable and
  needs no rebuild
- bleve returns the classification alongside post-persist errors and
  the server warns before the error check, so the one-time additive
  warning is not lost when close or reopen fails
- a golden fixture pins the marshaled bleve mapping so a dependency
  bump that changes marshaling fails in CI instead of refusing every
  installation in the field
2026-08-31 15:12:54 +02:00
Dominik Schmidt 451e893d7d chore(search): tighten doc comments 2026-08-31 15:12:54 +02:00
Dominik Schmidt b67811ab3c chore(search): warn on additive opensearch changes and name the exact delete step
Addresses the two Copilot review comments on the PR: the additive
opensearch log now matches the bleve warning (level and re-index hint),
and the refuse message spells out how to delete the index per engine
(DELETE /<name> vs removing the bleve directory).
2026-08-31 15:12:54 +02:00
Dominik Schmidt 09ea7e5f15 chore(search): mention the impact of disabling search in the refuse message 2026-08-31 15:12:54 +02:00
Dominik Schmidt 1c130c448c chore(search): drop the changelog entry 2026-08-31 15:12:54 +02:00
Dominik Schmidt 03dfabddbb feat(search): check the index schema on startup and refuse breaking changes
Both engines now diff the stored/live index schema against the schema
generated from code when the service starts. A shared recursive
classifier in the mapping package is the single oracle:

- equal: start normally.
- additive (new fields without any indexed data): applied in place.
  OpenSearch gets a PUT _mapping with the full code properties, bleve
  persists the code mapping into the index (SetInternal + reopen) so
  the new fields are properly typed immediately and later startups
  classify equal. A startup warning lists the new fields because
  documents indexed before the upgrade lack them until re-indexed.
- breaking (changed definitions or analyzers, removed or renamed
  fields, or new fields that already contain data of unknown form):
  refuse to start with an error describing the rebuild procedure
  (delete the index, start, run "opencloud search index --all-spaces")
  and the OC_EXCLUDE_RUN_SERVICES=search escape hatch.

PUT _mapping is deliberately only the apply mechanism, never the
judge: its merge semantics cannot see removals or renames and it
accepts in-place updatable param changes with an ack. bleve
additionally checks idx.Fields() so previously dynamically indexed
data (which leaves no schema trace in bleve) is caught, matching by
exact name and by path prefix.

While at it: the OpenSearch startup check runs with a real,
minute-bounded context instead of context.TODO(), bleve indexes are
opened with a 5s bolt_timeout so a second process fails fast instead
of hanging on the file lock, and the reversed errors.Is arguments in
bleve.NewIndex were fixed.

https://github.com/opencloud-eu/opencloud/issues/3092
2026-08-31 15:12:54 +02:00
Dominik Schmidt d38fbc8e52 Merge pull request #3345 from opencloud-eu/refactor/search-mapping
refactor: reflection-based search mapping + location geopoint
2026-08-31 15:12:49 +02:00
Benedikt Kulmann 109e9555c8 chore(policies): disable gRPC or event handlers by configuration + metrics (#3287)
* add the ability to disable the gRPC API handler
   (POLICIES_GRPC_DISABLED)

 * add the ability to disable the Events API handler
   (POLICIES_EVENTS_DISABLED)

 * add metrics

 * add support for specifying rego files via the environment varirable
   POLICIES_ENGINE_FILES

 * for file paths specified in yaml or in POLICIES_ENGINE_FILES, support
   'config:' and 'data:' path prefixes

 * fix typos in the documentation, and try to make it more clear

 * add metrics to the documentation

 * add a section for testing in the documentation

 * introduces a new top-level package pkg/metrics/ with utilities for
   metrics that are backported from the groupware branch, with unit
   tests
2026-08-31 14:58:52 +02:00
Dominik Schmidt 1bf813068e docs(search): version-free index example in the README too 2026-08-31 14:17:29 +02:00
Dominik Schmidt aa2f7da029 docs(search): the unversioned index is the one up to 7.4 2026-08-31 14:15:17 +02:00
Dominik Schmidt 0188b18d2b docs(search): everything but the current index generation can go 2026-08-31 14:14:19 +02:00
Dominik Schmidt 73bfc1cf48 docs(search): just the reindex step 2026-08-31 14:12:52 +02:00
Dominik Schmidt 3a7d6ca54b docs(search): no index names in the migration steps, admins never type them 2026-08-31 14:12:38 +02:00
Dominik Schmidt 5d3c67b587 docs(search): the only migration path is a reindex
Copying the old index over misses the search sibling fields, copied documents would be unfindable.
2026-08-31 14:06:12 +02:00
Dominik Schmidt 2458d51ed1 feat(search): favorites are opaque user ids, no lowercase sibling
Same rule as the other ids; removing the sibling later would take another schema generation.
2026-08-31 13:50:36 +02:00
Dominik Schmidt d4f1ae910c fix(search): scope the OpenSearch purge to the resource's space
The delete-by-query matched the bare Path, so a purge could take same-path documents in other spaces with it; bleve was already RootID-scoped. Pinned as rootscope-04.
2026-08-31 13:50:36 +02:00
Dominik Schmidt 5abef94482 test(search): regenerate the parity matrix for the mediatype:file rows 2026-08-31 13:45:42 +02:00
Dominik Schmidt e14bef7ef3 chore(search): version-free schema examples in docs strings 2026-08-31 13:44:47 +02:00
Dominik Schmidt 98bdce156b chore: drop the duplicated vendor entry for analysis/char/regexp 2026-08-31 13:44:47 +02:00
Dominik Schmidt dd6b3548f9 fix(search): tighten the generated search siblings
The OpenSearch _lowercase siblings are search-only like bleve's (no doc_values, own map instance instead of aliasing the base), and the dead Path_words field is gone: _words exists for keyword fields only, as SearchSiblings declares.
2026-08-31 13:44:47 +02:00
Dominik Schmidt 9e8bb474b4 fix(search): compose the mediatype:file negation correctly
The file expansion is grouped again so its NOT stays atomic next to other terms (OpenSearch turned 'mediatype:file OR x' into '(NOT dir) AND x'), and the bleve compiler no longer re-keys resolved groups (a negated mediatype group targeted the raw 'mediatype' field and matched everything). Pinned as MEDIATYPE-07..10.
2026-08-31 13:43:19 +02:00
Dominik Schmidt 9503884472 docs(search): migration notes for schema v4
Also make the reindex copy safe to run after the service already indexed (op_type create, conflicts proceed).
2026-08-31 13:40:43 +02:00
Dominik Schmidt 2383bcddcc test(search): leave behavior to the parity suite
The engine suites keep what is engine-specific (index setup, health, purge-space batching); every behavior answer lives in the parity suite once. The shared test client learns IndicesCount, and FIELDS-15 pins the Size/Type gate for number queries.
2026-08-31 13:40:43 +02:00
Dominik Schmidt b92d671c02 chore(search): drop the orphaned hand-written v3 mapping leftovers 2026-08-31 13:40:43 +02:00
Dominik Schmidt 523d377a08 test(search): carry the parity suite over to the rebased semantics
Typed Mtime fixtures, VersionedIndexName, and audio.artist matches case-insensitively now (facets search case-insensitively by default).
2026-08-31 13:40:43 +02:00
Dominik Schmidt d5872aa851 fix(search): match fulltext phrases in order on bleve
Content:"reports monthly" must not match 'monthly reports'; the phrase runs on the analyzed field like on OpenSearch.
2026-08-31 13:40:43 +02:00
Dominik Schmidt 283de0d29a feat(search): adopt the parity-pinned query semantics on the sibling routing
From #3408: hidden takes bool words only, type categories map to the stored value in the shared pass, ? counts as a wildcard, a non-suffix wildcard on a word-broken field forgives the extension, = matches the whole value on the lowercased sibling, paths lose their trailing slash. Dead compiler helpers removed.
2026-08-31 13:40:42 +02:00
Dominik Schmidt ba210232ab feat(search): analyze Content by words on both engines, no stemming
Adopts the parity-pinned semantics from #3408: 'report' does not match 'reports'; the porter fulltext analyzer is gone.
2026-08-31 13:40:42 +02:00
Dominik Schmidt b5693d7058 chore(search): one schema generation, bump it to 4
The hand-written v3 of the interim mapping is taken; both engines derive index name and directory from search.SchemaVersion.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 903ef4521b feat(search): search facets by word too, whole values only by opt-out
SharePoint's default for a text property is word breaking, so ours is too:
every keyword field gets the _words sibling unless it opts out with
NoWordBreaker, which now carries SharePoint's polarity as well. Artist,
album, camera model and the other facets match by word like name and title;
tags and favorites stay one label, ids, paths and the mime type one value.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 96e728901e feat(search): search every keyword field case-insensitively by default
KQL searches case-insensitively, so every keyword field gets its lowercased
search sibling unless it opts out: ids are opaque, paths are POSIX, the mime
type is normalized already. That takes the facets along, artist or camera
model match regardless of case, while the case-preserved base still answers
and aggregates. Which siblings a field carries is decided once, from the
struct and the overrides, and the renderers, the document writer and the
query lowering all read it from there.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 6b07ea745c feat(search): split names and titles into words on both engines
A single word finds the names and titles that contain it, `report` finds
Report.txt, on bleve as well as on OpenSearch, which so far only did it by
accident of its dynamic mapping. Modelled like SharePoint's NoWordBreaker:
a keyword field is one whole value unless the override switches that off,
which adds a search-only _words sibling next to _lowercase, analyzed into
lowercased words (a dot is a word boundary, no stemming). The base stays the
whole value for returning and aggregating, wildcards and whole values keep
using _lowercase. Quotes do not change the meaning, a phrase is a phrase
either way, and there is no exact-match operator yet.
2026-08-31 13:40:42 +02:00
Dominik Schmidt e64692ce9e test(search): re-apply opensearch backend coverage under ginkgo 2026-08-31 13:40:42 +02:00
Dominik Schmidt 9c46713a65 refactor(search): make paths case-sensitive and scope searches at query level
Paths act as references (location scoping, deep links): /Foo and /foo
are distinct siblings, so path: matching must be exact. Case-insensitive
folder discovery is served by name: and its lowercase sibling. This also
matches bleve on main, where path queries have always been
case-sensitive.

Dropping the sibling removes its biggest maintenance cost: Path is the
one mutable sibling field, a move rewrites the paths of a whole subtree
and the OpenSearch move script had to rebuild Path_lowercase alongside
the base field.

With paths case-sensitive, the ref path scope moves into the query
itself: bleve as a term/prefix disjunction on the keyword Path,
OpenSearch as a term filter on its path_hierarchy tokens. The
post-filter that used to drop out-of-scope hits after the query ran is
gone; totals and paging now respect the scope instead of being computed
over the whole space, and a wrong-cased scope simply matches nothing.
2026-08-31 13:40:42 +02:00
Dominik Schmidt f6739f552d fix(search): bind a leading NOT to the term right after it, on both backends
A leading NOT next to an operator was miscompiled: the bleve compiler left the consumed term in `next`, so `NOT x AND y` dropped `y` and produced a self-contradicting clause; the OpenSearch transpiler checked nextOp==AND before prevOp==NOT, so the negated term landed in `must` instead of `must_not`. NOT is unary and binds to the node directly after it regardless of what follows. This also fixes `mediatype:file AND <term>` (the web Files filter) at the root, so the earlier mediatype:file group workaround is dropped.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 5d9a255b16 test(search): port fieldindex, mimetype and normalize tests to ginkgo 2026-08-31 13:40:42 +02:00
Dominik Schmidt 17144d4791 fix(search): keep OpenSearch path queries as unanalyzed term queries
A path value with spaces went through a match_phrase query, which analyzes
the query with the path_hierarchy analyzer; the resulting "." prefix token
matches every document in the space, breaking descendant matching and the
stale-path check after a move.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 0284224787 refactor(search): anchor the OpenSearch move prefix replace to the path start
The move script rewrote Path/Path_lowercase with painless String.replace, which replaces every occurrence of the old path, not just the leading prefix. OpenCloud paths are ./-prefixed so the full old path only occurs at the start and the result is byte-identical, but startsWith + substring makes the prefix-only intent explicit and robust to any path format. Not a live bug fix, a hardening.
2026-08-31 13:40:42 +02:00
Dominik Schmidt fc41995a47 fix(search): validate CaseInsensitive against the effective field type
The guard only rejected CaseInsensitive when a non-keyword/path Type was set explicitly. With no Type, isCasedType treated the field as cased, so CaseInsensitive on an inferred numeric/bool/datetime field passed validation but produced no _lowercase sibling, and the query would silently match nothing. Validate now falls back to the inferred Go type.
2026-08-31 13:40:42 +02:00
Dominik Schmidt c94059fb85 fix(search): keep mediatype:file atomic so it composes with other terms
mediatype:file expands to a NOT restriction. Spliced inline as `NOT MimeType:httpd/unix-directory`, the bleve compiler's NOT branch left a stale operand, so `mediatype:file AND name:x` dropped `name:x` and matched nothing (the web Files filter). It is now wrapped in a group so the negation stays atomic; verified fixing both bleve and OpenSearch.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 69c517a8b2 fix(search): nest json-tagged embedded structs instead of flattening them
resolveField marked every anonymous field embedded, so walkFields (mapping, field index, validate) and fillStruct (deserializer) flattened a json-tagged embedded struct, while conversions.To/encoding/json on the write path nests it under the tag, mapping and deserializing it at the wrong path. An anonymous field is now embedded only without a json tag name, matching encoding/json; fillStruct also recurses into a value nested struct. No current type has a tagged embedded struct, so runtime behavior is unchanged; this hardens the reflection walker.
2026-08-31 13:40:42 +02:00
Dominik Schmidt dad99e85b2 fix(search): make mediatype categories and MIME types case-insensitive
mediatype:Folder / mediatype:IMAGE resolved to a literal MimeType search and matched nothing because Expand switched on the raw value. The value is now lowercased in the lowering pass, so categories and literal MIME types match regardless of case, consistently on both backends.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 8cb973d695 test(search): cover mediatype and direct MimeType search on both backends
Adds bleve and OpenSearch coverage for category (image), literal MIME (image/svg+xml, with + and /), and raw MimeType: queries. Documents why MimeType skips the bleve escaper: it is not a bug, bleve treats / and + as literals mid-term, so a literal MIME still matches exactly while the category wildcard image/* keeps its *.
2026-08-31 13:40:42 +02:00
Dominik Schmidt c680a90e49 fix(search): reject CaseInsensitive on non-keyword/path fields
CaseInsensitive routes queries to a <field>_lowercase sibling that is only generated for keyword/path fields, so marking any other type CaseInsensitive would silently match nothing. Validate now rejects it up front.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 1bf15599f7 fix(search): write a consistent empty _lowercase sibling for empty arrays
The []any branch skipped the sibling for an empty array while the []string branch wrote an empty one; both now write it, matching the base field.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 727ca92afe fix(search): review fixes for path AND-term and content wildcard
bleve compiled a path restriction to a DisjunctionQuery, which mapBinary redistributes as an OR-chain, so `path:/Foo AND name:bar` matched the folder itself unconditionally. It is now a BooleanQuery (should: folder OR descendants), which mapBinary keeps atomic under an enclosing AND.

The OpenSearch full-text branch ran before the wildcard check, so `content:foo*` degraded to a phrase match and diverged from bleve; the wildcard check now comes first.

Adds the missing coverage the review flagged: path AND term, content wildcard, case-insensitive tags (the array sibling branch), and a spaced path with descendants on OpenSearch.
2026-08-31 13:40:42 +02:00
Dominik Schmidt e3c45dd1d0 fix(search): analyze OpenSearch full-text queries, stem Content like bleve
Single-term `content:` built an unanalyzed term query, so once this branch dropped the blanket query-value lowercasing, `content:Foo` missed on OpenSearch (bleve was unaffected, its query analyzes). Fielded full-text queries now use a match query. OpenSearch `Content` also gets a porter stemming analyzer (it used the default standard analyzer and never stemmed), so full-text search matches bleve on both case and stemming.
2026-08-31 13:40:42 +02:00
Dominik Schmidt ec58861e4e feat(search): per-field case-insensitive search via _lowercase siblings
Keyword and path fields always index their case-preserved base and, when CaseInsensitive is set, an additional <field>_lowercase sibling used only for matching. The KQL lowering marks a restriction case-insensitive; each backend searches the sibling and lowercases the query value the same way the sibling is precomputed at index time (Go strings.ToLower on both sides, so non-ASCII stays consistent).

Search always returns the case-preserved base, so the sibling never has to be read back. In bleve it is indexed but not stored, kept out of _all, and without doc values. In OpenSearch it deliberately stays in _source: excluding it would make every update-by-query script rebuild all siblings from the document via painless toLowerCase, which lowercases differently than Go and would drift from the query side. Keeping it in _source avoids that, and a lowercased copy of a name or path is negligible disk in a cluster.

The OpenSearch move script keeps the base and its sibling in sync by swapping the moved prefix in Path_lowercase and setting Name_lowercase from Go-lowercased params, so case-insensitive search still finds a file after it moves (previously the sibling went stale). bleve re-indexes the whole document on move/delete/restore, so its siblings stay fresh for free.

This also repairs OpenSearch path search (the query value was no longer folded to lowercase, so path:<Foo> returned nothing) and makes bleve path queries match a folder and its descendants like OpenSearch's path_hierarchy. The Path base stays case-preserved so the move/delete descendant update (an exact TermQuery on Path) matches mixed-case folders.
2026-08-31 13:40:42 +02:00
Dominik Schmidt fb22dd81a4 refactor(search): run the OpenSearch backend on the shared lowering pass
KQLToOpenSearchBoolQuery runs query.Normalize, then only value lowercasing stays
backend-specific; remapKey and unfoldValue are gone.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 1cba974c1d refactor(search): run the bleve backend on the shared lowering pass
The bleve Creator runs query.Normalize before compiling; the compiler consumes a
canonical AST with no field resolution or media-type special-casing.
2026-08-31 13:40:42 +02:00
Dominik Schmidt bf7d04f7a1 feat(search): add shared KQL lowering pass
query.Normalize resolves field names (query.ResolveField, from the derived
index + a small alias overlay) and expands media-type restrictions
(mimetype.Expand) once, between parse and backend compilation.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 45a8096617 feat(search): derive a case-insensitive field-name index from the resource struct
mapping.FieldNameIndex walks the struct and maps a lowercased field path to the
real field name, including nested facet sub-fields. Backend-neutral.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 65f615d927 refactor(kql): move parse/validation errors into pkg/kql
The KQL parser produced its own validation errors but imported them from the
search service's query package. Move them into pkg/kql and let the search
backend consume kql.IsValidationError, so the parser stops depending on a
service package.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 40a2f14600 fix(search): preserve query value case for case-sensitive fields on OpenSearch
OpenSearch lowercased every KQL query value, so exact-match queries on
case-preserved keyword fields (facet values, ids) never matched their stored
token. Fold the value only for fields with a lowercasing analyzer, mirroring the
bleve backend. The field set is derived once in search.LowercaseValueFields and
shared by both backends (bleve's local buildLowercaseFields is dropped).
2026-08-31 13:40:42 +02:00
Dominik Schmidt 7d55849900 feat(search): version the search index by schema version
Both backends carry a shared search.SchemaVersion in the index name
(OpenSearch <base>-vN) and data path (bleve-vN). A breaking schema change
bumps the version so the service builds a fresh index instead of colliding
with the incompatible previous one; the old index is left in place.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 450f380198 fix(search): type mtime as a date 2026-08-31 13:40:42 +02:00
Dominik Schmidt 7cc144a9c3 test(search): set Mtime on opensearch folder and root fixtures
The Mtime field is mapped as an OpenSearch `date`, which rejects an
empty value with `mapper_parsing_exception: cannot parse empty date`.
The folder and root fixtures had no Mtime, so serializing them to
`"Mtime": ""` made TestEngine_Purge/purge_resource_trees fail when the
document was indexed. Give both a valid RFC3339 Mtime, matching the
file fixture.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 6695f99c90 test(search): convert mapping package tests to ginkgo
New package, so use the repo's standard test framework.
2026-08-31 13:40:42 +02:00
Dominik Schmidt c6c36212f5 test(search): convert bleve geo/mtime tests to ginkgo
The package's engine suite is ginkgo; these new tests were plain.
2026-08-31 13:40:42 +02:00
Dominik Schmidt c384322d94 test(search): use RFC3339 Mtime in opensearch fixture
Mtime is now a date field; the fixture's Go-format string fails
OpenSearch date parsing.
2026-08-31 13:40:42 +02:00
Dominik Schmidt 29cfdb01a0 feat(search): index Location as a geopoint on both backends
Add a TypeGeopoint field type. The libregraph Location facet is kept as an
object (retrieval / numeric queries) and a sibling <name>_geopoint field
carries the {lat,lon} form for geo-distance / bbox / polygon queries,
uniform across bleve and OpenSearch via the shared mapping. PrepareForIndex
splices the sibling in at write time.
2026-08-31 13:40:42 +02:00
Dominik Schmidt e78cad1cbe refactor: reflection-based search mapping
Build the bleve and OpenSearch index mappings from the Go struct via
reflection (json tags + per-field overrides) instead of hand-rolled
mappings and hit deserializers. New mapping package: BleveBuildMapping,
OpenSearchBuildMapping, Deserialize[T], PrepareForIndex; field decoding is
fail-soft. Mtime is typed as a date so mtime ranges are chronological on
both backends. Route CS3 facet parsing through mapping.DeserializeStringMap.

The any-valued (bleve hit) and string-valued (CS3 metadata) deserializers
share one generic fillStruct walker with a per-value setLeaf callback.
2026-08-31 13:40:42 +02:00
Jörn Friedrich Dreyer 0becbb8212 Merge pull request #3419 from fredrikblau/fix/3413-oidc-nil-provider
fix(middleware): reject empty OIDC providers safely
2026-08-31 13:37:09 +02:00
Florian Schade a9da258dcc Merge pull request #3408 from fschade/fix-search-field-matching
fix(search): make openSearch and bleve behave the same
2026-08-31 13:24:49 +02:00
Florian Schade 1bde3d14bb fix(search): bring back rebase removals 2026-08-31 11:57:19 +02:00
Pascal Bleser 8821053ff8 chore(policies): disable gRPC or event handlers by configuration + metrics
* add the ability to disable the gRPC API handler
   (POLICIES_GRPC_DISABLED)

 * add the ability to disable the Events API handler
   (POLICIES_EVENTS_DISABLED)

 * add metrics

 * add support for specifying rego files via the environment varirable
   POLICIES_ENGINE_FILES

 * for file paths specified in yaml or in POLICIES_ENGINE_FILES, support
   'config:' and 'data:' path prefixes

 * fix typos in the documentation, and try to make it more clear

 * add metrics to the documentation

 * add a section for testing in the documentation

 * introduces a new top-level package pkg/metrics/ with utilities for
   metrics that are backported from the groupware branch, with unit
   tests
2026-08-31 11:54:32 +02:00
Florian Schade 5c153ff74a test(search): drop the engine overrides, both engines answer alike 2026-08-31 11:15:03 +02:00
Florian Schade 0106a700c2 test(search): follow the versioned index and pin the equals operator 2026-08-31 10:58:13 +02:00
dependabot[bot] 4639f8f7c6 build(deps): bump github.com/blevesearch/bleve/v2 from 2.6.0 to 2.6.1
Bumps [github.com/blevesearch/bleve/v2](https://github.com/blevesearch/bleve) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/blevesearch/bleve/releases)
- [Commits](https://github.com/blevesearch/bleve/compare/v2.6.0...v2.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 10:48:06 +02:00
Florian Schade bd4287762e fix(search): answer the same on both engines
both engines now agree on names, titles, tags, paths, types, sizes, dates,
hidden flags, facet values and wildcards. quotes only delimit phrases and
the equals operator matches the whole field value, following the kql spec.
the index name carries a generation so a changed mapping starts on a fresh
index, MIGRATION.md says how to fill it.
2026-08-31 10:44:03 +02:00
Florian Schade ffbfe171c3 Merge pull request #3418 from opencloud-eu/test/search-parity-suite
test(search): engine parity suite for bleve and opensearch
2026-08-31 10:22:25 +02:00
dependabot[bot] 1bd59d9210 build(deps): bump github.com/sirupsen/logrus from 1.10.0 to 1.10.1
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.10.0...v1.10.1)

---
updated-dependencies:
- dependency-name: github.com/sirupsen/logrus
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 10:00:26 +02:00
opencloudeu 32a35f8653 [tx] updated from transifex 2026-08-30 23:17:11 +00:00
amir 0a3378eef8 fix(middleware): reject empty OIDC providers safely 2026-08-29 19:29:18 +03:30
opencloudeu dee8d7b0f7 [tx] updated from transifex 2026-08-28 23:17:11 +00:00
Dominik Schmidt fef2df8671 chore(codacy): exclude the generated search parity matrix
Markdown lint has nothing to say about a table the test suite writes.
2026-08-28 19:56:57 +02:00
Florian Schade 18759b0183 test(search): cover a deleted space on both engines 2026-08-28 19:38:09 +02:00
Dominik Schmidt 545aa114e8 test(search): let the parity suite replace the per-engine backend suites
Everything the bleve and OpenSearch backend suites checked now runs against
both engines in services/search/pkg/parity. The two cases without a twin
there join it (a facet value keeps its case, batches stay apart), the one
thing only OpenSearch can do, refuse an unhealthy cluster, stays in its
package.
2026-08-28 19:38:01 +02:00
Dominik Schmidt 889483d69c test(search): document where the engines still differ from the parity suite
The expectations are the desired state; where bleve or OpenSearch answer
differently on main today the case records that answer as an engine
override, so the suite is green and the README lists every divergence as
known.
2026-08-28 19:38:01 +02:00
Dominik Schmidt 1668e23f80 test(search): port the parity suite to ginkgo
Every case is one spec per engine, so one engine failing leaves the other's
answer in the matrix. Query groups share their engines through an Ordered
container, operations build their own. The README is assembled from report
entries in ReportAfterSuite, which also works across ginkgo -p processes, and
is only written once every planned row got an answer from every engine.

A case can carry, per engine, what that engine answers today where it still
differs from the expectation (engineOverrides): the spec asserts the override,
the README marks the row as known, and the override fails once the engine
answers as expected.
2026-08-28 19:37:45 +02:00
Dominik Schmidt c24bfb2e41 test(search): keep the OpenSearch testcontainer between runs on request
KEEP_TEST_CONTAINER=true skips the terminate; the next run picks the
container up by name instead of booting a fresh one.
2026-08-28 19:20:58 +02:00
Dominik Schmidt a03a294a12 test(search): make the OpenSearch testcontainer start reliably
Cold boots take well over the 5s startup timeout, and a full host disk
tripped the flood-stage create-index block mid-run; test indexes are tiny.
2026-08-28 19:15:51 +02:00
Dominik Schmidt a3e40a7471 test(search): run the parity suite against the current main
Move the OpenSearch test helpers to services/search/internal/opensearchtest so
the parity package can import them, and drop the index generation and clock
patch the suite was written against, neither exists on main yet.
2026-08-28 19:15:50 +02:00
Florian Schade 5601be1276 test(search): introduce engine parity suite 2026-08-28 18:41:37 +02:00
Alex Ababii 6ceedcff0e clean up frontend configs after the event consumer move to sharing service 2026-08-26 23:52:46 +02:00
Alex Ababii 74d6ac1183 moved ShareCreated event consumer from frontend to shared service 2026-08-26 23:52:46 +02:00
Ralf Haferkamp 0c601bbb8c chore: Adapt for change opensearch APIs 2026-08-26 11:51:05 +02:00
dependabot[bot] 578861b473 build(deps): bump github.com/opensearch-project/opensearch-go/v4
Bumps [github.com/opensearch-project/opensearch-go/v4](https://github.com/opensearch-project/opensearch-go) from 4.6.0 to 4.7.3.
- [Release notes](https://github.com/opensearch-project/opensearch-go/releases)
- [Changelog](https://github.com/opensearch-project/opensearch-go/blob/v4.7.3/CHANGELOG.md)
- [Commits](https://github.com/opensearch-project/opensearch-go/compare/v4.6.0...v4.7.3)

---
updated-dependencies:
- dependency-name: github.com/opensearch-project/opensearch-go/v4
  dependency-version: 4.7.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 11:51:05 +02:00
Ralf Haferkamp a5bad69e20 chore: adjust for changed automemlimit API 2026-08-26 11:31:01 +02:00
dependabot[bot] f41a1e394e build(deps): bump github.com/KimMachineGun/automemlimit
Bumps [github.com/KimMachineGun/automemlimit](https://github.com/KimMachineGun/automemlimit) from 0.7.5 to 1.0.0.
- [Release notes](https://github.com/KimMachineGun/automemlimit/releases)
- [Commits](https://github.com/KimMachineGun/automemlimit/compare/v0.7.5...v1.0.0)

---
updated-dependencies:
- dependency-name: github.com/KimMachineGun/automemlimit
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 11:31:01 +02:00
dependabot[bot] 1160a70960 build(deps): bump github.com/nats-io/nats.go from 1.52.0 to 1.53.1
Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.52.0 to 1.53.1.
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.52.0...v1.53.1)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats.go
  dependency-version: 1.53.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 08:21:02 +02:00
dependabot[bot] 7d73743a94 build(deps): bump github.com/open-policy-agent/opa from 1.19.0 to 1.19.1
Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/open-policy-agent/opa/releases)
- [Changelog](https://github.com/open-policy-agent/opa/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-policy-agent/opa/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: github.com/open-policy-agent/opa
  dependency-version: 1.19.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 17:42:30 +02:00
OpenCloud Devops cd9095833a 🎉 Release 7.5.0 (#3238) 2026-08-25 15:53:40 +02:00
Benedikt Kulmann 321bcfabd5 Merge pull request #3401 from opencloud-eu/more-version-placeholders-7.5.0
chore: replace remaining version placeholders for 7.5.0
2026-08-25 15:47:02 +02:00
Benedikt Kulmann c01e2d46ec chore: replace remaining version placeholders for 7.5.0 2026-08-25 14:57:45 +02:00
Benedikt Kulmann e857bff6af Merge pull request #3400 from opencloud-eu/env-var-introduction-version-7.5.0
chore: set introduction version to 7.5.0 for new env vars
2026-08-25 14:20:54 +02:00
Benedikt Kulmann 709543234f chore: set introduction version to 7.5.0 for new env vars 2026-08-25 12:53:03 +02:00
Benedikt Kulmann 6eebeb8b57 Merge pull request #3333 from opencloud-eu/bump-webcommitid-180526
[full-ci] chore: bump webcommitid
2026-08-25 12:03:58 +02:00
Alexander Ackermann 71bda95ea6 chore: bump webcommitid 2026-08-25 11:17:17 +02:00
Benedikt Kulmann 81e22fd655 Merge pull request #3399 from opencloud-eu/chore/bump-web-7.4.0
[full-ci] chore: bump web to v7.4.0
2026-08-25 11:16:17 +02:00
Benedikt Kulmann 850b4e7ac6 Merge pull request #3398 from opencloud-eu/reva-bump-2.49.0
[full-ci] chore: bump reva to 2.49.0
2026-08-25 10:47:18 +02:00
Benedikt Kulmann 82dca95473 chore: fix commit message typo in bumping-reva skill 2026-08-25 10:01:26 +02:00
Benedikt Kulmann 330e874110 chore: bump reva to 2.49.0 2026-08-25 10:01:26 +02:00
Benedikt Kulmann 65051a63db chore: bump web to v7.4.0 2026-08-25 09:55:39 +02:00
dependabot[bot] 498fa40613 build(deps): bump google.golang.org/grpc from 1.83.0 to 1.83.1
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.0 to 1.83.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.83.0...v1.83.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 09:47:08 +02:00
dependabot[bot] 011892025b build(deps): bump github.com/stretchr/testify from 1.12.0 to 1.12.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.12.0 to 1.12.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.12.0...v1.12.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 09:32:01 +02:00
dependabot[bot] 3052b28fdc build(deps): bump github.com/go-chi/chi/v5 from 5.3.1 to 5.3.2
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.3.1 to 5.3.2.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.3.1...v5.3.2)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-version: 5.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 09:16:07 +02:00
dependabot[bot] cc77ceb92a build(deps): bump github.com/grpc-ecosystem/grpc-gateway/v2
Bumps [github.com/grpc-ecosystem/grpc-gateway/v2](https://github.com/grpc-ecosystem/grpc-gateway) from 2.29.0 to 2.30.0.
- [Release notes](https://github.com/grpc-ecosystem/grpc-gateway/releases)
- [Commits](https://github.com/grpc-ecosystem/grpc-gateway/compare/v2.29.0...v2.30.0)

---
updated-dependencies:
- dependency-name: github.com/grpc-ecosystem/grpc-gateway/v2
  dependency-version: 2.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-24 15:10:02 +02:00
dependabot[bot] 2880a632ff build(deps): bump golang.org/x/image from 0.44.0 to 0.45.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/image/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-24 14:04:19 +02:00
dependabot[bot] 1a5c0d58e8 build(deps): bump github.com/beevik/etree from 1.7.0 to 1.7.1
Bumps [github.com/beevik/etree](https://github.com/beevik/etree) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/beevik/etree/releases)
- [Changelog](https://github.com/beevik/etree/blob/main/RELEASE_NOTES.md)
- [Commits](https://github.com/beevik/etree/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: github.com/beevik/etree
  dependency-version: 1.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-24 13:23:58 +02:00
Sawjan Gurung cc8a161fce test(api): update php test dependencies (#3335)
* test: update php dependencies

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: use custom tus client

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: fix php code style and commands

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: use doctrine coding standard

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: convert headers value to string

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: initialize phpunit config

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: fix tus upload

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

* test: fix php code style

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>

---------

Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>
2026-08-21 19:38:34 +02:00
Jannik Stehle ec4d5b7e6b Merge pull request #3386 from opencloud-eu/feat/extend-thumbnail-service-resolutions
feat(thumbnails): extend list of default resolutions
2026-08-21 14:03:23 +02:00
Florian Schade 7ddd8566f1 Merge pull request #3388 from fschade/fix-search-write-visibility
fix(search): refresh the opensearch index after a write
2026-08-21 13:50:13 +02:00
Jörn Friedrich Dreyer 9796123462 Merge pull request #3279 from opencloud-eu/feat/eventhistory_event_handler_on_demand
ability to disable grpc and/or event consumer for event history service
2026-08-21 13:31:51 +02:00
Jörn Friedrich Dreyer ed5b6ee780 Update tests/acceptance/features/apiSearch1/favoriteSearch.feature 2026-08-21 13:23:22 +02:00
Florian Schade 97a8be52d2 fix(search): refresh the index after a write 2026-08-21 10:03:52 +02:00
Jannik Stehle e1aff97fe2 Merge pull request #3377 from opencloud-eu/fix/vips-thumbnail-height
fix(thumbnails): respect the requested height in libvips builds
2026-08-21 09:38:16 +02:00
Jörn Friedrich Dreyer efc3e713b1 Merge pull request #3289 from opencloud-eu/enable-direct-download
refactor datagateway into proxy middleware
2026-08-21 09:07:21 +02:00
Jannik Stehle 1e5de6e01b feat(thumbnails): extend list of default resolutions
Extend the list by these resolutions:

"500x280", "280x500", "1000x560", "560x1000", "512x2048"

This allows for a more granular thumbnail generation for requested previews
between 128 and 1080 px.
2026-08-21 09:04:59 +02:00
Jörn Friedrich Dreyer 962b425078 test for Allow Methods returned by tus
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 08:16:26 +02:00
Jörn Friedrich Dreyer f065e8acdc expect only the tus related cors allow header values
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 07:34:47 +02:00
Jörn Friedrich Dreyer 738ec6de67 make ocm internally expose the dataprovider
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 07:34:47 +02:00
Jörn Friedrich Dreyer 898b6e3479 tus always returns 200 on OPTIONS requests
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 07:34:46 +02:00
Jörn Friedrich Dreyer 72c0ee9e54 do not set datagateway url for storage drivers
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 07:34:46 +02:00
Jörn Friedrich Dreyer 0776a479da send all /data throught middleware
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 07:34:46 +02:00
Jörn Friedrich Dreyer a93ec92789 rewrite more /data urls
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 07:34:46 +02:00
Jörn Friedrich Dreyer c0d5380017 drop unused datagateway service and gateway config
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 07:34:46 +02:00
Jörn Friedrich Dreyer fff84b844f drop STORAGE_USERS_EXPOSE_DATA_SERVER env var
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 07:34:46 +02:00
Jörn Friedrich Dreyer 2830c4eed5 make dataprovider routable in tests
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 07:34:46 +02:00
Jörn Friedrich Dreyer a74661618e refactor datagateway into proxy middleware
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-21 07:34:46 +02:00
Jannik Stehle d562d44edf fix(thumbnails): respect the requested height in libvips builds
The libvips generator passed 0 as the target height to vips_thumbnail,
so the value was rejected and dropped.

As a result, a preview of a 4000x5000 portrait image requested with e.g.
x=500&y=500 returned a ...x1920 image instead of a ...x1080 one, which
would be the next correct size in the pre-defined resolutions list. This
was due to the missing height, so it used the width (=1920) to determine
the longest side.

This also aligns it with the non-libvips behavior.
2026-08-20 19:11:28 +02:00
Florian Schade 3b47c6550d Merge pull request #3372 from fschade/fix-search-hidden-flag
fix(search): keep the hidden flag in sync
2026-08-20 17:58:07 +02:00
Florian Schade de1173202d fix(search): keep the hidden flag in sync 2026-08-20 16:54:44 +02:00
Florian Schade b62af81bde Merge pull request #3361 from fschade/fix-search-opensearch-queries
fix(search): fix open-search kql field compilation
2026-08-20 13:39:22 +02:00
Pascal Bleser f88765ed7f fix(acceptance): fix running acceptance tests against host on Linux (#3358)
Add an entry host.docker.internal -> host-gateway entry to the
acceptable tests container in tests/acceptance/docker/src/acceptance.yml
in order to be able to run them against an opencloud server running on
the host on Linux as well.

Reference: https://www.baeldung.com/ops/docker-compose-add-host
2026-08-20 10:56:24 +02:00
Florian Schade 3ddc5b47c8 test(search): drop the issue markers the fixes above resolve 2026-08-20 10:52:12 +02:00
Anja Barz 719327d1ad docs: clarify custom role bootstrap behavior 2026-08-20 09:56:46 +02:00
Jannik Stehle 5b2529602f feat(graph): add LibreGraphContentType on drive 2026-08-20 09:03:56 +02:00
Pascal Bleser f034f2058f fix(graph): adding the same user as multiple members in a group (#3354)
* make sure the list of members to add to a group is unique, by
   removing duplicates in the inbound list of member IDs

 * not attempting to detect duplicate DNs as that should not be
   necessary, and results in an error with OpenLDAP; the built-in IDM
   does not catch those as of now but the issue is tracked there, and
   should not happen due to the UUID filtering in this commit

 * remove the corresponding failing test from the list of expected
   failures

Fixes 3354
2026-08-20 08:11:49 +02:00
dependabot[bot] e25afdd420 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.14.4 to 2.14.5.
- [Release notes](https://github.com/nats-io/nats-server/releases)
- [Changelog](https://github.com/nats-io/nats-server/blob/main/RELEASES.md)
- [Commits](https://github.com/nats-io/nats-server/compare/v2.14.4...v2.14.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-20 08:02:19 +02:00
André Duffeck 78796c4d95 Only log a debug message when an item is still in processing state 2026-08-20 07:58:40 +02:00
dependabot[bot] c1528c53ff build(deps): bump go.opentelemetry.io/contrib/zpages
Bumps [go.opentelemetry.io/contrib/zpages](https://github.com/open-telemetry/opentelemetry-go-contrib) from 0.69.0 to 0.70.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.69.0...zpages/v0.70.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/zpages
  dependency-version: 0.70.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-20 07:49:16 +02:00
dependabot[bot] e96f8bacb1 build(deps): bump github.com/sirupsen/logrus from 1.9.4 to 1.10.0
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.4 to 1.10.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.9.4...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/sirupsen/logrus
  dependency-version: 1.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-20 07:48:49 +02:00
Florian Schade 5cef433c5a fix(search): answer a query the parser refuses with a bad request 2026-08-19 17:35:05 +02:00
Florian Schade 378e2c45ec fix(search): search a value the way the field is analyzed 2026-08-19 17:35:05 +02:00
Andre Duffeck 5f1a3ee869 Merge pull request #3329 from opencloud-eu/chore/bump-reva
chore: bump reva to latest main
2026-08-19 15:54:30 +02:00
Florian Schade 06bc072925 Merge pull request #3337 from fschade/fix-search-event-acks
fix(search): ack every event the index service handles
2026-08-19 15:46:34 +02:00
André Duffeck db23f1a1ca Bump reva 2026-08-19 15:00:00 +02:00
Florian Schade 0747008037 fix(search): ack every event the index service handles 2026-08-19 14:45:28 +02:00
Florian Schade 8a9889c12c Merge pull request #3336 from fschade/fix-search-space-deleted-event
fix(search): take a deleted space out of the index when it is deleted
2026-08-19 14:26:22 +02:00
André Duffeck 3d632fb90e Expose upload status and message 2026-08-19 14:08:12 +02:00
Florian Schade 45403e0909 fix(search): take a deleted space out of the index when it is deleted 2026-08-19 13:47:44 +02:00
Florian Schade 5e55b80e2c Merge pull request #3338 from fschade/refactor-search-param-names
refactor(search): re-name the engine parameters
2026-08-19 13:39:29 +02:00
Florian Schade 545af8e735 refactor(search): re-name the engine parameters 2026-08-19 12:52:47 +02:00
André Duffeck f70e44a474 Run buildifier 2026-08-19 12:48:34 +02:00
André Duffeck 320284d03e Bump reva 2026-08-19 12:35:43 +02:00
André Duffeck e3bf541d88 Disable ldap lookup cache during tests 2026-08-19 12:34:30 +02:00
André Duffeck 12e85ae950 Expose ldap cache ttl, set a default of 10s 2026-08-19 12:31:30 +02:00
junkerderprovinz 654f0e8e20 fix(postprocessing): retry publishing events instead of killing the server
A single transient nats: timeout on event publish was fatal to the whole
single-binary server, with no retry despite the service already having
backoff machinery for step failures. Now retries with the same exponential
backoff, capped at half the ack wait so the source event isn't redelivered
to another worker mid-retry. Also stops acking the source event when
publish ultimately fails, so the upload resumes after restart instead of
staying orphaned, and no longer exits during graceful shutdown.
2026-08-19 11:23:46 +02:00
Jörn Friedrich Dreyer 6d2cbd365f Merge pull request #3344 from opencloud-eu/antivirus-log-error
actually log error on exit
2026-08-19 09:39:11 +02:00
Ralf Haferkamp f94b6c7cdd cleanup: Remove old changelog leftovers
We've switched away from calens for changelog generation. This removes
some leftovers.
2026-08-19 08:40:32 +02:00
dependabot[bot] 07adbac96f build(deps): bump github.com/stretchr/testify from 1.11.1 to 1.12.0
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.1 to 1.12.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.11.1...v1.12.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-19 08:40:11 +02:00
junkerderprovinz 810ff866a0 chore: remove deprecated changelog fragment per review feedback 2026-08-19 08:39:26 +02:00
junkerderprovinz 27eed22bdc fix(activitylog): log missing parent id cache entry at debug level
Moving a file logged "could not delete parent id cache" at error
level even though the operation succeeded. The parent id cache is
populated lazily and its entries expire, so removing a key that was
never cached returns "key not found", which is the expected case
rather than an error. Log it at debug level instead.

Two of the three log sites reported in the issue live in the
vendored opencloud-eu/reva module and need a separate fix there.

Fixes: https://github.com/opencloud-eu/opencloud/issues/1043
2026-08-19 08:39:26 +02:00
dependabot[bot] 965a7f0f12 build(deps): bump golang.org/x/net from 0.57.0 to 0.58.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.57.0 to 0.58.0.
- [Commits](https://github.com/golang/net/compare/v0.57.0...v0.58.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.58.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-19 08:05:19 +02:00
dependabot[bot] 528bacce7d build(deps): bump github.com/onsi/ginkgo/v2 from 2.32.0 to 2.32.1
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.32.0 to 2.32.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.32.0...v2.32.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-19 08:04:46 +02:00
opencloudeu 80fadeb381 [tx] updated from transifex 2026-08-18 23:17:04 +00:00
Dominik Schmidt 03cfbacafa docs(adr): add concrete mtime/name backend-drift examples 2026-08-18 17:57:27 +02:00
Dominik Schmidt 2223b8ed9d docs(adr): include location.* in the OpenSearch facet sub-strings list
The bullet enumerating which OpenSearch facet sub-fields change
from text+keyword to keyword-only listed audio, photo and image but
omitted location, which is in the same dynamic-template-handled
facet block per the context section. Add it for consistency.
2026-08-18 17:57:27 +02:00
Dominik Schmidt 8053275166 docs(adr): add 0005 unified search index mapping
Propose a single central Go-struct + overrides map as the source of
truth for the search index layout across bleve and OpenSearch — the
same definition drives the per-backend index mapping, the write-time
adapter, the hit-decoding path, and the KQL compiler's case-folding
rules, so the two backends cannot drift silently again.

Also records the end-to-end case-handling principle for facet data
(indexed as case-preserving keywords so aggregation buckets return
correct display values), the sibling-field pattern for geopoint on
Location, and the rationale for replacing the two backends' implicit
defaults with an explicit, backend-agnostic contract.

PR #2659 is a proof-of-concept implementation the proposal emerged
from; scope and APIs there will be revisited once this ADR lands.
2026-08-18 17:57:26 +02:00
Florian Schade 2e5dd7b5ef Merge pull request #3297 from fschade/fix-mention-notification-review
fix(collaboration): harden the mention notification endpoint
2026-08-18 17:57:04 +02:00
Jörn Friedrich Dreyer dcd717f83c actually log error on exit 2026-08-18 17:17:30 +02:00
dependabot[bot] 5056d4a0f4 build(deps): bump github.com/testcontainers/testcontainers-go/modules/opensearch
Bumps [github.com/testcontainers/testcontainers-go/modules/opensearch](https://github.com/testcontainers/testcontainers-go) from 0.43.0 to 0.44.0.
- [Release notes](https://github.com/testcontainers/testcontainers-go/releases)
- [Commits](https://github.com/testcontainers/testcontainers-go/compare/v0.43.0...v0.44.0)

---
updated-dependencies:
- dependency-name: github.com/testcontainers/testcontainers-go/modules/opensearch
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 15:51:36 +02:00
Jannik Stehle c198c2e453 chore: bump reva to latest main 2026-08-18 15:25:23 +02:00
Florian Schade bb8e185fbd fix(graph): use authResponse token exclusively 2026-08-18 15:03:57 +02:00
Viktor Scharf c2a01e3aad ci: run search acceptance tests against OpenSearch on opensearch PR title (#3323) 2026-08-18 12:51:24 +02:00
Andre Duffeck 8aa55eb010 Merge pull request #3331 from aduffeck/fix-opensearch-space-scope
Fix opensearch space scope
2026-08-18 12:38:42 +02:00
Florian Schade 99b01b0734 refactor(graph): let the cs3 mapper answer the statuses the handler does not name 2026-08-18 11:48:59 +02:00
André Duffeck 6a9ffd6842 Switch to Ginkgo tests for readability 2026-08-18 11:26:23 +02:00
Jannik Stehle 5ad9d61fdc build(deps): bump github.com/opencloud-eu/libre-graph-api-go to 69f82a7dde55 2026-08-18 11:12:23 +02:00
André Duffeck 701276bc80 Make sure to scope updates to the index by space 2026-08-18 10:54:24 +02:00
dependabot[bot] 3445b75216 build(deps): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace
Bumps [go.opentelemetry.io/otel/exporters/stdout/stdouttrace](https://github.com/open-telemetry/opentelemetry-go) from 1.44.0 to 1.45.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.44.0...v1.45.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/stdout/stdouttrace
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 10:45:45 +02:00
Florian Schade 516734b18a fix(graph): map the cs3 status of a mention to the matching http error 2026-08-18 10:35:02 +02:00
Pascal Bleser fedfddf407 Merge pull request #3307 from fschade/mise-toml
[docs-only] feat: add mise toml
2026-08-18 10:33:33 +02:00
Sawjan Gurung 4bcf97353a test(api): fix share role update test scenario (#3322)
Signed-off-by: Saw-jan <saw.jan.grg3e@gmail.com>
2026-08-18 09:49:18 +02:00
dependabot[bot] 2fa4a0458d build(deps): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
Bumps [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) from 0.69.0 to 0.70.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.69.0...zpages/v0.70.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
  dependency-version: 0.70.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 09:43:19 +02:00
Florian Schade bbee2b7433 fix: restructure call order 2026-08-18 09:40:01 +02:00
Alex a3c7b2dbb3 feat: update space template image (#3324)
* feat: update space template image

* skip test

* reduce asset size
2026-08-18 09:14:20 +02:00
opencloudeu 7130c10ef3 [tx] updated from transifex 2026-08-17 23:19:38 +00:00
Florian Schade 93d66a0f37 fix(graph): answer dropped mentions with 202 to prevent access probing 2026-08-17 15:48:00 +02:00
Florian Schade 8b922127b8 feat: move the collaboration mentioning api to graph 2026-08-17 15:44:04 +02:00
Florian Schade 8a3d8fea4d fix(collaboration): check the requester's access before a mention goes out 2026-08-17 15:44:04 +02:00
Florian Schade 6077e7453c Merge pull request #3321 from fschade/bump-libre-graph-api-go
build(deps): bump github.com/opencloud-eu/libre-graph-api-go
2026-08-17 15:30:41 +02:00
Viktor Scharf 1959d79717 test: add api tests for cross-space search index mutation (#3320) 2026-08-17 15:10:34 +02:00
Florian Schade 26bd26f998 build(deps): bump github.com/opencloud-eu/libre-graph-api-go to 387da64db0b1 2026-08-17 12:29:18 +02:00
Viktor Scharf c8a1f55657 ci: run search acceptance tests against OpenSearch in nightly (#3302)
* ci: run search acceptance tests against OpenSearch in nightly

* fix

* add issue tag to failed search tests
2026-08-17 08:19:54 +02:00
opencloudeu 7243c922ef [tx] updated from transifex 2026-08-16 23:17:02 +00:00
opencloudeu 2e912d233a [tx] updated from transifex 2026-08-14 23:17:03 +00:00
Alex Ababii 1d55c4d300 eventhistory: moved consumer related actions to it's constructor 2026-08-14 17:33:09 +02:00
Jannik Stehle 29a56da565 Merge pull request #3313 from opencloud-eu/feat/enable-rclone-crypt-app
feat(web): add rclone-crypt to default apps
2026-08-14 16:05:52 +02:00
Jannik Stehle 4e0cd4038e feat(web): add rclone-crypt to default apps
Enable the rclone-crypt app per default for the upcoming release.
2026-08-14 14:42:45 +02:00
Alex Ababii 4e5bef96a6 Merge branch 'main' of github.com:opencloud-eu/opencloud into feat/eventhistory_event_handler_on_demand 2026-08-14 13:30:32 +02:00
Thomas Schweiger 1fa8950571 Merge pull request #3229 from opencloud-eu/favourite-sse-event 2026-08-14 13:00:30 +02:00
Alex Ababii 6e87f6665b eventhistory service: fix consumer isolation and conditional initialization 2026-08-14 11:56:23 +02:00
Alex AbabiiandViktor Scharf 8989106e91 fix restore file version for shared resource (#3268)
* tmp upd of web and reva version for testing purposes

* chore: bump reva and web

* upd expected failures list in acceptance test for revert file version case

* bump reva and web versions

---------

Co-authored-by: Viktor Scharf <v.scharf@opencloud.eu>
2026-08-14 10:40:59 +02:00
Thomas Schweiger 9a9443fe85 enhance: send events for adding/removing favorite items 2026-08-14 10:16:57 +02:00
Florian Schade 7fd5079973 feat: add mise toml 2026-08-14 09:32:43 +02:00
Alex b2cf7a965f fix(csp): allow blob: in style-src for EPUB reader iframe (#3300) 2026-08-13 13:16:04 +02:00
Jörn Friedrich Dreyer 8478b5daa1 allow tuning the proxies http client
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-08-13 11:54:42 +02:00
Pascal Bleser 331f78eb43 feat(nats): enable the NATS monitoring HTTP endpoint in the embedded NATS server
* add configuration options to enable the NATS server HTTP monitoring
   endpoint:
   - NATS_MONITORING_ENABLED (defaults to false)
   - NATS_MONITORING_HTTP_HOST (defaults to 127.0.0.1)
   - NATS_MONITORING_HTTP_PORT (defaults to 8222)
   - NATS_MONITORING_ENABLE_TLS (defaults to false)

Fixes #3294
2026-08-13 11:46:53 +02:00
dependabot[bot] 5deb863dd8 build(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://github.com/open-telemetry/opentelemetry-go) from 1.44.0 to 1.45.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.44.0...v1.45.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 11:42:47 +02:00
dependabot[bot] 1cf3ff4863 build(deps): bump github.com/rogpeppe/go-internal from 1.15.0 to 1.16.0
Bumps [github.com/rogpeppe/go-internal](https://github.com/rogpeppe/go-internal) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/rogpeppe/go-internal/releases)
- [Commits](https://github.com/rogpeppe/go-internal/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: github.com/rogpeppe/go-internal
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 11:42:08 +02:00
dependabot[bot] 9ae863ca3a build(deps): bump github.com/kovidgoyal/imaging from 1.8.22 to 1.8.23
Bumps [github.com/kovidgoyal/imaging](https://github.com/kovidgoyal/imaging) from 1.8.22 to 1.8.23.
- [Release notes](https://github.com/kovidgoyal/imaging/releases)
- [Commits](https://github.com/kovidgoyal/imaging/compare/v1.8.22...v1.8.23)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 11:40:19 +02:00
Viktor Scharf 0fc2a98e6e api-test: notification settings and getting email notifications (#3281)
* api-test: notification settings and getting email notifications

* disable all email notifications
2026-08-13 11:32:12 +02:00
Jörn Friedrich Dreyer 6f5f993ada Merge pull request #3288 from fschade/feat-modernize-opa
perf(policies): reuse the compiled rego query across evaluations
2026-08-12 17:58:13 +02:00
Florian Schade 1e7cfaf92d refactor(policies): read the rego policies once at startup 2026-08-12 17:01:49 +02:00
Viktor Scharf b41d0c1040 fix(ci): stamp version into daily docker images (#3291)
* fix(ci): stamp version into daily docker images

* fix after review
2026-08-12 12:09:22 +02:00
opencloudeu 716922f76f [tx] updated from transifex 2026-08-11 23:17:55 +00:00
Florian Schade 573844ada4 Merge pull request #3286 from fschade/fix-proxy-middleware-race
Fix proxy middleware race
2026-08-11 12:40:54 +02:00
Florian Schade cd7f8f91b6 fix: oidc middleware claim race 2026-08-11 11:39:50 +02:00
Alex Ababii fec4640b5a eventhistory: debug service always enabled 2026-08-11 11:15:31 +02:00
opencloudeu 8504e29743 [tx] updated from transifex 2026-08-10 23:17:14 +00:00
Alex Ababii 695f6a9e4f uneeded type conversion 2026-08-10 15:31:50 +02:00
Alex Ababii 4838167696 ability to disable grpc and/or event consumer for event history service 2026-08-10 14:36:40 +02:00
dependabot[bot] f03429f47c build(deps): bump go.opentelemetry.io/otel/trace from 1.44.0 to 1.45.0
Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.44.0 to 1.45.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.44.0...v1.45.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/trace
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-10 11:24:31 +02:00
Viktor Scharf 97908ac6c1 api-test: add posixfs scan and consistency CLI tests (#3263)
* api-test: add posixfs scan and consistency CLI tests

* fix after review
2026-08-10 08:50:47 +02:00
opencloudeu 3a3cba602c [tx] updated from transifex 2026-08-08 23:16:53 +00:00
Jannik Stehle 1c138efac2 Merge pull request #3259 from opencloud-eu/feat/yjs-server-url
feat(web): add yjsServerUrl config
2026-08-06 15:28:11 +02:00
Jannik Stehle 07fd041efb feat(web): add yjsServerUrl config
This config specifies a URL for a yjs server that is used for document
collaboration in web apps (e.g. tiptap editor, excalidraw, ...). It's
optional, not setting this disables this feature in Web.
2026-08-06 13:37:54 +02:00
Viktor Scharf da7849251d update rolling release template (#3242) 2026-08-06 09:29:16 +02:00
Michael Barz 466c3c8c79 Merge pull request #3252 from aduffeck/fix-favorite-flag
Fix missing favorite flag on opensearch hits
2026-08-06 09:07:57 +02:00
André Duffeck 2142894cba Fix missing favorite flag on opensearch hits 2026-08-05 18:43:13 +02:00
Viktor Scharf 76ff13b47e api-test: replace sleeps with WaitHelper poll for async state (#3239)
* api-test: replace sleeps with WaitHelper poll for async state

* php-style
2026-08-05 09:23:57 +02:00
Andre Duffeck 799f98339e Merge pull request #3235 from pbleser-oc/feat/3182-posixfs-scan-add-subtree
feat(posixfs): #3182 add basepath option in the "posixfs scan" command
2026-08-04 09:15:22 +02:00
OpenCloud Devopsandv.scharf d695d283f6 🎉 Release 7.4.0 (#3143)
* 🎉 Release 7.3.1

* 🎉 Release 7.3.1

* 🎉 Release 7.3.1

* 🎉 Release 7.3.1

* 🎉 Release 7.3.1

* 🎉 Release 7.3.1

* 🎉 Release 7.3.1

* 🎉 Release 7.3.1

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

* 🎉 Release 7.4.0

---------

Co-authored-by: v.scharf <v.scharf@opencloud.eu>
2026-08-03 20:42:51 +02:00
Viktor Scharf 553c85b7ff set introduction version 7.4.0 (#3234) 2026-08-03 20:07:23 +02:00
Pascal Bleser 6663fbeeed feat(posixfs): #3182 add basepath option in the "posixfs scan" command
* add support for specifying a set of resources when running the
   posixfs scan command scanning, or a singular file to scan, as opposed

 * refactors the resource walking function implemented in
   https://github.com/opencloud-eu/opencloud/pull/3220 in order to
   reuse it for the posixfs scan command

 * implements https://github.com/opencloud-eu/opencloud/issues/3182

 * add --halt-on-error flag

 * collect errors and fail command when more than one error occured
2026-08-03 20:04:39 +02:00
Viktor Scharf 19e44f9b64 [full-ci] chore: bump web to v7.3.0 (#3223)
* chore: bump web to v7.3.0

* Update WEB_COMMITID in .woodpecker.env
2026-08-03 19:58:05 +02:00
Andre Duffeck c93f50dc37 Merge pull request #3233 from aduffeck/extend-posixfs-consistency-check
Do not check ignored paths
2026-08-03 17:50:35 +02:00
Viktor Scharf bb4cd1ebf5 chore: reva bump -2.48.0 (#3230) 2026-08-03 17:43:51 +02:00
dependabot[bot] e12966a4fd build(deps): bump github.com/open-policy-agent/opa from 1.18.2 to 1.19.0
Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 1.18.2 to 1.19.0.
- [Release notes](https://github.com/open-policy-agent/opa/releases)
- [Changelog](https://github.com/open-policy-agent/opa/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-policy-agent/opa/compare/v1.18.2...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/open-policy-agent/opa
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 17:00:51 +02:00
André Duffeck b350b355c0 Do not check ignored paths 2026-08-03 16:29:30 +02:00
dependabot[bot] 9df3881128 build(deps): bump google.golang.org/grpc from 1.82.0 to 1.83.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.82.0 to 1.83.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.82.0...v1.83.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 16:15:46 +02:00
Andre Duffeck 557adfc327 Merge pull request #3220 from aduffeck/extend-posixfs-consistency-check
Extend posixfs consistency check
2026-08-03 14:04:10 +02:00
André Duffeck 97ad525d5b Infer the indexes path from the config instead of calculating it 2026-08-03 13:24:23 +02:00
Ralf Haferkamp 0fd02c5ecf chore(idp): bump dependencies 2026-08-03 13:20:03 +02:00
Viktor Scharf 27a77567dd chore(skills): add skill for bumping reva (#3224)
* chore(skills): add skill for bumping reva

* chore(skills): derive OpenCloud version from open release PR

* chore: exclude .agents from codacy analysis
2026-08-03 13:18:43 +02:00
dependabot[bot] 57a615d6ce 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.14.3 to 2.14.4.
- [Release notes](https://github.com/nats-io/nats-server/releases)
- [Changelog](https://github.com/nats-io/nats-server/blob/main/RELEASES.md)
- [Commits](https://github.com/nats-io/nats-server/compare/v2.14.3...v2.14.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 13:17:14 +02:00
André Duffeck deb3247b99 Move consistency check logic into a checker struct
This makes it possible to get rid of the globals without passing
multiple state vars around.
2026-08-03 12:17:19 +02:00
dependabot[bot] b9adb507f8 build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.13 to 3.4.14
Bumps [github.com/go-ldap/ldap/v3](https://github.com/go-ldap/ldap) from 3.4.13 to 3.4.14.
- [Release notes](https://github.com/go-ldap/ldap/releases)
- [Commits](https://github.com/go-ldap/ldap/compare/v3.4.13...v3.4.14)

---
updated-dependencies:
- dependency-name: github.com/go-ldap/ldap/v3
  dependency-version: 3.4.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 12:16:16 +02:00
André Duffeck c6fba8543f Improve wording 2026-08-03 11:55:14 +02:00
dependabot[bot] 7c2cb14a80 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.23.2 to 1.24.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.24.1/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.23.2...v1.24.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.24.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 11:04:15 +02:00
André Duffeck b125f0681f Fix typos 2026-08-03 11:03:45 +02:00
André Duffeck 20d16c5275 Allow for providing multiple paths to check 2026-08-03 10:58:53 +02:00
dependabot[bot] 639e3657cd build(deps): bump github.com/gabriel-vasile/mimetype
Bumps [github.com/gabriel-vasile/mimetype](https://github.com/gabriel-vasile/mimetype) from 1.4.13 to 1.4.15.
- [Release notes](https://github.com/gabriel-vasile/mimetype/releases)
- [Commits](https://github.com/gabriel-vasile/mimetype/compare/v1.4.13...v1.4.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 10:44:05 +02:00
André Duffeck 559b10e90a Check if the given path is part of the storage 2026-08-03 09:49:10 +02:00
dependabot[bot] e63f1d542a build(deps): bump golang.org/x/net from 0.56.0 to 0.57.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.56.0 to 0.57.0.
- [Commits](https://github.com/golang/net/compare/v0.56.0...v0.57.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 09:26:21 +02:00
dependabot[bot] 51089765d0 build(deps): bump github.com/beevik/etree from 1.6.0 to 1.7.0
Bumps [github.com/beevik/etree](https://github.com/beevik/etree) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/beevik/etree/releases)
- [Changelog](https://github.com/beevik/etree/blob/main/RELEASE_NOTES.md)
- [Commits](https://github.com/beevik/etree/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: github.com/beevik/etree
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 09:25:47 +02:00
André Duffeck 54cced7c56 Streamline logging in the commands 2026-08-03 08:58:19 +02:00
André Duffeck 3d546693c6 Allow for checking specific spaces and files as well 2026-08-03 08:55:49 +02:00
André Duffeck 40ced9997e Also check blobsize and checksums (when --fix-checkums is set) 2026-08-03 08:55:10 +02:00
André Duffeck 3bd188122e Simplify output, remove spinner lib 2026-08-03 08:55:10 +02:00
opencloudeu 9b5849f470 [tx] updated from transifex 2026-07-31 23:16:44 +00:00
Andre Duffeck 942cca8eef Merge pull request #3191 from pbleser-oc/feat/#3182-posixfs-scan-subtree
feat(posixfs): #3182 add basepath option in the "posixfs scan" command
2026-07-31 16:39:48 +02:00
Dominik Schmidt a9f39b85d5 Merge pull request #3140 from opencloud-eu/fix/service-startup-logging
fix(runtime): log service startup errors instead of printing them beside the log
2026-07-31 16:37:19 +02:00
Andre Duffeck 15fc44be94 Merge pull request #3213 from aduffeck/improve-reindex-command
Improve reindex command
2026-07-31 16:34:54 +02:00
André Duffeck 2eb1fe069c Add a --concurrency flag to the index command
It controls the number of spaces that are indexed in parallel. The max
concurrency configured in the server is still considered.
2026-07-31 16:07:18 +02:00
André Duffeck a82b6223aa Remove the SilenceUsage flag again
It will be obsolete once #3140 has been merged
2026-07-31 15:55:36 +02:00
Pascal Bleser 433dea41e8 feat(posixfs): #3182 add basepath option in the "posixfs scan" command
* add support for specifying a basepath using -p when running the
   posixfs scan command, to indicate a directory under which to start
   scanning, or a singular file to scan, as opposed to scanning from the
   storage root directory as is the default behaviour

 * implements https://github.com/opencloud-eu/opencloud/issues/3182
2026-07-31 15:06:50 +02:00
André Duffeck e26a009fc1 Improve "opencloud search index" command
The command no longer times out after 10 minutes when the processing
hasn't been finished yet. Instead it shows progress and logs successful
and failed spaces. The process can also be aborted using ctrl+c.

Example output:

```
$ bin/opencloud search index --all-spaces --insecure --force-rescan
[1/602] indexed space a9033d65-6c13-4556-b923-f321ab33a9ca$eb6a8a0e-2e62-4b40-bce9-360063236676!eb6a8a0e-2e62-4b40-bce9-360063236676 in 5.501190908s
[2/602] indexed space a9033d65-6c13-4556-b923-f321ab33a9ca$34b7c565-da3b-4f53-8559-825e87db4011!34b7c565-da3b-4f53-8559-825e87db4011 in 5.935075697s
[3/602] indexed space a9033d65-6c13-4556-b923-f321ab33a9ca$e08c6026-24f6-4a9e-a805-ed335b34b8da!e08c6026-24f6-4a9e-a805-ed335b34b8da in 6.016350958s
^Caborted, indexing has been stopped
```

Fixes #2592
2026-07-31 14:50:09 +02:00
André Duffeck 49a81c9c1f Cleanup .bingo 2026-07-31 14:50:09 +02:00
Andre Duffeck 7e8179a704 Merge pull request #3207 from aduffeck/concurrent-reindex
Reindex spaces concurrently
2026-07-31 14:49:58 +02:00
André Duffeck d50d8d8900 Rename max concurrency option 2026-07-31 12:04:44 +02:00
André Duffeck 243514ec17 Do not abort reindexing all spaces if one of the spaces fails 2026-07-30 15:34:57 +02:00
André Duffeck be75d8fe95 Reindex spaces concurrently
This speeds up reindexing all spaces which happend sequentially space
after space until now. The level of concurrency can be configured using
the SEARCH_REINDEX_CONCURRENCY env var (3 by default).
2026-07-30 15:25:13 +02:00
Dominik Schmidt bb8cefa33b Merge pull request #3189 from opencloud-eu/feat/announcement-banner
feat: add announcement banner
2026-07-30 10:44:17 +02:00
Viktor Scharf 0a44bbae9e api-test: mark group last-manager removal scenario as flaky (#3194) 2026-07-30 09:50:05 +05:45
opencloudeu fb387a16b6 [tx] updated from transifex 2026-07-29 23:16:50 +00:00
Ralf Haferkamp e358cb6e6e fix(posixfs scan): Setup logger for scan command
Setup a logger for the FS. The nats kv clients for the caches need one
to be able to log NAT connection issues.

Fixes: #3184
2026-07-29 14:29:19 +02:00
Dominik Schmidt 341ebdf6cc refactor: mark the announcement config as output-only (yaml:"-")
The announcement is runtime-managed and injected into config.json; it is not a
static yaml config option. Exclude it from yaml (yaml:"-") so it is not
presented as a configurable knob, keeping the json tag only for the config.json
output.
2026-07-29 12:03:57 +02:00
Dominik Schmidt 6d2d6c38ec fix: fail fast when the announcement store is unreachable
Replace reva's cache.NewNatsKeyValue (which retries with backoff and can hang
the web service startup) with a direct nats.Connect + jetstream setup like the
graph service, so an unreachable or misconfigured store surfaces as a clear
startup error instead of a hang.
2026-07-29 11:53:58 +02:00
Dominik Schmidt d27b7ef40a refactor: drop static config for the announcement, runtime store only
A statically configured announcement is not supported: it cannot be removed via
the runtime API and disappears from the running session once the admin settings
load (they read the store, not config.json). Make the store the single source
of truth, config.json always reflects it and a statically configured
options.announcement is ignored.
2026-07-29 11:52:09 +02:00
Dominik Schmidt 27bdda265f refactor: back the announcement store with plain NATS JetStream
Replace the go-micro store with a NATS JetStream key-value bucket, created via
reva's cache.NewNatsKeyValue, so the web service no longer depends on go-micro
stores. The store methods now take a context, threaded through the config.json
render path and the management handlers. Tests use a mockery mock of
jetstream.KeyValue.
2026-07-29 10:22:43 +02:00
Dominik Schmidt 7d59a83c18 feat: log announcement handler errors with the request id
Add a logger to the announcement service and log server-side errors before
returning a 5xx, tagged with the request id so they correlate with the
request log line from the logging middleware.
2026-07-29 10:08:47 +02:00
Dominik Schmidt 9d472290f2 fix: return 413 when the announcement body exceeds the size cap
Distinguish the http.MaxBytesReader limit from a malformed body: an
oversized payload now returns 413 Request Entity Too Large instead of a
generic 400.
2026-07-29 08:46:46 +02:00
Dominik Schmidt e29d961be3 refactor: rename Announcement.Write to Announcement.ReadWrite
The permission gates reading the full announcement state (including
disabled ones) as well as writing it, so name it ReadWrite to match the
other management permissions (Accounts.ReadWrite, Settings.ReadWrite, ...)
and use the READWRITE operation.
2026-07-29 08:40:29 +02:00
Dominik Schmidt 809d32c4d8 fix: keep a static announcement as fallback when the store is empty
Make the store authoritative only once it manages an announcement: an
enabled record is exposed, a disabled record clears any static config, and
an empty store falls back to a statically configured
web.config.options.announcement. This matches the review request and stops
a static announcement from being silently dropped.
2026-07-29 08:29:58 +02:00
Dominik Schmidt 046f46ce8d fix: make the runtime store authoritative for the announcement
Always reflect the stored announcement in config.json: expose it while live
and clear it otherwise. A disabled announcement now clears the config output
instead of falling back to a statically configured one, which removes the
precedence ambiguity raised in review.
2026-07-29 08:18:56 +02:00
Dominik Schmidt 51dd107767 feat: add announcement banner 2026-07-29 00:39:59 +02:00
Viktor Scharf d9e406ca99 rerun flaky tests (#3183) 2026-07-28 13:44:28 +02:00
Viktor Scharf a2bdbc8b56 api-test: fix removeAccessToDrive.feature:145 (#3179)
* api-test: fix removeAccessToDrive.feature:145

* fix after review
2026-07-28 13:19:26 +02:00
Viktor Scharf 743899055a delete qa report (#3178) 2026-07-28 09:29:44 +02:00
opencloudeu f6ffd125c9 [tx] updated from transifex 2026-07-27 23:16:42 +00:00
Viktor Scharf f3693ef112 api-test: cover additional unified roles in acceptance tests (#3169) 2026-07-27 09:12:38 +02:00
opencloudeu a87163fb14 [tx] updated from transifex 2026-07-25 23:16:48 +00:00
Thomas Schweiger 2649fa0aab Merge pull request #3155 from opencloud-eu/clean-up-auth-app-docu
maint: clean-up auth-app documentation
2026-07-24 21:31:41 +02:00
opencloudeu 12cec14b56 [tx] updated from transifex 2026-07-22 23:16:36 +00:00
Viktor Scharf 44ae3a8ab5 ci: sync tests/README.md to docs (#3164) 2026-07-22 14:27:50 +02:00
Viktor Scharf a3a271aad5 test(apiAuthApp): fix flaky token pattern (#3163) 2026-07-22 17:39:01 +05:45
Florian Schadeandv.scharf 9041ee8df9 feat: add space viewer with versions role (#2961)
* feat: add space viewer with versions role

* fix after review

---------

Co-authored-by: v.scharf <v.scharf@opencloud.eu>
2026-07-22 07:05:51 +02:00
opencloudeu 052636ff0f [tx] updated from transifex 2026-07-21 23:16:43 +00:00
Ralf Haferkamp c8805265e2 chore: don't log non-error message at error level
When walking the tree for creating the search index, ignore "not found"
errors. Search indexing happens asynchronously so the underlying space,
file or folder might just not exist anymore when the indexing starts.
2026-07-21 11:40:44 +02:00
Thomas Schweiger a28ddc8285 maint: clean-up auth-app documentation 2026-07-20 10:37:57 +02:00
opencloudeu 0245a2197b [tx] updated from transifex 2026-07-19 23:16:36 +00:00
Michael Stingl bf3b5c5e1f test(apiArchiver): the single-resource archive is named after the resource (#3080)
An archive download of a single folder was always named "download.zip" until the
handler resolved the resource name for the Content-Disposition header
(https://github.com/opencloud-eu/reva/pull/661). Add a scenario asserting the
archive of a folder carries a Content-Disposition filename of the folder,
reusing the archiver download step and the header-regex assertion.

Verified red->green on posix and decomposed: red on an opencloud built on a reva
with the change reverted (the archive is "download.zip"), green on the shipped
reva.
2026-07-17 16:37:36 +02:00
Michael Stinglandv.scharf 76ffc82bba test(coreApiWebdavUploadTUS): assert etag and permissions on the finalizing TUS chunk (#3078)
* test(coreApiWebdavUploadTUS): assert etag and permissions on the finalizing TUS chunk

The chunked TUS finalize returns OC-ETag, ETag and OC-Perm since
https://github.com/opencloud-eu/reva/pull/718, so a client no longer
needs a follow-up PROPFIND for the new etag and permissions. Add a
scenario asserting these headers on the finalizing chunk, reusing
existing step definitions (the etag is a dynamic quoted hash, hence the
header-regex assertion).

Verified locally red->green: red on stock (headers absent), green on an
opencloud built on a reva carrying that change, on posix and decomposed.

https://github.com/opencloud-eu/opencloud/issues/2409

* test(coreApiWebdavUploadTUS): assert exact OC-Perm and tighten etag regex

---------

Co-authored-by: v.scharf <v.scharf@opencloud.eu>
2026-07-17 14:40:37 +02:00
Michael Stingl 3893be6ed1 test(coreApiWebdavOperations): download a file with a literal "%" via its oc:downloadURL (#3079)
A file whose name contains a literal "%" 404s on download because the
server-built oc:downloadURL did not percent-encode the path (#2852, fixed in
reva by https://github.com/opencloud-eu/reva/pull/653). Add a scenario mirroring
the web client download flow: read the oc:downloadURL from PROPFIND and GET it
as-is (the signature in the URL authenticates it), asserting 200 and the content.

The new step "user X downloads the P file via its oc:downloadURL" lives in
WebDavPropertiesContext; a regex annotation avoids the ":" in oc:downloadURL
colliding with the turnip :placeholder syntax.

Verified red->green on posix and decomposed: red on an opencloud built on a reva
with the fix reverted (the download 404s), green on the shipped reva.

https://github.com/opencloud-eu/opencloud/issues/2852
2026-07-17 14:20:00 +02:00
Viktor Scharf a82aeef2eb add header to release pr. remove 📦️ Dependencies from release description (#3144) 2026-07-17 08:07:28 +02:00
Viktor Scharf e13b86950a fix(ci): replace deprecated runs_on. Removed skipCheckStep() (#3145)
* fix(ci): replace deprecated runs_on

* drop skipCheckStep()

* fix
2026-07-17 08:06:35 +02:00
Alex 2f9d9c582c add version to apps config (#3139) 2026-07-16 11:28:45 +02:00
Jannik Stehle d731cb4c0f Merge pull request #3137 from opencloud-eu/refactor/remove-color-palette-from-theme
refactor(web): remove colorPalette from theme
2026-07-16 06:29:50 +02:00
Dominik Schmidt fec92d2f22 fix(runtime): keep the usage block for flag errors only 2026-07-15 19:01:03 +02:00
Dominik Schmidt 00dd4f1b7f fix(runtime): log service startup errors instead of printing them beside the log 2026-07-15 18:32:04 +02:00
Jannik Stehle e7f850c2aa refactor(web): remove colorPalette from theme
With the recently added new file type icons in Web, the colorPalette
option in the theme.json becomes obsolete. Hence remove it.

Also see https://github.com/opencloud-eu/web/pull/2892
2026-07-15 08:56:37 +02:00
OpenCloud Devopsandv.scharf 85e63ca252 🎉 Release 7.3.0 (#2890)
* 🎉 Release 6.2.1

* 🎉 Release 6.2.1

* 🎉 Release 6.3.0

* 🎉 Release 6.3.0

* 🎉 Release 6.3.0

* 🎉 Release 6.3.0

* 🎉 Release 6.3.0

* 🎉 Release 6.3.0

* 🎉 Release 6.3.0

* 🎉 Release 6.3.0

* 🎉 Release 7.0.0

* add release 7.2.0 to changelog

* add release 7.3.0 to changelog

---------

Co-authored-by: v.scharf <v.scharf@opencloud.eu>
2026-07-14 18:52:14 +02:00
Viktor Scharf 5accb6056b set 7.3.0 version placeholder (#3132) 2026-07-14 18:12:24 +02:00
Elias Schneider 4734c57f2b fix(proxy): honor access token cache ttl 2026-07-14 17:49:40 +02:00
dependabot[bot] fcde95750b build(deps): bump golang.org/x/image from 0.43.0 to 0.44.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.43.0 to 0.44.0.
- [Commits](https://github.com/golang/image/compare/v0.43.0...v0.44.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-14 16:38:06 +02:00
dependabot[bot] eab58b8190 build(deps): bump golang.org/x/text from 0.39.0 to 0.40.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.39.0 to 0.40.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.39.0...v0.40.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-14 16:37:38 +02:00
Jörn Friedrich Dreyer 48e40a385c update collaboration readme
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-07-14 16:29:57 +02:00
Viktor Scharf 4d511a3a42 [full-ci] chore: reva bump -2.47.0 (#3127) 2026-07-14 13:15:54 +02:00
Viktor Scharf a53eb44083 steps refactoring (#3100) 2026-07-14 09:32:35 +02:00
Benedikt Kulmann ea1a1dd5e2 Merge pull request #3121 from opencloud-eu/chore/bump-web-v7.2.0
[full-ci] chore: bump web to v7.2.0
2026-07-13 12:30:10 +02:00
v.scharf ff5bcdee3f add rclone to web-config 2026-07-13 11:56:15 +02:00
v.scharf 559706696a adjust e2e tests in CI 2026-07-13 11:19:51 +02:00
Benedikt Kulmann 02aea2e65e chore: bump web to v7.2.0 2026-07-13 10:29:16 +02:00
Ralf Haferkamp 8c3ef60da1 chore(idp): bump form-data and xmldom
To address issues raised in the security dashboard. Also ran `pnpm
dedupe` for cleanup
2026-07-10 14:46:32 +02:00
dependabot[bot] b5b6a4ee26 build(deps): bump golang.org/x/term from 0.44.0 to 0.45.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/term/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 13:11:45 +02:00
dependabot[bot] e992f952d3 build(deps): bump github.com/coreos/go-oidc/v3 from 3.19.0 to 3.20.0
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.19.0 to 3.20.0.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.19.0...v3.20.0)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-version: 3.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 13:11:02 +02:00
Jörn Friedrich Dreyer e5105ab33f Merge pull request #3097 from opencloud-eu/update-idp-i18n
chore(idp): update i18n, document workflow
2026-07-10 08:38:34 +02:00
opencloudeu 44c51e88ca [tx] updated from transifex 2026-07-09 23:16:27 +00:00
Jörn Friedrich Dreyer bb028a569f chore(idp): update i18n, document workflow
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2026-07-09 14:48:34 +02:00
2610 changed files with 239531 additions and 156924 deletions

No files matched your search

+153
View File
@@ -0,0 +1,153 @@
---
name: bumping-reva
description: Use when the user asks to bump, update, or upgrade the OpenCloud reva dependency to a specific version (e.g. "reva bump to 2.48.0", "bump reva to v2.48.0"). Covers editing go.mod, re-vendoring, bumping the OpenCloud LatestTag, the single commit, and opening the PR against main.
---
# Bumping Reva
## Overview
Bumping reva updates the pinned [opencloud-eu/reva](https://github.com/opencloud-eu/reva) dependency (`github.com/opencloud-eu/reva/v2`) to a tagged release, re-vendors the module graph, and bumps the OpenCloud dev version. It touches `go.mod`, `go.sum`, `vendor/**` and `pkg/version/version.go`, lands as one commit, and ships as a PR whose body is the reva changelog for that version.
Template PR: https://github.com/opencloud-eu/opencloud/pull/3127
## Prerequisites
- **The reva release must already be tagged.** A reva release is cut by merging its release PR (title `🎉 Release X.Y.Z`, branch `next-release/main`). The **user merges that PR themselves** — this skill starts *after* the tag `vX.Y.Z` exists. Verify the tag before doing anything (step 1); if it's missing, stop and ask the user to merge the reva release PR first.
- **`gh` (GitHub CLI), authenticated** — tag lookup and PR creation go through `gh api` / `gh pr`. Verify with `gh auth status`; if it fails, ask the user to run `gh auth login` (suggest `! gh auth login` so it runs in-session).
- `gh` needs the system keyring, so run all `gh` commands with the sandbox disabled.
- **Go toolchain + network** — `go get` / `go mod tidy` / `go mod vendor` hit the Go module proxy. Run them with the sandbox disabled (network access).
- `git` and `base64` (decoding the changelog) — standard on macOS/Linux.
## Inputs
- `REVA_VERSION` — the new reva tag, always normalized to a leading `v` (e.g. `v2.48.0`). If the user didn't give it, ask (or take the latest reva release tag).
- `OC_VERSION` — the OpenCloud target for `LatestTag`, **without** the `+dev` suffix (e.g. `7.4.0`). **Do not ask or guess** — derive it from the open OpenCloud release PR (step 3). It can be a new major (e.g. `8.0.0`) when release-please picked up a breaking change.
## What changes
| File | What |
| ------------------------ | ------------------------------------------------------------------- |
| `go.mod` | `github.com/opencloud-eu/reva/v2``REVA_VERSION` (+ indirect deps pulled by `go mod tidy`) |
| `go.sum` | updated by `go get` / `go mod tidy` |
| `vendor/**` | re-vendored by `go mod vendor` (incl. `vendor/modules.txt`) |
| `pkg/version/version.go` | `LatestTag = "<OC_VERSION>+dev"` |
## Procedure
### 1. Verify the reva tag exists
```bash
gh api repos/opencloud-eu/reva/commits/$REVA_VERSION --jq '.sha'
```
- Success (a sha) → the release is tagged, continue.
- `422`/`404` → the tag does not exist yet. The reva release PR (`🎉 Release X.Y.Z`) is probably not merged. **Stop** and tell the user to merge it first. Helpful checks:
```bash
gh api repos/opencloud-eu/reva/releases/latest --jq '.tag_name' # current latest tag
gh pr list --repo opencloud-eu/reva --search '🎉 Release in:title' --state open --json number,title
```
### 2. Bump the dependency and re-vendor
```bash
go get github.com/opencloud-eu/reva/v2@$REVA_VERSION
go mod tidy
go mod vendor
```
(Sandbox disabled — these need network.) Notes:
- Right after a fresh tag the module proxy can lag; if `go get` reports the version as unknown, retry, or use `GOPROXY=direct go get github.com/opencloud-eu/reva/v2@$REVA_VERSION`.
- `go mod tidy` will also bump indirect dependencies that reva pulled in — that is expected (the template PR did the same).
### 3. Bump the OpenCloud dev version
`OC_VERSION` is the version of the **open OpenCloud release PR** — the release-please PR from branch `next-release/main`, titled `🎉 Release X.Y.Z` (e.g. #3143). Derive it, don't ask:
```bash
gh pr list --repo opencloud-eu/opencloud --head next-release/main --state open \
--json title --jq '.[0].title' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
```
This is the next release target and tracks breaking changes — it may be a new major (e.g. `8.0.0`), not just a minor bump. If no such PR is open, stop and ask the user.
Edit `pkg/version/version.go`:
```go
LatestTag = "<OC_VERSION>+dev" // e.g. "7.4.0+dev"
```
### 4. Fetch the reva changelog for the PR body
```bash
gh api "repos/opencloud-eu/reva/contents/CHANGELOG.md?ref=$REVA_VERSION" --jq '.content' | base64 -d
```
Take **only the section for this version** and trim it exactly like the web bump: start at the first content heading (`### 🐛 Bug Fixes` / `### 📈 Enhancement` / `### 💥 Breaking changes`), drop the `# Changelog` title, the `## [x.y.z] - date` header and the `### ❤️ Thanks to all contributors!` block, and stop before the next `## [...]` version header.
Then **prepend two summary bullets** so the final PR body is:
```
- bump opencloud version to <OC_VERSION>
- reva bump <REVA_VERSION without the leading v>
### 🐛 Bug Fixes
...trimmed reva changelog...
```
Write this to a file for `--body-file` (e.g. in the scratchpad).
### 5. Confirm before committing
The `vendor/` diff is huge — do **not** dump it. Show the user:
```bash
git diff go.mod pkg/version/version.go # the meaningful edits
git diff --stat | tail -1 # vendor churn summary
```
Confirm the reva line in `go.mod` is exactly `github.com/opencloud-eu/reva/v2 REVA_VERSION`, show the target branch (`main`), and the PR body. **Do not commit until the user approves.**
### 6. Commit, push, open PR
- Create a branch (do not commit on `main`), e.g. `reva-bump-2.48.0`.
- Stage everything the bump touched: `git add go.mod go.sum vendor pkg/version/version.go`.
- One commit, conventional-commits format, **empty body**:
```
chore: bump reva to 2.48.0
```
- PR base: **`main`** (reva bumps always target main).
- PR title: `[full-ci] chore: bump reva to 2.48.0` (the commit message prefixed with `[full-ci] `).
- PR body: the file from step 4.
- Add the label `Type:Maintenance`.
```bash
gh pr create --base main \
--title "[full-ci] chore: bump reva to $REVA_VERSION_NO_V" \
--label "Type:Maintenance" \
--body-file <body-file>
```
(`gh` commands need the sandbox disabled — they require the system keyring.)
## Quick reference
```bash
REVA_VERSION=v2.48.0
gh api repos/opencloud-eu/reva/commits/$REVA_VERSION --jq '.sha' # verify tag exists
OC_VERSION=$(gh pr list --repo opencloud-eu/opencloud --head next-release/main --state open \
--json title --jq '.[0].title' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) # from open release PR
go get github.com/opencloud-eu/reva/v2@$REVA_VERSION && go mod tidy && go mod vendor # bump + re-vendor
# edit pkg/version/version.go -> LatestTag = "$OC_VERSION+dev"
gh api "repos/opencloud-eu/reva/contents/CHANGELOG.md?ref=$REVA_VERSION" --jq '.content' | base64 -d # changelog
```
## Common mistakes
- Running the bump before the reva release PR is merged — the tag won't exist and `go get` will fail. Verify the tag first (step 1).
- Forgetting to re-run `go mod vendor` after `go mod tidy`, leaving `vendor/` out of sync with `go.mod`.
- Asking for or hardcoding `OC_VERSION` — always derive it from the open `next-release/main` release PR (step 3); it can even be a new major after a breaking change.
- Reading the changelog from reva `main` instead of the tag (`?ref=$REVA_VERSION`). Always pin to the tag.
- Missing the `[full-ci] ` prefix in the PR title, the `Type:Maintenance` label, or the two summary bullets at the top of the body.
- Dumping the full `vendor/` diff at the confirmation step instead of `go.mod` + `version.go` + a `--stat` summary.
- Committing before the user confirms the diff.
+6
View File
@@ -65,6 +65,12 @@ $(GOVULNCHECK): $(BINGO_DIR)/govulncheck.mod
@echo "(re)installing $(GOBIN)/govulncheck-v1.1.4"
@cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=govulncheck.mod -o=$(GOBIN)/govulncheck-v1.1.4 "golang.org/x/vuln/cmd/govulncheck"
GOWRAP := $(GOBIN)/gowrap-v1.4.3
$(GOWRAP): $(BINGO_DIR)/gowrap.mod
@# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies.
@echo "(re)installing $(GOBIN)/gowrap-v1.4.3"
@cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=gowrap.mod -o=$(GOBIN)/gowrap-v1.4.3 "github.com/hexdigest/gowrap/cmd/gowrap"
MOCKERY := $(GOBIN)/mockery-v3.4.0
$(MOCKERY): $(BINGO_DIR)/mockery.mod
@# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies.
+5
View File
@@ -0,0 +1,5 @@
module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT
go 1.25.8
require github.com/hexdigest/gowrap v1.4.3 // cmd/gowrap
+55
View File
@@ -0,0 +1,55 @@
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hexdigest/gowrap v1.4.3 h1:m+t8aj1pUiFQbEiE8QJg2xdYVH5DAMluLgZ9P/qEF0k=
github.com/hexdigest/gowrap v1.4.3/go.mod h1:XWL8oQW2H3fX5ll8oT3Fduh4mt2H3cUAGQHQLMUbmG4=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.1.2 h1:Th2TIvG1+6ma3e/0/bopBKohOTY7s4dA8V2q4EUcBJ0=
github.com/mitchellh/copystructure v1.1.2/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+2
View File
@@ -24,6 +24,8 @@ GOLANGCI_LINT="${GOBIN}/golangci-lint-v1.64.6"
GOVULNCHECK="${GOBIN}/govulncheck-v1.1.4"
GOWRAP="${GOBIN}/gowrap-v1.4.3"
MOCKERY="${GOBIN}/mockery-v3.4.0"
MUTAGEN="${GOBIN}/mutagen-v0.18.1"
+4
View File
@@ -1,6 +1,7 @@
---
exclude_paths:
- '.github/**'
- '.agents/**'
- 'CHANGELOG.md'
- '**/CHANGELOG.md'
- 'changelog/**'
@@ -15,7 +16,10 @@ exclude_paths:
- 'idp/src/**'
- 'devtools/**'
- 'deployments/**'
- "release-config.ts"
- 'tests/acceptance/expected-failures-*.md'
# written by the search engine parity suite, table rows are as wide as they are
- 'services/search/pkg/parity/README.md'
- 'tests/acceptance/bootstrap/**'
- 'tests/acceptance/TestHelpers/**'
- 'tests/acceptance/scripts/run.sh'
+5 -3
View File
@@ -2,6 +2,8 @@
[Release Template](https://github.com/opencloud-eu/opencloud/blob/main/.github/rolling_release_template.md)
## Prerequisites
* [ ] replace `%%NEXT%%` with the release version
* [ ] web release
* [ ] bump web version
* [ ] squash and merge the web release PR
@@ -14,17 +16,17 @@
## QA Phase
* [ ] bump `opencloud_commitid` in web and run all working tests in CI
* [ ] compatibility test
* [ ] n8n integration QA
* [ ] confirmatory testing, if needed
* [ ] squash and merge Release PR
## Collected bugs
## After QA Phase
* [ ] replace `%%NEXT%%` wuth the release version
* [ ] squash and merge Release PR
* [ ] publish release notes to the docs
* [ ] add migration guide to changelog with prefix `**ACTION REQUIRED:**`, if needed.
* [ ] add release notes from web and reva to opencloud changelog
* [ ] n8n integration - update new opencloud version
* [ ] docker-compose - update new opencloud version
* [ ] update the public matrix channel topic
* [ ] update https://update.opencloud.eu/server.json
* [ ] update the version on demo.opencloud.eu
+2 -3
View File
@@ -36,12 +36,11 @@ vendor-bin/**/composer.lock
vendor-php
# API acceptance tests - auto-generated files
.php-cs-fixer.cache
suite-logs
tests/acceptance/filesForUpload/filesWithVirus/
# QA activity reports
tests/qa-activity-report/reports/
# Generated docs page (published to the server-testing-docs branch by CI)
tests/.docs-dist/
# drone CI is in .drone.star, do not let someone accidentally commit a local .drone.yml
.drone.yml
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
set -euo pipefail
# Purpose of this script is to parse the go.mod file to retrieve the version of
# Go that we are using, either from a "toolchain" directive (preferred), or from
# a "go" directive as a fallback.
#
# The script then outputs that version, where it will be picked up as an environment
# variable in mise.
F="./go.mod"
AWK=awk
# prefer 'gawk' over 'awk' to make sure we get GNU awk
command -v gawk &>/dev/null && AWK=gawk
[[ -e $F ]] || { echo "ERROR: failed to find $F" >&2; exit 1;}
GO_VERSION=$("$AWK" '/^toolchain/ {print $2; exit} /^go / {print $2}' "$F")
GO_VERSION=${GO_VERSION#go} # strip the potential 'go' prefix:
echo "${GO_VERSION}"
-31
View File
@@ -1,31 +0,0 @@
<?php
$dirToParse = 'tests/acceptance/';
$dirIterator = new DirectoryIterator(__DIR__ . '/' . $dirToParse);
$excludeDirs = [
'node_modules',
'vendor-php'
];
$finder = PhpCsFixer\Finder::create()
->exclude($excludeDirs)
->in(__DIR__);
$ocRule = (new OC\CodingStandard\Config())->getRules();
$config = new PhpCsFixer\Config();
$config->setFinder($finder)
->setIndent("\t")
->setRules(
array_merge(
$ocRule,
[
"return_type_declaration" => [
"space_before" => "none",
],
'single_space_around_construct' => true
]
)
);
$config->setFinder($finder);
return $config;
+5
View File
@@ -0,0 +1,5 @@
{
"recommendations": [
"alexkrechik.cucumberautocomplete"
]
}
-151
View File
@@ -76,157 +76,6 @@
"OC_SERVICE_ACCOUNT_SECRET": "service-account-secret"
}
},
{
"name": "OpenCloud server with Groupware",
"type": "go",
"request": "launch",
"mode": "debug",
"buildFlags": [
// "-tags", "enable_vips"
],
"program": "${workspaceFolder}/opencloud/cmd/opencloud",
"args": ["server"],
"env": {
// log settings for human developers
"OC_LOG_LEVEL": "info",
"OC_LOG_PRETTY": "true",
"OC_LOG_COLOR": "true",
// set insecure options because we don't have valid certificates in dev environments
"OC_INSECURE": "true",
// enable basic auth for dev setup so that we can use curl for testing
"PROXY_ENABLE_BASIC_AUTH": "true",
// demo users
"IDM_CREATE_DEMO_USERS": "true",
// OC_RUN_SERVICES allows to start a subset of services even in the supervised mode
//"OC_RUN_SERVICES": "settings,storage-system,graph,idp,idm,ocs,store,thumbnails,web,webdav,frontend,gateway,users,groups,auth-basic,storage-authmachine,storage-users,storage-shares,storage-publiclink,storage-system,app-provider,sharing,proxy,ocdav",
/*
* Keep secrets and passwords in one block to allow easy uncommenting
*/
// user id of "admin", for user creation and admin role assignement
"OC_ADMIN_USER_ID": "some-admin-user-id-0000-000000000000", // FIXME currently must have the length of a UUID, see reva/pkg/storage/utils/decomposedfs/spaces.go:228
// admin user default password
"IDM_ADMIN_PASSWORD": "admin",
// system user
"OC_SYSTEM_USER_ID": "some-system-user-id-000-000000000000", // FIXME currently must have the length of a UUID, see reva/pkg/storage/utils/decomposedfs/spaces.go:228
"OC_SYSTEM_USER_API_KEY": "some-system-user-machine-auth-api-key",
// set some hardcoded secrets
"OC_JWT_SECRET": "some-opencloud-jwt-secret",
"OC_MACHINE_AUTH_API_KEY": "some-opencloud-machine-auth-api-key",
"OC_TRANSFER_SECRET": "some-opencloud-transfer-secret",
// collaboration
"COLLABORATION_WOPIAPP_SECRET": "some-wopi-secret",
// idm ldap
"IDM_SVC_PASSWORD": "some-ldap-idm-password",
"GRAPH_LDAP_BIND_PASSWORD": "some-ldap-idm-password",
// reva ldap
"IDM_REVASVC_PASSWORD": "some-ldap-reva-password",
"GROUPS_LDAP_BIND_PASSWORD": "some-ldap-reva-password",
"USERS_LDAP_BIND_PASSWORD": "some-ldap-reva-password",
"AUTH_BASIC_LDAP_BIND_PASSWORD": "some-ldap-reva-password",
// idp ldap
"IDM_IDPSVC_PASSWORD": "some-ldap-idp-password",
"IDP_LDAP_BIND_PASSWORD": "some-ldap-idp-password",
// storage users mount ID
"GATEWAY_STORAGE_USERS_MOUNT_ID": "storage-users-1",
"STORAGE_USERS_MOUNT_ID": "storage-users-1",
// graph application ID
"GRAPH_APPLICATION_ID": "application-1",
// service accounts
"OC_SERVICE_ACCOUNT_ID": "service-account-id",
"OC_SERVICE_ACCOUNT_SECRET": "service-account-secret",
"OC_ADD_RUN_SERVICES": "auth-api,groupware",
"GROUPWARE_LOG_LEVEL": "trace",
"GROUPWARE_HTTP_TRACE_REQUESTS": "true",
"GROUPWARE_HTTP_TRACE_MAX_REQUEST_BODY_SIZE": "8192",
"GROUPWARE_HTTP_TRACE_RESPONSES": "true",
"GROUPWARE_HTTP_TRACE_MAX_RESPONSE_BODY_SIZE": "8192",
"GROUPWARE_JMAP_MASTER_USERNAME": "admin@example.org",
"GROUPWARE_JMAP_MASTER_PASSWORD": "admin",
"GROUPWARE_SEND_DURATIONS_RESPONSE": "true",
"GROUPWARE_TLS_INSECURE": "true",
"GROUPWARE_ENABLE_MOCK_DATA": "true",
"GROUPWARE_DEBUG_ADDR": "0.0.0.0:9203",
"AUTHAPI_HTTP_ADDR": "0.0.0.0:10000",
"AUTHAPI_AUTH_REQUIRE_SHARED_SECRET": "true",
"AUTHAPI_AUTH_SHARED_SECRETS": "stalwart=maethaR9eiXaiph8ahn8ohH6dahPiequ;unused=eeyaigh6hae1zo5ahGeete6oohaiquei",
"WEB_ASSET_CORE_PATH": "${workspaceFolder}/../web/dist",
"WEB_UI_CONFIG_FILE": "${workspaceFolder}/../web/dev/docker/opencloud.web.config.json",
"FRONTEND_GROUPWARE_ENABLED": "true"
}
},
{
"name": "OpenCloud server with external services",
"type": "go",
"request": "launch",
"mode": "debug",
"buildFlags": [
// "-tags", "enable_vips"
],
"program": "${workspaceFolder}/opencloud/cmd/opencloud",
"args": ["server"],
"env": {
"OC_URL": "https://localhost:9200/",
"PROXY_DEBUG_ADDR": "0.0.0.0:9205",
"OC_BASE_DATA_PATH": "${env:HOME}/.opencloud-with-external",
"OC_CONFIG_DIR": "${env:HOME}/.opencloud-with-external/config",
"GROUPWARE_LOG_LEVEL": "trace",
"OC_LOG_LEVEL": "info",
"OC_LOG_PRETTY": "true",
"OC_LOG_COLOR": "true",
"OC_INSECURE": "true",
"PROXY_ENABLE_BASIC_AUTH": "false",
"IDM_CREATE_DEMO_USERS": "false",
"OC_LDAP_URI": "ldaps://localhost:636",
"OC_LDAP_INSECURE": "true",
"OC_LDAP_BIND_DN": "cn=admin,dc=opencloud,dc=eu",
"OC_LDAP_BIND_PASSWORD": "admin",
"OC_LDAP_GROUP_BASE_DN": "ou=groups,dc=opencloud,dc=eu",
"OC_LDAP_GROUP_SCHEMA_ID": "entryUUID",
"OC_LDAP_USER_BASE_DN": "ou=users,dc=opencloud,dc=eu",
"OC_LDAP_USER_FILTER": "(objectclass=inetOrgPerson)",
"OC_LDAP_USER_SCHEMA_ID": "entryUUID",
"OC_LDAP_DISABLE_USER_MECHANISM": "none",
"OC_LDAP_SERVER_WRITE_ENABLED": "false",
"OC_EXCLUDE_RUN_SERVICES": "idm",
"OC_ADD_RUN_SERVICES": "notifications,groupware",
"NATS_NATS_HOST": "0.0.0.0",
"NATS_NATS_PORT": "9233",
"FRONTEND_ARCHIVER_MAX_SIZE": "10000000000",
"MICRO_REGISTRY_ADDRESS": "127.0.0.1:9233",
"NOTIFICATIONS_SMTP_HOST": "localhost",
"NOTIFICATIONS_SMTP_PORT": "2500",
"NOTIFICATIONS_SMTP_SENDER": "OpenCloud notifications <notifications@cloud.opencloud.test>",
"NOTIFICATIONS_SMTP_USERNAME": "notifications@cloud.opencloud.test",
"NOTIFICATIONS_SMTP_INSECURE": "true",
"NOTIFICATIONS_SMTP_PASSWORD": "",
"NOTIFICATIONS_SMTP_AUTHENTICATION": "",
"NOTIFICATIONS_SMTP_ENCRYPTION": "none",
"PROXY_AUTOPROVISION_ACCOUNTS": "false",
"PROXY_ROLE_ASSIGNMENT_DRIVER": "oidc",
"OC_OIDC_ISSUER": "https://keycloak.opencloud.test/realms/openCloud",
"PROXY_OIDC_REWRITE_WELLKNOWN": "true",
"WEB_OIDC_CLIENT_ID": "web",
"PROXY_USER_OIDC_CLAIM": "uuid",
"PROXY_USER_CS3_CLAIM": "userid",
"WEB_OPTION_ACCOUNT_EDIT_LINK_HREF": "https://keycloak.opencloud.test/realms/openCloud/account",
"OC_ADMIN_USER_ID": "",
"SETTINGS_SETUP_DEFAULT_ASSIGNMENTS": "false",
"GRAPH_ASSIGN_DEFAULT_USER_ROLE": "false",
"GRAPH_USERNAME_MATCH": "none",
"KEYCLOAK_DOMAIN": "keycloak.opencloud.test",
"IDM_ADMIN_PASSWORD": "admin",
"GRAPH_LDAP_SERVER_UUID": "true",
"GRAPH_LDAP_GROUP_CREATE_BASE_DN": "ou=custom,ou=groups,dc=opencloud,dc=eu",
"GRAPH_LDAP_REFINT_ENABLED": "true",
"GATEWAY_GRPC_ADDR": "0.0.0.0:9142",
}
},
{
"name": "Fed OpenCloud server",
"type": "go",
+16
View File
@@ -0,0 +1,16 @@
{
"cucumberautocomplete.steps": [
"tests/acceptance/bootstrap/*.php"
],
"cucumberautocomplete.syncfeatures": "tests/acceptance/features/**/*.feature",
"cucumberautocomplete.strictGherkinCompletion": false,
"cucumberautocomplete.strictGherkinValidation": false,
"cucumberautocomplete.customParameters": [
{ "parameter": ":string", "value": "\"([^\"]*)\"" },
{ "parameter": "'/\\^(.*)\\$/'", "value": "'^$1$$'", "isRegex": true, "flags": "g" },
{ "parameter": "\\$\\w+", "value": "(.*)", "isRegex": true, "flags": "g" },
{ "parameter": "(?<!\\?):[a-zA-Z_][a-zA-Z0-9_]*", "value": "\"([^\"]*)\"", "isRegex": true, "flags": "g" }
],
"cucumberautocomplete.gherkinDefinitionPart": "(Given|When|Then|But)\\(",
"cucumberautocomplete.pureTextSteps": false
}
+1 -1
View File
@@ -1,3 +1,3 @@
# The test runner source for UI tests
WEB_COMMITID=47a1cfb1dbda4479b88e831d089bfa54b1f4bb8d
WEB_COMMITID=dffb3808702dab198065faa7a8aa16228e048385
WEB_BRANCH=main
+338 -200
View File
File diff suppressed because it is too large. Load diff
+198
View File
@@ -1,5 +1,203 @@
# Changelog
## [7.5.0](https://github.com/opencloud-eu/opencloud/releases/tag/v7.5.0) - 2026-08-25
### ❤️ Thanks to all contributors! ❤️
@AlexAndBear, @JammingBen, @Svanvith, @aduffeck, @butonic, @fschade, @junkerderprovinz, @kulmann, @maki5, @pbleser-oc, @rhafer, @saw-jan, @schweigisito, @v-scharf
### ✅ Tests
- test(api): update php test dependencies [[#3335](https://github.com/opencloud-eu/opencloud/pull/3335)]
- fix(acceptance): fix running acceptance tests against host on Linux [[#3358](https://github.com/opencloud-eu/opencloud/pull/3358)]
- fix(graph): adding the same user as multiple members in a group (#3354) [[#3356](https://github.com/opencloud-eu/opencloud/pull/3356)]
- test(api): fix share role update test scenario [[#3322](https://github.com/opencloud-eu/opencloud/pull/3322)]
- test: add api tests for cross-space search index mutation [[#3320](https://github.com/opencloud-eu/opencloud/pull/3320)]
- ci: run search acceptance tests against OpenSearch in nightly [[#3302](https://github.com/opencloud-eu/opencloud/pull/3302)]
- api-test: notification settings and getting email notifications [[#3281](https://github.com/opencloud-eu/opencloud/pull/3281)]
- api-test: add posixfs scan and consistency CLI tests [[#3263](https://github.com/opencloud-eu/opencloud/pull/3263)]
- api-test: replace sleeps with WaitHelper poll for async state [[#3239](https://github.com/opencloud-eu/opencloud/pull/3239)]
### 📈 Enhancement
- feat(thumbnails): extend list of default resolutions [[#3386](https://github.com/opencloud-eu/opencloud/pull/3386)]
- ability to disable grpc and/or event consumer for event history service [[#3279](https://github.com/opencloud-eu/opencloud/pull/3279)]
- feat(graph): add LibreGraphContentType on drive [[#3355](https://github.com/opencloud-eu/opencloud/pull/3355)]
- feat: update space template image [[#3324](https://github.com/opencloud-eu/opencloud/pull/3324)]
- feat(web): add rclone-crypt to default apps [[#3313](https://github.com/opencloud-eu/opencloud/pull/3313)]
- enhance: send events for adding/removing favourite items [[#3229](https://github.com/opencloud-eu/opencloud/pull/3229)]
- allow tuning the proxies http client [[#3278](https://github.com/opencloud-eu/opencloud/pull/3278)]
- feat(web): add yjsServerUrl config [[#3259](https://github.com/opencloud-eu/opencloud/pull/3259)]
- feat(posixfs): #3182 add basepath option in the "posixfs scan" command [[#3235](https://github.com/opencloud-eu/opencloud/pull/3235)]
### 🐛 Bug Fixes
- fix(search): refresh the opensearch index after a write [[#3388](https://github.com/opencloud-eu/opencloud/pull/3388)]
- fix(thumbnails): respect the requested height in libvips builds [[#3377](https://github.com/opencloud-eu/opencloud/pull/3377)]
- refactor datagateway into proxy middleware [[#3289](https://github.com/opencloud-eu/opencloud/pull/3289)]
- Only log a debug message when an item is still in processing state [[#3158](https://github.com/opencloud-eu/opencloud/pull/3158)]
- fix(postprocessing): retry publishing events instead of killing the server [[#3347](https://github.com/opencloud-eu/opencloud/pull/3347)]
- actually log error on exit [[#3344](https://github.com/opencloud-eu/opencloud/pull/3344)]
- fix(activitylog): log missing parent id cache entry at debug level [[#3325](https://github.com/opencloud-eu/opencloud/pull/3325)]
- fix restore file version for shared resource [[#3268](https://github.com/opencloud-eu/opencloud/pull/3268)]
- Fix missing favorite flag on opensearch hits [[#3252](https://github.com/opencloud-eu/opencloud/pull/3252)]
### 📚 Documentation
- docs: clarify custom role bootstrap behavior [[#3366](https://github.com/opencloud-eu/opencloud/pull/3366)]
### 📦️ Dependencies
- [full-ci] chore: bump web to v7.4.0 [[#3399](https://github.com/opencloud-eu/opencloud/pull/3399)]
- build(deps): bump google.golang.org/grpc from 1.83.0 to 1.83.1 [[#3396](https://github.com/opencloud-eu/opencloud/pull/3396)]
- build(deps): bump github.com/stretchr/testify from 1.12.0 to 1.12.1 [[#3395](https://github.com/opencloud-eu/opencloud/pull/3395)]
- build(deps): bump github.com/go-chi/chi/v5 from 5.3.1 to 5.3.2 [[#3394](https://github.com/opencloud-eu/opencloud/pull/3394)]
- build(deps): bump github.com/grpc-ecosystem/grpc-gateway/v2 from 2.29.0 to 2.30.0 [[#3380](https://github.com/opencloud-eu/opencloud/pull/3380)]
- build(deps): bump golang.org/x/image from 0.44.0 to 0.45.0 [[#3381](https://github.com/opencloud-eu/opencloud/pull/3381)]
- build(deps): bump github.com/beevik/etree from 1.7.0 to 1.7.1 [[#3382](https://github.com/opencloud-eu/opencloud/pull/3382)]
- build(deps): bump github.com/nats-io/nats-server/v2 from 2.14.4 to 2.14.5 [[#3357](https://github.com/opencloud-eu/opencloud/pull/3357)]
- build(deps): bump go.opentelemetry.io/contrib/zpages from 0.69.0 to 0.70.0 [[#3360](https://github.com/opencloud-eu/opencloud/pull/3360)]
- build(deps): bump github.com/sirupsen/logrus from 1.9.4 to 1.10.0 [[#3359](https://github.com/opencloud-eu/opencloud/pull/3359)]
- chore: bump reva to latest main [[#3329](https://github.com/opencloud-eu/opencloud/pull/3329)]
- build(deps): bump github.com/stretchr/testify from 1.11.1 to 1.12.0 [[#3341](https://github.com/opencloud-eu/opencloud/pull/3341)]
- build(deps): bump golang.org/x/net from 0.57.0 to 0.58.0 [[#3342](https://github.com/opencloud-eu/opencloud/pull/3342)]
- build(deps): bump github.com/onsi/ginkgo/v2 from 2.32.0 to 2.32.1 [[#3340](https://github.com/opencloud-eu/opencloud/pull/3340)]
- build(deps): bump github.com/testcontainers/testcontainers-go/modules/opensearch from 0.43.0 to 0.44.0 [[#3303](https://github.com/opencloud-eu/opencloud/pull/3303)]
- build(deps): bump github.com/opencloud-eu/libre-graph-api-go [[#3326](https://github.com/opencloud-eu/opencloud/pull/3326)]
- build(deps): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace from 1.44.0 to 1.45.0 [[#3306](https://github.com/opencloud-eu/opencloud/pull/3306)]
- build(deps): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp from 0.69.0 to 0.70.0 [[#3305](https://github.com/opencloud-eu/opencloud/pull/3305)]
- build(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc from 1.44.0 to 1.45.0 [[#3280](https://github.com/opencloud-eu/opencloud/pull/3280)]
- build(deps): bump github.com/rogpeppe/go-internal from 1.15.0 to 1.16.0 [[#3250](https://github.com/opencloud-eu/opencloud/pull/3250)]
- build(deps): bump github.com/kovidgoyal/imaging from 1.8.22 to 1.8.23 [[#3135](https://github.com/opencloud-eu/opencloud/pull/3135)]
- build(deps): bump go.opentelemetry.io/otel/trace from 1.44.0 to 1.45.0 [[#3266](https://github.com/opencloud-eu/opencloud/pull/3266)]
## [7.4.0](https://github.com/opencloud-eu/opencloud/releases/tag/v7.4.0) - 2026-08-03
### ❤️ Thanks to all contributors! ❤️
@AlexAndBear, @JammingBen, @aduffeck, @dschmidt, @fschade, @michaelstingl, @pbleser-oc, @rhafer, @schweigisito, @v-scharf
### 📈 Enhancement
- Do not check ignored paths [[#3233](https://github.com/opencloud-eu/opencloud/pull/3233)]
- Extend posixfs consistency check [[#3220](https://github.com/opencloud-eu/opencloud/pull/3220)]
- Improve reindex command [[#3213](https://github.com/opencloud-eu/opencloud/pull/3213)]
- Reindex spaces concurrently [[#3207](https://github.com/opencloud-eu/opencloud/pull/3207)]
- feat: add announcement banner [[#3189](https://github.com/opencloud-eu/opencloud/pull/3189)]
- feat: add space viewer with versions role [[#2961](https://github.com/opencloud-eu/opencloud/pull/2961)]
### 🐛 Bug Fixes
- fix(runtime): log service startup errors instead of printing them beside the log [[#3140](https://github.com/opencloud-eu/opencloud/pull/3140)]
- fix(posixfs scan): Setup logger for scan command [[#3185](https://github.com/opencloud-eu/opencloud/pull/3185)]
### ✅ Tests
- api-test: mark group last-manager removal scenario as flaky [[#3194](https://github.com/opencloud-eu/opencloud/pull/3194)]
- rerun flaky tests [[#3183](https://github.com/opencloud-eu/opencloud/pull/3183)]
- api-test: fix removeAccessToDrive.feature:145 [[#3179](https://github.com/opencloud-eu/opencloud/pull/3179)]
- api-test: cover additional unified roles in acceptance tests [[#3169](https://github.com/opencloud-eu/opencloud/pull/3169)]
- test(apiAuthApp): fix flaky token pattern [[#3163](https://github.com/opencloud-eu/opencloud/pull/3163)]
- test(apiArchiver): the single-resource archive is named after the resource [[#3080](https://github.com/opencloud-eu/opencloud/pull/3080)]
- test(coreApiWebdavUploadTUS): assert etag and permissions on the finalizing TUS chunk [[#3078](https://github.com/opencloud-eu/opencloud/pull/3078)]
- test(coreApiWebdavOperations): download a file with a literal "%" via its oc:downloadURL [[#3079](https://github.com/opencloud-eu/opencloud/pull/3079)]
### 📚 Documentation
- maint: clean-up auth-app documentation [[#3155](https://github.com/opencloud-eu/opencloud/pull/3155)]
- ci: sync tests/README.md to docs [[#3164](https://github.com/opencloud-eu/opencloud/pull/3164)]
### 📦️ Dependencies
- [full-ci] chore: bump web to v7.3.0 [[#3223](https://github.com/opencloud-eu/opencloud/pull/3223)]
- build(deps): bump github.com/open-policy-agent/opa from 1.18.2 to 1.19.0 [[#3231](https://github.com/opencloud-eu/opencloud/pull/3231)]
- build(deps): bump google.golang.org/grpc from 1.82.0 to 1.83.0 [[#3232](https://github.com/opencloud-eu/opencloud/pull/3232)]
- chore(idp): bump dependencies [[#3226](https://github.com/opencloud-eu/opencloud/pull/3226)]
- build(deps): bump github.com/nats-io/nats-server/v2 from 2.14.3 to 2.14.4 [[#3221](https://github.com/opencloud-eu/opencloud/pull/3221)]
- build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.13 to 3.4.14 [[#3222](https://github.com/opencloud-eu/opencloud/pull/3222)]
- build(deps): bump github.com/prometheus/client_golang from 1.23.2 to 1.24.1 [[#3218](https://github.com/opencloud-eu/opencloud/pull/3218)]
- build(deps): bump github.com/gabriel-vasile/mimetype from 1.4.13 to 1.4.15 [[#3217](https://github.com/opencloud-eu/opencloud/pull/3217)]
- build(deps): bump golang.org/x/net from 0.56.0 to 0.57.0 [[#3136](https://github.com/opencloud-eu/opencloud/pull/3136)]
- build(deps): bump github.com/beevik/etree from 1.6.0 to 1.7.0 [[#3134](https://github.com/opencloud-eu/opencloud/pull/3134)]
## [7.3.0](https://github.com/opencloud-eu/opencloud/releases/tag/v7.3.0) - 2026-07-14
### 🐛 Bug Fixes
- fix(proxy): honor access token cache ttl [[#3056](https://github.com/opencloud-eu/opencloud/pull/3056)]
- chore(idp): update axios [[#3094](https://github.com/opencloud-eu/opencloud/pull/3094)]
- fix: make the collaboration service events optional [[#3001](https://github.com/opencloud-eu/opencloud/pull/3001)]
- Revert "fix: disallow thumbnails for tiff and jpeg2000 images" [[#2973](https://github.com/opencloud-eu/opencloud/pull/2973)]
- bump reva [[#2950](https://github.com/opencloud-eu/opencloud/pull/2950)]
- change error level for trashing items interaction with search [[#2951](https://github.com/opencloud-eu/opencloud/pull/2951)]
- fix: Send SSE events for SpaceEnabled/Disabled to affected users [[#2871](https://github.com/opencloud-eu/opencloud/pull/2871)]
### 📚 Documentation
- update collaboration readme [[#3076](https://github.com/opencloud-eu/opencloud/pull/3076)]
- fix: fix typo in proxy service documentation [[#3089](https://github.com/opencloud-eu/opencloud/pull/3089)]
- roling release template [[#2972](https://github.com/opencloud-eu/opencloud/pull/2972)]
- enhance: fix typos in webfinger service description [[#2958](https://github.com/opencloud-eu/opencloud/pull/2958)]
### ✅ Tests
- [full-ci] gherkin steps refactoring. cleaning code [[#3100](https://github.com/opencloud-eu/opencloud/pull/3100)]
- [decomposed] more cli command tests [[#3087](https://github.com/opencloud-eu/opencloud/pull/3087)]
- test(apiSpaces): a space admin can delete a space with no manager [[#3040](https://github.com/opencloud-eu/opencloud/pull/3040)]
- Update tests for opencloud-eu/reva#655 [[#2889](https://github.com/opencloud-eu/opencloud/pull/2889)]
- api-test: deleting space [[#2970](https://github.com/opencloud-eu/opencloud/pull/2970)]
### 📈 Enhancement
- feat: add disableSponsorLink web config option [[#3093](https://github.com/opencloud-eu/opencloud/pull/3093)]
- feat(graph): add MS Graph colon-syntax path lookup middleware [[#2688](https://github.com/opencloud-eu/opencloud/pull/2688)]
- feat: adjust theme chrome colors and logos [[#2599](https://github.com/opencloud-eu/opencloud/pull/2599)]
- add tls support for all nats connections [[#2063](https://github.com/opencloud-eu/opencloud/pull/2063)]
- feat: add more roles [[#2928](https://github.com/opencloud-eu/opencloud/pull/2928)]
- next to main [[#2924](https://github.com/opencloud-eu/opencloud/pull/2924)]
- feat: add core apps env variable to override the default core apps [[#2930](https://github.com/opencloud-eu/opencloud/pull/2930)]
### 📦️ Dependencies
- build(deps): bump golang.org/x/image from 0.43.0 to 0.44.0 [[#3112](https://github.com/opencloud-eu/opencloud/pull/3112)]
- build(deps): bump golang.org/x/text from 0.39.0 to 0.40.0 [[#3101](https://github.com/opencloud-eu/opencloud/pull/3101)]
- [full-ci] chore: bump web to v7.2.0 [[#3121](https://github.com/opencloud-eu/opencloud/pull/3121)]
- build(deps): bump golang.org/x/term from 0.44.0 to 0.45.0 [[#3103](https://github.com/opencloud-eu/opencloud/pull/3103)]
- build(deps): bump github.com/coreos/go-oidc/v3 from 3.19.0 to 3.20.0 [[#3102](https://github.com/opencloud-eu/opencloud/pull/3102)]
- build(deps): bump golang.org/x/text from 0.38.0 to 0.39.0 [[#3085](https://github.com/opencloud-eu/opencloud/pull/3085)]
- build(deps): bump github.com/go-chi/chi/v5 from 5.3.0 to 5.3.1 [[#3073](https://github.com/opencloud-eu/opencloud/pull/3073)]
- build(deps): bump github.com/nats-io/nats-server/v2 from 2.14.2 to 2.14.3 [[#3063](https://github.com/opencloud-eu/opencloud/pull/3063)]
- build(deps): bump github.com/gookit/config/v2 from 2.2.8 to 2.2.9 [[#3075](https://github.com/opencloud-eu/opencloud/pull/3075)]
- build(deps): bump github.com/open-policy-agent/opa from 1.18.1 to 1.18.2 [[#3061](https://github.com/opencloud-eu/opencloud/pull/3061)]
- build(deps): bump github.com/kovidgoyal/imaging from 1.8.21 to 1.8.22 [[#3060](https://github.com/opencloud-eu/opencloud/pull/3060)]
- build(deps): bump github.com/libregraph/lico from 0.66.0 to 0.67.0 [[#3028](https://github.com/opencloud-eu/opencloud/pull/3028)]
- chore: bump web to v7.2.0-beta.3 [[#2953](https://github.com/opencloud-eu/opencloud/pull/2953)]
- chore: bump reva to latest main [[#2943](https://github.com/opencloud-eu/opencloud/pull/2943)]
## [7.2.0](https://github.com/opencloud-eu/opencloud/releases/tag/v7.2.0) - 2026-06-24
### ❤️ Thanks to all contributors! ❤️
@Heiko-Pohl, @JammingBen, @ScharfViktor, @aduffeck, @butonic, @dragonchaser, @kulmann, @rhafer
### 🐛 Bug Fixes
- [stable-7.2] Backport fixes from main [[#2999](https://github.com/opencloud-eu/opencloud/pull/2999)]
- fix(idp): aarch64 build [[#2906](https://github.com/opencloud-eu/opencloud/pull/2906)]
- use ldap instead of ldaps internally [[#2880](https://github.com/opencloud-eu/opencloud/pull/2880)]
### 📚 Documentation
- Rename role_name from "guest" to "user-light" [[#2912](https://github.com/opencloud-eu/opencloud/pull/2912)]
### 📦️ Dependencies
- [full-ci] chore: bump web to v7.1.2 [[#3012](https://github.com/opencloud-eu/opencloud/pull/3012)]
- [full-ci] chore: bump web to v7.1.1 [[#2998](https://github.com/opencloud-eu/opencloud/pull/2998)]
- bump reva to latest main [[#2922](https://github.com/opencloud-eu/opencloud/pull/2922)]
- build(deps-dev): bump webpack-manifest-plugin from 5.0.0 to 6.0.1 in /services/idp [[#2884](https://github.com/opencloud-eu/opencloud/pull/2884)]
- build(deps): bump axios from 1.16.0 to 1.16.1 in /services/idp [[#2883](https://github.com/opencloud-eu/opencloud/pull/2883)]
## [7.1.0](https://github.com/opencloud-eu/opencloud/releases/tag/v7.1.0) - 2026-06-02
### ❤️ Thanks to all contributors! ❤️
+8 -30
View File
@@ -27,7 +27,6 @@ OC_MODULES = \
services/app-provider \
services/app-registry \
services/audit \
services/auth-api \
services/auth-app \
services/auth-basic \
services/auth-bearer \
@@ -40,7 +39,6 @@ OC_MODULES = \
services/gateway \
services/graph \
services/groups \
services/groupware \
services/idm \
services/idp \
services/invitations \
@@ -70,11 +68,8 @@ OC_MODULES = \
protogen
# bin file definitions
PHP_CS_FIXER=php -d zend.enable_gc=0 vendor-bin/opencloud-codestyle/vendor/bin/php-cs-fixer
PHP_CODESNIFFER=vendor-bin/php_codesniffer/vendor/bin/phpcs
PHP_CODEBEAUTIFIER=vendor-bin/php_codesniffer/vendor/bin/phpcbf
PHAN=php -d zend.enable_gc=0 vendor-bin/phan/vendor/bin/phan
PHPSTAN=php -d zend.enable_gc=0 vendor-bin/phpstan/vendor/bin/phpstan
PHP_CODESNIFFER=vendor-bin/opencloud-codestyle/vendor/bin/phpcs
PHP_CODEBEAUTIFIER=vendor-bin/opencloud-codestyle/vendor/bin/phpcbf
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include .bingo/Variables.mk
@@ -109,6 +104,7 @@ help:
@echo -e "${GREEN}Tools for linting gherkin feature files:\n${RESET}"
@echo -e "\tmake test-gherkin-lint\t\t${BLUE}run lint checks on Gherkin feature files${RESET}"
@echo -e "\tmake test-gherkin-lint-fix\t${BLUE}apply lint fixes to gherkin feature files${RESET}"
@echo -e "\tmake find-unused-steps\t\t${BLUE}list Behat step definitions unused by any .feature file${RESET}"
@echo
.PHONY: clean-tests
@@ -230,6 +226,10 @@ test-gherkin-lint:
test-gherkin-lint-fix:
gherlint --fix tests/acceptance/features -c tests/acceptance/config/.gherlintrc.json
.PHONY: find-unused-steps
find-unused-steps: vendor-bin/behat/vendor
tests/acceptance/scripts/find-unused-steps.sh
.PHONY: bingo-update
bingo-update: $(BINGO)
$(BINGO) get -l -v -t 20
@@ -316,37 +316,15 @@ ci-format: $(BUILDIFIER)
$(BUILDIFIER) --mode=fix .woodpecker.star
.PHONY: test-php-style
test-php-style: vendor-bin/opencloud-codestyle/vendor vendor-bin/php_codesniffer/vendor
$(PHP_CS_FIXER) fix -v --diff --allow-risky yes --dry-run
test-php-style: vendor-bin/opencloud-codestyle/vendor
$(PHP_CODESNIFFER) --cache --runtime-set ignore_warnings_on_exit --standard=phpcs.xml tests/acceptance tests/acceptance/TestHelpers
.PHONY: test-php-style-fix
test-php-style-fix: vendor-bin/opencloud-codestyle/vendor
$(PHP_CS_FIXER) fix -v --diff --allow-risky yes
$(PHP_CODEBEAUTIFIER) --cache --runtime-set ignore_warnings_on_exit --standard=phpcs.xml tests/acceptance
.PHONY: vendor-bin-codestyle
vendor-bin-codestyle: vendor-bin/opencloud-codestyle/vendor
.PHONY: vendor-bin-codesniffer
vendor-bin-codesniffer: vendor-bin/php_codesniffer/vendor
vendor-bin/opencloud-codestyle/vendor: vendor/bamarni/composer-bin-plugin vendor-bin/opencloud-codestyle/composer.lock
composer bin opencloud-codestyle install --no-progress
vendor-bin/opencloud-codestyle/composer.lock: vendor-bin/opencloud-codestyle/composer.json
@echo opencloud-codestyle composer.lock is not up to date.
vendor-bin/php_codesniffer/vendor: vendor/bamarni/composer-bin-plugin vendor-bin/php_codesniffer/composer.lock
composer bin php_codesniffer install --no-progress
vendor-bin/php_codesniffer/composer.lock: vendor-bin/php_codesniffer/composer.json
@echo php_codesniffer composer.lock is not up to date.
.PHONY: generate-qa-activity-report
generate-qa-activity-report: node_modules
@if [ -z "${MONTH}" ] || [ -z "${YEAR}" ]; then \
echo "Please set the MONTH and YEAR environment variables. Usage: make generate-qa-activity-report MONTH=<month> YEAR=<year>"; \
exit 1; \
fi
go run tests/qa-activity-report/generate-qa-activity-report.go --month ${MONTH} --year ${YEAR}
-62
View File
@@ -1,62 +0,0 @@
# Table of Contents
{{ range . -}}
* [Changelog for {{ .Version }}](#changelog-for-{{ .Version | replace "." ""}}-{{ .Date | lower -}})
{{ end -}}
{{ $allVersions := . }}
{{- range $index, $changes := . }}{{ with $changes -}}
{{ if gt (len $allVersions) 1 }}
# Changelog for [{{ .Version }}] ({{ .Date }})
The following sections list the changes for {{ .Version}}.
{{/* creating version compare links */ -}}
{{ $next := add1 $index -}}
{{ if ne (len $allVersions) $next -}}
{{ $previousVersion := (index $allVersions $next).Version -}}
{{ if eq .Version "unreleased" -}}
[{{ .Version}}]: https://github.com/opencloud-eu/opencloud/compare/v{{ $previousVersion }}...master
{{ else -}}
[{{ .Version}}]: https://github.com/opencloud-eu/opencloud/compare/v{{ $previousVersion }}...v{{ .Version}}
{{ end -}}
{{ end -}}
{{- /* last version managed by calens, end of the loop */ -}}
{{ if eq .Version "0.1.0" -}}
[{{ .Version }}]: https://github.com/opencloud-eu/opencloud/compare/94f19e653e30cdf16dcf23dbaf36c6d753d37ae9...v{{ .Version }}
{{ end -}}
{{ else -}}
# Changes in {{ .Version}}
{{ end -}}
## Summary
{{ range $entry := .Entries }}{{ with $entry }}
* {{ .Type }} - {{ .Title }}: [#{{ .PrimaryID }}]({{ .PrimaryURL }})
{{- end }}{{ end }}
## Details
{{ range $entry := .Entries }}{{ with $entry }}
* {{ .Type }} - {{ .Title }}: [#{{ .PrimaryID }}]({{ .PrimaryURL }})
{{ range $par := .Paragraphs -}}
{{/* Workaround for keeping lists inside of changelog items well formatted */ -}}
{{ if hasPrefix "*" $par }}
{{ $par | replace " *" "\n *" }}
{{- else }}
{{ wrapIndent $par 80 3 -}}
{{ end }}
{{ end -}}
{{ range $url := .IssueURLs }}
{{ $url -}}
{{ end -}}
{{ range $url := .PRURLs }}
{{ $url -}}
{{ end -}}
{{ range $url := .OtherURLs }}
{{ $url -}}
{{ end }}
{{ end }}{{ end -}}
{{ end }}{{ end -}}
-16
View File
@@ -1,16 +0,0 @@
# Changelog
We are using [calens](https://github.com/restic/calens) to properly generate a
changelog before we are tagging a new release. To get an idea how this could
look like <https://github.com/restic/restic/tree/master/changelog> would be the
best reference.
## Create changelog items
Create a file according to the template for each changelog in the unreleased folder.
The following change types are possible:
- Bugfix (general Bugfix)
- Enhancement (new feature)
- Change (breaking change)
- Security (security related issues)
-15
View File
@@ -1,15 +0,0 @@
Bugfix: Fix behavior for foobar (in present tense)
We've fixed the behavior for foobar, a long-standing annoyance for users. The
text should be wrapped at 80 characters length.
The text in the paragraphs is written in past tense. The last section is a list
of issue URLs, PR URLs and other URLs. The first issue ID (or the first PR ID,
in case there aren't any issue links) is used as the primary ID.
https://github.com/opencloud-eu/opencloud/pull/55555
https://github.com/opencloud-eu/opencloud/issues/1234
Note: Possible keywords are Bugfixes (for bug fixes), Enhancement (for new features),
Change (for breaking changes), Security (for security related topics)
-8
View File
@@ -1,8 +0,0 @@
## Release, Date, Type, Title, Primary ID, Primary URL
{{ range . -}}
{{ $v := .Version -}}
{{ $d := .Date -}}
{{ range $entry := .Entries -}}
{{ $v }},{{ $d }},{{ .Type }},'{{ .Title }}',{{ .PrimaryID }},{{ .PrimaryURL }}
{{ end -}}
{{ end -}}
View File
Whitespace-only changes.
@@ -1,7 +0,0 @@
Enhancement: Add WAYF configuration for reva OCM service
Add WAYF configuration support for the Reva OCM service,
enabling federation discovery functionality for Open Cloud Mesh.
This includes configuration for federations file storage and invite accept dialog URL.
https://github.com/opencloud-eu/opencloud/pull/1714
@@ -1,9 +0,0 @@
Bugfix: Don't re-escape notification email vars for each recipient
When a notification went to several recipients in one call (e.g. a group
invite to a space), each recipient past the first got subjects and bodies
with one extra layer of HTML escaping. escapeStringMap mutated its input
map, and the recipient loop reused the same map across iterations. It now
returns a new map.
https://github.com/opencloud-eu/opencloud/issues/2804
@@ -0,0 +1,21 @@
Bugfix: Retry publishing postprocessing events before giving up
A single transient failure while publishing an event to the event system took
the whole server down. The postprocessing service treated every publish error
as fatal and called log.Fatal, which exits the process and so also stopped all
the other services running in the same binary. A burst of uploads was enough to
run into one nats publish timeout and lose the server with it.
Publishing is now retried using the same exponential backoff that is already
used for failed postprocessing steps. Between the attempts the source event is
marked as in progress and the backoff is capped at half the ack wait, so the
event should not be redelivered to another worker while we are still retrying.
The number of retries is configurable via POSTPROCESSING_PUBLISH_MAX_RETRIES.
Should all attempts fail, the source event is no longer acknowledged. Before,
the event was acknowledged even though its successor was never published, so
the upload was left half processed in the store and did not recover on restart.
https://github.com/opencloud-eu/opencloud/issues/3271
https://github.com/opencloud-eu/opencloud/issues/2232
https://github.com/opencloud-eu/opencloud/issues/2422
+4 -3
View File
@@ -2,7 +2,7 @@
"name": "opencloud-eu/opencloud",
"config": {
"platform": {
"php": "8.3"
"php": "8.4"
},
"vendor-dir": "./vendor-php",
"allow-plugins": {
@@ -11,11 +11,12 @@
},
"require-dev": {
"ext-simplexml": "*",
"bamarni/composer-bin-plugin": "^1.8"
"bamarni/composer-bin-plugin": "^1.9"
},
"extra": {
"bamarni-bin": {
"bin-links": false
"bin-links": false,
"forward-command": false
}
}
}
@@ -45,3 +45,4 @@ directives:
style-src:
- '''self'''
- '''unsafe-inline'''
- 'blob:'
+1 -14
View File
@@ -305,21 +305,8 @@ KEYCLOAK_ADMIN_PASSWORD=
# Leaving it default stores data in docker internal volumes.
#RADICALE_DATA_DIR=/your/local/radicale/data
### Stalwart Settings ###
# Note: the leading colon is required to enable the service.
#STALWART=:stalwart.yml
# Domain of Stalwart
# Defaults to "stalwart.opencloud.test"
STALWART_DOMAIN=
# LDAP configuration to use for Stalwart:
# Can either be either
# - idmldap: for the built-in IDP/IDM, using Master Authentication between Groupware and Stalwart, and LDAP in Stalwart
# - idmoidc: built-in IDP/IDM, using OIDC Userinfo between Groupware and Stalwart
# - ldap: when using KeyCloak and OpenLDAP, with Master Authentication between Groupware and Stalwart, and LDAP in Stalwart
STALWART_AUTH_DIRECTORY=idmldap
## IMPORTANT ##
# This MUST be the last line as it assembles the supplemental compose files to be used.
# ALL supplemental configs must be added here, whether commented or not.
# Each var must either be empty or contain :path/file.yml
COMPOSE_FILE=docker-compose.yml${OPENCLOUD:-}${TIKA:-}${DECOMPOSEDS3:-}${DECOMPOSEDS3_MINIO:-}${DECOMPOSED:-}${COLLABORA:-}${MONITORING:-}${IMPORTER:-}${CLAMAV:-}${INBUCKET:-}${EXTENSIONS:-}${UNZIP:-}${DRAWIO:-}${JSONVIEWER:-}${PROGRESSBARS:-}${EXTERNALSITES:-}${KEYCLOAK:-}${LDAP:-}${KEYCLOAK_AUTOPROVISIONING:-}${LDAP_MANAGER:-}${RADICALE:-}${STALWART:-}
COMPOSE_FILE=docker-compose.yml${OPENCLOUD:-}${TIKA:-}${DECOMPOSEDS3:-}${DECOMPOSEDS3_MINIO:-}${DECOMPOSED:-}${COLLABORA:-}${MONITORING:-}${IMPORTER:-}${CLAMAV:-}${INBUCKET:-}${EXTENSIONS:-}${UNZIP:-}${DRAWIO:-}${JSONVIEWER:-}${PROGRESSBARS:-}${EXTERNALSITES:-}${KEYCLOAK:-}${LDAP:-}${KEYCLOAK_AUTOPROVISIONING:-}${LDAP_MANAGER:-}${RADICALE:-}
@@ -1,26 +0,0 @@
dn: ou=policies,dc=opencloud,dc=eu
objectClass: organizationalUnit
objectClass: top
ou: policies
dn: cn=default,ou=policies,dc=opencloud,dc=eu
cn: default
objectClass: pwdPolicy
objectClass: person
objectClass: top
pwdAllowUserChange: TRUE
pwdAttribute: userPassword
pwdCheckQuality: 0
pwdExpireWarning: 600
pwdFailureCountInterval: 30
pwdGraceAuthNLimit: 5
pwdInHistory: 5
pwdLockout: FALSE
pwdLockoutDuration: 0
pwdMaxAge: 0
pwdMaxFailure: 5
pwdMinAge: 0
pwdMinLength: 1
pwdMustChange: FALSE
pwdSafeModify: FALSE
sn: default
@@ -1,2 +0,0 @@
/*.crt
/*.key
@@ -42,3 +42,4 @@ directives:
style-src:
- '''self'''
- '''unsafe-inline'''
- 'blob:'
@@ -1,21 +0,0 @@
# Stalwart Configuration
The mechanics are currently to mount a different configuration file depending on the environment, as we support two scenarios that are described in [`services/groupware/DEVELOPER.md`](../../../../../services/groupware/DEVELOPER.md):
* &laquo;production&raquo; setup, with OpenLDAP and Keycloak containers
* &laquo;homelab&raquo; setup, with the built-in IDM (LDAP) and IDP that run as part of the `opencloud` container
The Docker Compose setup (in [`stalwart.yml`](../../stalwart.yml)) mounts either [`idmldap.toml`](./idmldap.toml) or [`ldap.toml`](./ldap.toml) depending on how the variable `STALWART_AUTH_DIRECTORY` is set, which is either `idmldap` for the homelab setup, or `ldap` for the production setup.
This is thus all done automatically, but whenever changes are performed to Stalwart configuration files, they must be reflected across those two files, to keep them in sync, as the only entry that should differ is this one:
```ruby
storage.directory = "ldap"
```
or this:
```ruby
storage.directory = "idmldap"
```
@@ -1 +0,0 @@
{"@type":"RocksDb","path":"/var/lib/stalwart/","blobSize":16834,"bufferSize":134217728,"poolWorkers":null}
@@ -1,110 +0,0 @@
authentication.fallback-admin.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
authentication.fallback-admin.user = "mailadmin"
authentication.master.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
authentication.master.user = "master"
directory.idmldap.attributes.class = "objectClass"
directory.idmldap.attributes.description = "displayName"
directory.idmldap.attributes.email = "mail"
directory.idmldap.attributes.groups = "memberOf"
directory.idmldap.attributes.name = "uid"
directory.idmldap.attributes.secret = "userPassword"
directory.idmldap.base-dn = "o=libregraph-idm"
directory.idmldap.bind.auth.method = "default"
directory.idmldap.bind.dn = "uid=reva,ou=sysusers,o=libregraph-idm"
directory.idmldap.bind.secret = "admin"
directory.idmldap.cache.size = 1048576
directory.idmldap.cache.ttl.negative = "10m"
directory.idmldap.cache.ttl.positive = "1h"
directory.idmldap.filter.email = "(&(|(objectClass=person)(objectClass=groupOfNames))(mail=?))"
directory.idmldap.filter.name = "(&(|(objectClass=person)(objectClass=groupOfNames))(uid=?))"
directory.idmldap.timeout = "15s"
directory.idmldap.tls.allow-invalid-certs = true
directory.idmldap.tls.enable = true
directory.idmldap.type = "ldap"
directory.idmldap.url = "ldaps://opencloud:9235"
directory.keycloak.auth.method = "user-token"
directory.keycloak.cache.size = 1048576
directory.keycloak.cache.ttl.negative = "10m"
directory.keycloak.cache.ttl.positive = "1h"
directory.keycloak.endpoint.method = "introspect"
directory.keycloak.endpoint.url = "http://keycloak:8080/realms/openCloud/protocol/openid-connect/userinfo"
directory.keycloak.fields.email = "email"
directory.keycloak.fields.full-name = "name"
directory.keycloak.fields.username = "preferred_username"
directory.keycloak.timeout = "15s"
directory.keycloak.type = "oidc"
directory.ldap.attributes.class = "objectClass"
directory.ldap.attributes.description = "displayName"
directory.ldap.attributes.email = "mail"
directory.ldap.attributes.email-alias = "mailAlias"
directory.ldap.attributes.groups = "memberOf"
directory.ldap.attributes.name = "uid"
directory.ldap.attributes.secret = "userPassword"
directory.ldap.attributes.secret-changed = "pwdChangedTime"
directory.ldap.base-dn = "dc=opencloud,dc=eu"
directory.ldap.bind.auth.dn = "cn=?,ou=users,dc=opencloud,dc=eu"
directory.ldap.bind.auth.enable = true
directory.ldap.bind.auth.search = true
directory.ldap.bind.dn = "cn=admin,dc=opencloud,dc=eu"
directory.ldap.bind.secret = "admin"
directory.ldap.cache.ttl.negative = "10m"
directory.ldap.cache.ttl.positive = "1h"
directory.ldap.filter.email = "(&(|(objectClass=person)(objectClass=groupOfNames))(|(uid=?)(mail=?)(mailAlias=?)(cn=?)))"
directory.ldap.filter.name = "(&(|(objectClass=person)(objectClass=groupOfNames))(|(uid=?)(cn=?)))"
directory.ldap.timeout = "5s"
directory.ldap.tls.allow-invalid-certs = true
directory.ldap.tls.enable = true
directory.ldap.type = "ldap"
directory.ldap.url = "ldap://ldap-server:1389"
http.allowed-endpoint = 200
http.hsts = true
http.permissive-cors = false
http.url = "'https://' + config_get('server.hostname')"
http.use-x-forwarded = true
metrics.prometheus.auth.secret = "secret"
metrics.prometheus.auth.username = "metrics"
metrics.prometheus.enable = true
server.listener.http.bind = "0.0.0.0:8080"
server.listener.http.protocol = "http"
server.listener.https.bind = "0.0.0.0:443"
server.listener.https.protocol = "http"
server.listener.https.tls.implicit = true
server.listener.imap.bind = "0.0.0.0:143"
server.listener.imap.protocol = "imap"
server.listener.imaptls.bind = "0.0.0.0:993"
server.listener.imaptls.protocol = "imap"
server.listener.imaptls.tls.implicit = true
server.listener.pop3.bind = "0.0.0.0:110"
server.listener.pop3.protocol = "pop3"
server.listener.pop3s.bind = "0.0.0.0:995"
server.listener.pop3s.protocol = "pop3"
server.listener.pop3s.tls.implicit = true
server.listener.sieve.bind = "0.0.0.0:4190"
server.listener.sieve.protocol = "managesieve"
server.listener.smtp.bind = "0.0.0.0:25"
server.listener.smtp.protocol = "smtp"
server.listener.submission.bind = "0.0.0.0:587"
server.listener.submission.protocol = "smtp"
server.listener.submissions.bind = "0.0.0.0:465"
server.listener.submissions.protocol = "smtp"
server.listener.submissions.tls.implicit = true
server.max-connections = 8192
server.socket.backlog = 1024
server.socket.nodelay = true
server.socket.reuse-addr = true
server.socket.reuse-port = true
storage.blob = "rocksdb"
storage.data = "rocksdb"
storage.directory = "%{env:STALWART_AUTH_DIRECTORY}%"
storage.fts = "rocksdb"
storage.lookup = "rocksdb"
store.rocksdb.compression = "lz4"
store.rocksdb.path = "/opt/stalwart/data"
store.rocksdb.type = "rocksdb"
tracer.console.ansi = true
tracer.console.buffered = true
tracer.console.enable = true
tracer.console.level = "trace"
tracer.console.lossy = false
tracer.console.multiline = false
tracer.console.type = "stdout"
File diff suppressed because one or more lines are too long.
@@ -1,67 +0,0 @@
authentication.fallback-admin.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
authentication.fallback-admin.user = "mailadmin"
authentication.master.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
authentication.master.user = "master"
directory.oidc.cache.size = 1048576
directory.oidc.cache.ttl.negative = "10m"
directory.oidc.cache.ttl.positive = "1h"
directory.oidc.endpoint.method = "userinfo"
directory.oidc.endpoint.url = "http://172.17.0.1:10000/auth/maethaR9eiXaiph8ahn8ohH6dahPiequ"
directory.oidc.fields.email = "email"
directory.oidc.fields.full-name = "name"
directory.oidc.fields.username = "preferred_username"
directory.oidc.timeout = "15s"
directory.oidc.type = "oidc"
http.allowed-endpoint = 200
http.hsts = true
http.permissive-cors = false
http.url = "'https://' + config_get('server.hostname')"
http.use-x-forwarded = true
metrics.prometheus.auth.secret = "secret"
metrics.prometheus.auth.username = "metrics"
metrics.prometheus.enable = true
server.listener.http.bind = "0.0.0.0:8080"
server.listener.http.protocol = "http"
server.listener.https.bind = "0.0.0.0:443"
server.listener.https.protocol = "http"
server.listener.https.tls.implicit = true
server.listener.imap.bind = "0.0.0.0:143"
server.listener.imap.protocol = "imap"
server.listener.imaptls.bind = "0.0.0.0:993"
server.listener.imaptls.protocol = "imap"
server.listener.imaptls.tls.implicit = true
server.listener.pop3.bind = "0.0.0.0:110"
server.listener.pop3.protocol = "pop3"
server.listener.pop3s.bind = "0.0.0.0:995"
server.listener.pop3s.protocol = "pop3"
server.listener.pop3s.tls.implicit = true
server.listener.sieve.bind = "0.0.0.0:4190"
server.listener.sieve.protocol = "managesieve"
server.listener.smtp.bind = "0.0.0.0:25"
server.listener.smtp.protocol = "smtp"
server.listener.submission.bind = "0.0.0.0:587"
server.listener.submission.protocol = "smtp"
server.listener.submissions.bind = "0.0.0.0:465"
server.listener.submissions.protocol = "smtp"
server.listener.submissions.tls.implicit = true
server.max-connections = 8192
server.socket.backlog = 1024
server.socket.nodelay = true
server.socket.reuse-addr = true
server.socket.reuse-port = true
sharing.allow-directory-query = false
storage.blob = "rocksdb"
storage.data = "rocksdb"
storage.directory = "oidc"
storage.fts = "rocksdb"
storage.lookup = "rocksdb"
store.rocksdb.compression = "lz4"
store.rocksdb.path = "/opt/stalwart/data"
store.rocksdb.type = "rocksdb"
tracer.console.ansi = true
tracer.console.buffered = true
tracer.console.enable = true
tracer.console.level = "trace"
tracer.console.lossy = false
tracer.console.multiline = false
tracer.console.type = "stdout"
@@ -1,110 +0,0 @@
authentication.fallback-admin.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
authentication.fallback-admin.user = "mailadmin"
authentication.master.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
authentication.master.user = "master"
directory.idmldap.attributes.class = "objectClass"
directory.idmldap.attributes.description = "displayName"
directory.idmldap.attributes.email = "mail"
directory.idmldap.attributes.groups = "memberOf"
directory.idmldap.attributes.name = "uid"
directory.idmldap.attributes.secret = "userPassword"
directory.idmldap.base-dn = "o=libregraph-idm"
directory.idmldap.bind.auth.method = "default"
directory.idmldap.bind.dn = "uid=reva,ou=sysusers,o=libregraph-idm"
directory.idmldap.bind.secret = "admin"
directory.idmldap.cache.size = 1048576
directory.idmldap.cache.ttl.negative = "10m"
directory.idmldap.cache.ttl.positive = "1h"
directory.idmldap.filter.email = "(&(|(objectClass=person)(objectClass=groupOfNames))(mail=?))"
directory.idmldap.filter.name = "(&(|(objectClass=person)(objectClass=groupOfNames))(uid=?))"
directory.idmldap.timeout = "15s"
directory.idmldap.tls.allow-invalid-certs = true
directory.idmldap.tls.enable = true
directory.idmldap.type = "ldap"
directory.idmldap.url = "ldaps://opencloud:9235"
directory.keycloak.auth.method = "user-token"
directory.keycloak.cache.size = 1048576
directory.keycloak.cache.ttl.negative = "10m"
directory.keycloak.cache.ttl.positive = "1h"
directory.keycloak.endpoint.method = "introspect"
directory.keycloak.endpoint.url = "http://keycloak:8080/realms/openCloud/protocol/openid-connect/userinfo"
directory.keycloak.fields.email = "email"
directory.keycloak.fields.full-name = "name"
directory.keycloak.fields.username = "preferred_username"
directory.keycloak.timeout = "15s"
directory.keycloak.type = "oidc"
directory.ldap.attributes.class = "objectClass"
directory.ldap.attributes.description = "displayName"
directory.ldap.attributes.email = "mail"
directory.ldap.attributes.email-alias = "mailAlias"
directory.ldap.attributes.groups = "memberOf"
directory.ldap.attributes.name = "uid"
directory.ldap.attributes.secret = "userPassword"
directory.ldap.attributes.secret-changed = "pwdChangedTime"
directory.ldap.base-dn = "dc=opencloud,dc=eu"
directory.ldap.bind.auth.dn = "cn=?,ou=users,dc=opencloud,dc=eu"
directory.ldap.bind.auth.enable = true
directory.ldap.bind.auth.search = true
directory.ldap.bind.dn = "cn=admin,dc=opencloud,dc=eu"
directory.ldap.bind.secret = "admin"
directory.ldap.cache.ttl.negative = "10m"
directory.ldap.cache.ttl.positive = "1h"
directory.ldap.filter.email = "(&(|(objectClass=person)(objectClass=groupOfNames))(|(uid=?)(mail=?)(mailAlias=?)(cn=?)))"
directory.ldap.filter.name = "(&(|(objectClass=person)(objectClass=groupOfNames))(|(uid=?)(cn=?)))"
directory.ldap.timeout = "5s"
directory.ldap.tls.allow-invalid-certs = true
directory.ldap.tls.enable = true
directory.ldap.type = "ldap"
directory.ldap.url = "ldap://ldap-server:1389"
http.allowed-endpoint = 200
http.hsts = true
http.permissive-cors = false
http.url = "'https://' + config_get('server.hostname')"
http.use-x-forwarded = true
metrics.prometheus.auth.secret = "secret"
metrics.prometheus.auth.username = "metrics"
metrics.prometheus.enable = true
server.listener.http.bind = "0.0.0.0:8080"
server.listener.http.protocol = "http"
server.listener.https.bind = "0.0.0.0:443"
server.listener.https.protocol = "http"
server.listener.https.tls.implicit = true
server.listener.imap.bind = "0.0.0.0:143"
server.listener.imap.protocol = "imap"
server.listener.imaptls.bind = "0.0.0.0:993"
server.listener.imaptls.protocol = "imap"
server.listener.imaptls.tls.implicit = true
server.listener.pop3.bind = "0.0.0.0:110"
server.listener.pop3.protocol = "pop3"
server.listener.pop3s.bind = "0.0.0.0:995"
server.listener.pop3s.protocol = "pop3"
server.listener.pop3s.tls.implicit = true
server.listener.sieve.bind = "0.0.0.0:4190"
server.listener.sieve.protocol = "managesieve"
server.listener.smtp.bind = "0.0.0.0:25"
server.listener.smtp.protocol = "smtp"
server.listener.submission.bind = "0.0.0.0:587"
server.listener.submission.protocol = "smtp"
server.listener.submissions.bind = "0.0.0.0:465"
server.listener.submissions.protocol = "smtp"
server.listener.submissions.tls.implicit = true
server.max-connections = 8192
server.socket.backlog = 1024
server.socket.nodelay = true
server.socket.reuse-addr = true
server.socket.reuse-port = true
storage.blob = "rocksdb"
storage.data = "rocksdb"
storage.directory = "ldap"
storage.fts = "rocksdb"
storage.lookup = "rocksdb"
store.rocksdb.compression = "lz4"
store.rocksdb.path = "/opt/stalwart/data"
store.rocksdb.type = "rocksdb"
tracer.console.ansi = true
tracer.console.buffered = true
tracer.console.enable = true
tracer.console.level = "trace"
tracer.console.lossy = false
tracer.console.multiline = false
tracer.console.type = "stdout"
@@ -1,7 +0,0 @@
---
services:
opencloud:
command: [ "-c", "opencloud init || true; dlv --listen=:40000 --headless=true --check-go-version=false --api-version=2 --accept-multiclient exec /usr/bin/opencloud server" ]
ports:
- 40000:40000
@@ -1,7 +1,7 @@
---
services:
traefik:
image: traefik:v3.7.5
image: traefik:v3.3.1
# release notes: https://github.com/traefik/traefik/releases
networks:
opencloud-net:
@@ -36,9 +36,6 @@ services:
- "--accessLog=true"
- "--accessLog.format=json"
- "--accessLog.fields.headers.names.X-Request-Id=keep"
- "--accessLog.fields.headers.names.Trace-Id=keep"
# enable the ping endpoint for monitoring
- "--ping=true"
ports:
- "80:80"
- "443:443"
@@ -57,12 +54,6 @@ services:
logging:
driver: ${LOG_DRIVER:-local}
restart: always
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping=true"]
interval: 10s
retries: 5
start_period: 3s
timeout: 3s
volumes:
certs:
@@ -57,8 +57,6 @@ services:
KC_FEATURES: impersonation
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USER:-admin}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
ports:
- "8080:8080"
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.entrypoints=https"
@@ -24,7 +24,6 @@ services:
OC_LDAP_SERVER_WRITE_ENABLED: "false" # assuming the external ldap is not writable
# OC_RUN_SERVICES specifies to start all services except glauth, idm and accounts. These are replaced by external services
OC_EXCLUDE_RUN_SERVICES: idm
STALWART_AUTH_DIRECTORY: "ldap"
ldap-server:
image: bitnamilegacy/openldap:2.6
@@ -40,9 +39,6 @@ services:
LDAP_TLS_KEY_FILE: /opt/bitnami/openldap/share/openldap.key
LDAP_ROOT: "dc=opencloud,dc=eu"
LDAP_ADMIN_PASSWORD: ${LDAP_ADMIN_PASSWORD:-admin}
LDAP_CONFIGURE_PPOLICY: "yes"
LDAP_PPOLICY_USE_LOCKOUT: "no"
LDAP_PPOLICY_HASH_CLEARTEXT: "no"
ports:
- "127.0.0.1:389:1389"
- "127.0.0.1:636:1636"
@@ -21,7 +21,7 @@ services:
# enable services that are not started automatically
OC_ADD_RUN_SERVICES: ${START_ADDITIONAL_SERVICES}
OC_URL: https://${OC_DOMAIN:-cloud.opencloud.test}
OC_LOG_LEVEL: "${LOG_LEVEL:-info}"
OC_LOG_LEVEL: ${LOG_LEVEL:-info}
OC_LOG_COLOR: "${LOG_PRETTY:-false}"
OC_LOG_PRETTY: "${LOG_PRETTY:-false}"
# do not use SSL between Traefik and OpenCloud
@@ -58,23 +58,10 @@ services:
COMPANION_DOMAIN: ${COMPANION_DOMAIN:-companion.opencloud.test}
# enable to allow using the banned passwords list
OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST: banned-password-list.txt
IDM_REVASVC_PASSWORD: "admin"
AUTH_BASIC_LDAP_BIND_PASSWORD: "admin"
USERS_LDAP_BIND_PASSWORD: "admin"
GROUPS_LDAP_BIND_PASSWORD: "admin"
IDM_LDAPS_ADDR: 0.0.0.0:9235
IDM_LDAPS_CERT: /etc/opencloud/certs/ldaps.crt
IDM_LDAPS_KEY: /etc/opencloud/certs/ldaps.key
OC_LDAP_CACERT: /etc/opencloud/certs/ldaps.crt
GROUPWARE_JMAP_BASE_URL: https://${STALWART_DOMAIN:-stalwart.opencloud.test}
GROUPWARE_JMAP_MASTER_USERNAME: "admin@example.org"
GROUPWARE_JMAP_MASTER_PASSWORD: "admin"
GROUPWARE_TLS_INSECURE: "true"
volumes:
- ./config/opencloud/app-registry.yaml:/etc/opencloud/app-registry.yaml
- ./config/opencloud/csp.yaml:/etc/opencloud/csp.yaml
- ./config/opencloud/banned-password-list.txt:/etc/opencloud/banned-password-list.txt
- ./config/opencloud/certs:/etc/opencloud/certs
# configure the .env file to use own paths instead of docker internal volumes
- ${OC_CONFIG_DIR:-opencloud-config}:/etc/opencloud
- ${OC_DATA_DIR:-opencloud-data}:/var/lib/opencloud
@@ -89,27 +76,6 @@ services:
logging:
driver: ${LOG_DRIVER:-local}
restart: always
healthcheck:
test: ["CMD-SHELL", "test $(curl -sSf http://localhost:9104/healthz) == 'OK'"]
interval: 10s
retries: 5
start_period: 3s
timeout: 3s
opencloud-certs:
image: alpine/openssl:latest
environment:
CERTS: /certs
entrypoint: ["/bin/sh", "-c"]
command:
- |
if [ ! -f "$$CERTS/ldaps.key" ]; then
mkdir -p "$$CERTS"
/usr/bin/openssl req -subj '/CN=opencloud.test' -x509 -newkey rsa:4096 -sha256 -days 3650 -batch -nodes -keyout "$$CERTS/ldaps.key" -out "$$CERTS/ldaps.crt"
fi
chmod 666 "$$CERTS"/ldaps.*
volumes:
- ./config/opencloud/certs:/certs
volumes:
opencloud-config:
@@ -1,27 +0,0 @@
#!/bin/bash
# Snapshots the current configuration of a Stalwart >= 0.16 server.
# Requires having stalwart-cli installed.
set -euo pipefail
exec stalwart-cli -k --url 'https://stalwart.opencloud.test' --user admin --password secret snapshot \
--include-secrets \
Tenant \
Domain \
Directory \
Authentication \
DkimSignature \
AcmeProvider \
Certificate \
DnsServer \
Role \
Authentication \
Account \
NetworkListener \
Tracer \
Sharing \
SystemSettings \
DataRetention \
BlobStore \
InMemoryStore \
SearchStore \
--allow-unresolved PublicKey \
"$@"
@@ -1,64 +0,0 @@
#!/usr/bin/env node
// Processes a Stalwart snapshot JSON file to perform the following:
// - replace masked passwords with their correct value
// - replace the defaultHostname system setting
import * as fs from 'node:fs'
import readline from 'node:readline'
const args = process.argv.slice(2)
const inputs = args.length > 0 ? args.map((file) => fs.createReadStream(file)) : [process.stdin]
async function processLines() {
for (const stream of inputs) {
const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity
})
for await (const line of rl) {
const trimmed = line.trim()
if (!trimmed) continue
const item = JSON.parse(trimmed)
const t = item['@type']
const o = item['object']
if (t === 'create' && o === 'Account') {
const value = item['value'] || {}
for (const [id, account] of Object.entries(value)) {
const name = account['name']
if (name === 'master' || name === 'admin') {
const credentials = account['credentials'] || {}
for (const [cid, creds] of Object.entries(credentials)) {
if (creds['@type'] === 'Password') {
creds['secret'] = 'supersecret1234'
}
}
}
}
}
if (t === 'create' && o === 'Directory') {
const value = item['value'] || {}
for (const [id, dir] of Object.entries(value)) {
if (dir['@type'] === 'Ldap') {
if (dir['bindSecret']) {
dir['bindSecret']['secret'] = 'admin'
}
}
}
}
if (t === 'update' && o === 'SystemSettings') {
if (item['value']) {
item['value']['defaultHostname'] = 'stalwart.opencloud.test'
}
}
console.log(JSON.stringify(item))
}
}
}
processLines()
@@ -1,116 +0,0 @@
---
services:
traefik:
networks:
opencloud-net:
aliases:
- ${STALWART_DOMAIN:-stalwart.opencloud.test}
stalwart:
image: ghcr.io/stalwartlabs/stalwart:v0.16.12-alpine
hostname: ${STALWART_DOMAIN:-stalwart.opencloud.test}
networks:
- opencloud-net
ports:
- "127.0.0.1:8443:443"
- "127.0.0.1:8080:8080"
- "127.0.0.1:143:143"
- "127.0.0.1:993:993"
- "127.0.0.1:1465:465"
volumes:
- ./config/stalwart/config.json:/etc/stalwart/config.json
- stalwart-data:/var/lib/stalwart
environment:
STALWART_RECOVERY_ADMIN: "admin:secret"
STALWART_PUBLIC_URL: "https://${STALWART_DOMAIN:-stalwart.opencloud.test}"
labels:
- "traefik.enable=true"
- "traefik.http.routers.stalwart.entrypoints=https"
- "traefik.http.routers.stalwart.rule=Host(`${STALWART_DOMAIN:-stalwart.opencloud.test}`)"
- "traefik.http.routers.stalwart.tls.certresolver=http"
- "traefik.http.routers.stalwart.service=stalwart"
- "traefik.http.services.stalwart.loadbalancer.server.port=8080"
logging:
driver: ${LOG_DRIVER:-local}
restart: unless-stopped
entrypoint: ["/bin/sh", "-c"]
command:
- |
if [ ! -f /var/lib/stalwart/.initialized ]; then
echo "Not initialized, starting in recovery mode"
STALWART_RECOVERY_MODE=1 /usr/local/bin/stalwart --config /etc/stalwart/config.json &
pid=$$!
while [ ! -f /var/lib/stalwart/.initialized ]; do sleep 1; done
echo "Initialized, stopping recovery mode"
kill -TERM $$pid
wait $$pid
fi
echo "Starting Stalwart in production mode"
exec /usr/local/bin/stalwart --config /etc/stalwart/config.json
healthcheck:
test: ["CMD-SHELL", "curl -LsSf http://localhost:8080/healthz/live|grep -q '\"status\":200'"]
interval: 10m
retries: 3
start_period: 5s
timeout: 3s
stalwart-import:
build:
context: ./config/stalwart/
args:
VERSION: "1.0.8"
dockerfile_inline: |
FROM alpine:latest AS downloader
ARG VERSION
RUN apk add --no-cache curl tar xz wget
RUN arch=$(apk --print-arch) && \
mkdir /cli && \
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/stalwartlabs/cli/releases/download/v$${VERSION}/stalwart-cli-$${arch}-unknown-linux-musl.tar.xz \
| tar xJf - --strip-components=1 -C /cli
RUN mv /cli/stalwart-cli /stalwart-cli && rm -rf /cli/
CMD ["/stalwart-cli"]
networks:
- opencloud-net
volumes:
- ./config/stalwart/${STALWART_AUTH_DIRECTORY:-idmldap}.json:/snapshot.json:ro
- stalwart-data:/var/lib/stalwart
environment:
STALWART_URL: "http://stalwart:8080"
STALWART_USER: "admin"
STALWART_PASSWORD: "secret"
entrypoint: ["/bin/sh", "-c"]
command:
- |
if [ -f /var/lib/stalwart/.initialized ]; then
echo "Already initialized, skipping stalwart-cli."
exit 0
fi
echo "Waiting for the Stalwart recovery API to become live"
until wget -qO- $$STALWART_URL/healthz/live >/dev/null 2>&1; do sleep 1; done
echo "Applying configuration"
/stalwart-cli --debug apply --file /snapshot.json
touch /var/lib/stalwart/.initialized
echo "Successfully initialized"
depends_on:
- stalwart
stalwart-reset:
image: alpine:latest
profiles:
- maintenance
volumes:
- stalwart-data:/var/lib/stalwart
entrypoint: ["/bin/sh", "-c"]
command:
- |
if [ -f /var/lib/stalwart/.initialized ]; then
rm -f /var/lib/stalwart/.initialized
echo "Success: Lockfile removed. Next time 'stalwart' boots, it will run in recovery mode."
fi
volumes:
stalwart-data:
@@ -1,343 +0,0 @@
---
title: "Authentication with Stalwart"
---
* Status: draft
## Context
In a groupware environment, not every user will always use the OpenCloud UI to read their emails, some will resort to other [MUAs (Mail User Agents)](https://en.wikipedia.org/wiki/Email_client) that support a subset of features, use older protocols (IMAP, POP, SMTP, CalDAV, CardDAV) and lesser authentication methods (basic authentication). Those email clients will talk to Stalwart directly, as opposed to the OpenCloud UI which will make use of APIs of the OpenCloud Groupware service, since those protocols are provided by Stalwart and implementing them in OpenCloud would offer very little benefits, but definitely a lot of (almost completely) unnecessary effort.
Those protocols and operations that bypass the OpenCloud UI also need to be authenticated, this in and by Stalwart, and we need to find the best fitting approach that fulfills most or all of the following constraints:
### Single Provisioning
We want to avoid multiple provisioning of users, groups, passwords and other resources as much as possible.
While it is possible to have e.g. OpenCloud's user management also perform [Management API](https://stalw.art/docs/category/management-api/) calls, one still inevitably ends up in situations where users, user passwords, or other resources are not in sync, which becomes complex to debug and fix, and should thus be avoided if possible.
To do so, we should strive to have a single source of truth regarding users, their passwords, and similar resources and attributes such as groups, roles, application passwords, etc...
### Attack Detection
Coordinated attacks such as [denial of service](https://en.wikipedia.org/wiki/Denial-of-service_attack) attempts don't necessarily focus on a single protocol but are commonly multi-pronged, e.g. by brute forcing the [OIDC API](https://www.keycloak.org/docs/latest/authorization_services/index.html#token-endpoint), the OpenCloud Groupware API, IMAP and SMTP, \*DAV protocols, etc...
In order to detect those as well as to quickly react by blacklisting clients that are identified to attempt such attacks, it is useful to have a single authentication service for all the components of the system, all protocols, all clients (e.g. [PowerDNS Weakforced](https://github.com/PowerDNS/weakforced), [Nauthilus](https://nauthilus.org/), ...)
Furthermore, such services typically make use of [DNSBL/RBL services](https://en.wikipedia.org/wiki/Domain_Name_System_blocklist) that allow IP addresses of botnets to be blocked across many services of many providers as a shared defense mechanism.
As a bonus, a centralized authentication component can also provide metrics and observability capabilities across all those protocols.
### Custom Authentication Implementations
Some customers might want custom authentication implementations to integrate with their environment, in which case we would want those to be done once and in the technology stack we're all most familiar with (thus as a service in Go in the OpenCloud framework, and not e.g. a Lua script in Nauthilus, or a Rust plugin in Stalwart, etc...)
## Decision Drivers
TODO
*
## Considered Options
First off, here is a brief explanation of each of the scenarios that we potentially or absolutely need to support, which we will explore for each implementation option:
* MUAs with basic authentication
* these are external mail clients (Thunderbird, Apple Mail, ...) with which users authenticate using legacy protocols (IMAP, POP3, SMTP) and their primary username and password in clear text (encrypted through the mandatory use of TLS)
* MUAs with application password authentication
* these are external mail clients (Thunderbird, Apple Mail, ...) with which users authenticate using legacy protocols (IMAP, POP3, SMTP) and one of the application passwords that they created in the OpenCloud UI, which is a useful security mechanism as it reduces the attack surface when one such password is leaked or discovered
* MUAs with SASL bearer token authentication
* these are more modern external mail clients (Thunderbird) with which users authenticate using legacy protocols (IMAP, POP3, SMTP) but more secure OIDC token based authentication (SASL OAUTHBEARER or SASL XOAUTH2), which closely resembles the OIDC authentication used by the OpenCloud UI towards the OpenCloud backends
* JMAP clients with basic authentication
* modern mail clients (Thunderbird) that speak the JMAP protocol over HTTP and authenticate using their primary username and password in clear text (encrypted through the use of HTTPS)
* JMAP clients with bearer token authentication
* modern mail clients (Thunderbird) that speak the JMAP protocol over HTTP and authenticate using an OIDC token (JWT) obtained from an IDP (typically KeyCloak)
* OpenCloud Groupware with master authentication
* the OpenCloud UI client uses APIs from the OpenCloud Groupware backend (and authenticates using OIDC)
* the OpenCloud Groupware backend, in turn, performs JMAP operations with Stalwart, and authenticates using Stalwart's shared secret master authentication protocol
* OpenCloud Groupware with generated token authentication
* the OpenCloud UI client uses APIs from the OpenCloud Groupware backend (and authenticates using OIDC)
* the OpenCloud Groupware backend, in turn, performs JMAP operations with Stalwart, and authenticates against Stalwart using bearer authentication with JWTs that it generates itself
* in the future, that JWT might also be the JWT that the OpenCloud UI used to authenticate against the OpenCloud Groupware in the first place
### Stalwart with the LDAP Directory
```mermaid
flowchart LR
c(client)
s(Stalwart)
l(LDAP)
c -- IMAP/SMTP --> s
c -- JMAP --> s
s -- LDAP --> l
```
Clients authenticate directly against Stalwart, that is configured to use an LDAP authentication Directory.
An LDAP server (e.g. OpenLDAP) is needed as part of the infrastructure.
OpenCloud also has to make use of the same LDAP server.
* ✅ MUAs with basic authentication
* MUAs authenticate directly against Stalwart
* Stalwart's LDAP Directory plugin supports plain text authentication by looking up the userPassword attribute in the LDAP server
* ❌ MUAs with application password authentication
* MUAs authenticate directly against Stalwart
* Stalwart's LDAP Directory plugin does not support application password as it is hardwired to look up the password in the userPassword attribute in the LDAP server
* even if it did support looking up alternative passwords in LDAP, this would hardly be practical as the application passwords are currently created and stored in OpenCloud, which would need to be modified to store them in LDAP in the first place
* ❌ MUAs with SASL bearer token authentication
* MUAs authenticate directly against Stalwart
* Stalwart's LDAP Directory plugin does not support verifying OIDC tokens
* ✅ JMAP clients with basic authentication
* JMAP clients authenticate directly against Stalwart
* Stalwart's LDAP Directory plugin supports plain text authentication by looking up the userPassword attribute in the LDAP server
* ❌ JMAP clients with bearer token authentication
* JMAP clients authenticate directly against Stalwart
* Stalwart's LDAP Directory plugin does not support verifying OIDC tokens
* ✅ OpenCloud Groupware with master authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* Stalwart detects and supports clear text password master authentication regardless of the Directory that is being used, and verifies it against the shared secret password that is configured in the server
* ❌ OpenCloud Groupware with generated token authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* Stalwart's LDAP Directory plugin does not support verifying OIDC tokens
### Stalwart with the OIDC Directory
```mermaid
flowchart LR
c(client)
s(Stalwart)
o(IDP)
c -- IMAP/SMTP --> s
c -- JMAP --> s
s -- OIDC HTTP --> o
```
Clients authenticate directly against Stalwart, that is configured to use an OIDC authentication Directory.
An OIDC IDP (server) is needed as part of the infrastructure, e.g. KeyCloak.
Optionally, an LDAP server (e.g. OpenLDAP) might be used as well, and KeyCloak would look up users and their credentials in LDAP.
OpenCloud also has to make use of the same LDAP server, or would need to be modified to be capable of only making use of an OIDC IDP (which would include limitations that are yet to be resolved, e.g. the option of using KeyCloak Admin APIs to retreieve groups, group members, ...)
* ❌ MUAs with basic authentication
* MUAs authenticate directly against Stalwart
* Stalwart's OIDC Directory plugin does not support plain text authentication
* ❌ MUAs with application password authentication
* MUAs authenticate directly against Stalwart
* Stalwart's OIDC Directory plugin does not support application passwords
* ❓ MUAs with SASL bearer token authentication
* MUAs authenticate directly against Stalwart
* Stalwart's OIDC Directory plugin does not currently support external IDPs, but is expected to in future versions
* as of Stalwart 0.12, this would only work if Stalwart itself is used as the IDP when acquiring a token
* ❌ JMAP clients with basic authentication
* JMAP clients authenticate directly against Stalwart
* Stalwart's OIDC Directory plugin does not support plain text authentication
* ❓ JMAP clients with bearer token authentication
* JMAP clients authenticate directly against Stalwart
* Stalwart's OIDC Directory plugin does not currently support external IDPs, but is expected to in future versions
* as of Stalwart 0.12, this would only work if Stalwart itself is used as the IDP when acquiring a token
* ✅ OpenCloud Groupware with master authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* Stalwart detects and supports clear text password master authentication regardless of the Directory that is being used, and verifies it against the shared secret password that is configured in the server
* ❓ OpenCloud Groupware with generated token authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* Stalwart's OIDC Directory plugin does not currently support external IDPs, but is expected to in future versions
* as of Stalwart 0.12, this would only work if Stalwart itself is used as the IDP when acquiring a token, which is not the case with this approach as the tokens are generated by the Groupware backend itself
### Stalwart with the Internal Directory
```mermaid
flowchart LR
c(client)
s(Stalwart)
c -- IMAP/SMTP --> s
c -- JMAP --> s
```
Clients authenticate directly against Stalwart, that is configured to use an Internal authentication Directory.
Neither an OIDC IDP nor an LDAP server are needed as part of the infrastructure, as principal resources (users, groups) and their credentials exist in Stalwart's storage.
OpenCloud would not be capable of accessing those resources, which means that provisioning of groups, users, user passwords must be duplicated and kept in sync between Stalwart and OpenCloud.
* ✅ MUAs with basic authentication
* MUAs authenticate directly against Stalwart
* Stalwart's Internal Directory plugin supports plain text authentication
* ✅ MUAs with application password authentication
* MUAs authenticate directly against Stalwart
* Stalwart's Internal Directory plugin supports application passwords
* users are able to create those themselves using the self-service web UI of Stalwart
* they are not shared with the OpenCloud application passwords though and would need to be provisioned into Stalwart when created in OpenCloud to provide a single UI
* ❌ MUAs with SASL bearer token authentication
* MUAs authenticate directly against Stalwart
* Stalwart's Internal Directory plugin does not support OIDC token authentication
* ✅ JMAP clients with basic authentication
* JMAP clients authenticate directly against Stalwart
* Stalwart's Internal Directory plugin supports plain text authentication
* ❌ JMAP clients with bearer token authentication
* JMAP clients authenticate directly against Stalwart
* Stalwart's Internal Directory plugin does not support OIDC token authentication
* ✅ OpenCloud Groupware with master authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* Stalwart detects and supports clear text password master authentication regardless of the Directory that is being used, and verifies it against the shared secret password that is configured in the server
* ❌ OpenCloud Groupware with generated token authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* Stalwart's Internal Directory plugin does not support OIDC token authentication
### Stalwart with the OpenCloud Authentication API
```mermaid
flowchart LR
c(client)
s(Stalwart)
o(OpenCloud)
l(LDAP)
c -- IMAP/SMTP --> s
c -- JMAP --> s
s -- REST --> o
o -- LDAP --> l
```
Clients authenticate directly against Stalwart, that is configured to use an "External" authentication Directory, that is yet to be developed. (warning)
Its protocol is currently not defined, but not particularly relevant at this time, as long as it supports accepting basic and bearer authentication in order to authenticate both username and password credentials as well as OIDC tokens.
That External Directory implementation forwards the basic or bearer credentials to an endpoint in the OpenCloud backend, that the responds with whether the authentication is successful or not, as well as with additional information that is needed for Stalwart (email address, display name, groups, roles, ...)
* ✅ MUAs with basic authentication
* MUAs authenticate directly against Stalwart
* Stalwart's External Directory supports plain text authentication by relaying the authentication operation to the OpenCloud backend, which can then authenticate users by username and password using an LDAP server
* note that this option requires having an LDAP server in the environment, including having it accessible by OpenCloud
* if that is not the case, then a viable option is also to support OIDC tokens and application passwords
* to clarify: this scenario is only about supporting authentication using the "primary" username and password
* ✅ MUAs with application password authentication
* MUAs authenticate directly against Stalwart
* Stalwart's External Directory supports application password authentication by relaying the authentication operation to the OpenCloud backend, which can then authenticate against its list of application passwords
* this is the ideal scenario for application passwords, since they are already supported by OpenCloud, and can be created and managed using the OpenCloud UI
* relaying the authentication operation to OpenCloud also prevents the need for duplicate provisioning of application passwords
* ✅ MUAs with SASL bearer token authentication
* MUAs authenticate directly against Stalwart
* Stalwart's External Directory supports OIDC token authentication by relaying the authentication operation to the OpenCloud backend, which can then either perform local token inspection and authentication by verifying the token's signature, or use the OIDC IDP's token introspection endpoint
* ✅ JMAP clients with basic authentication
* JMAP clients authenticate directly against Stalwart
* Stalwart's External Directory supports plain text authentication by relaying the authentication operation to the OpenCloud backend, which can then authenticate users by username and password using an LDAP server
* the same limitations/requirements as for the "MUAs with basic authentication" scenario apply here as well
* ✅ JMAP clients with bearer token authentication
* MUAs authenticate directly against Stalwart
* Stalwart's External Directory supports OIDC token authentication by relaying the authentication operation to the OpenCloud backend, which can then either perform local token inspection and authentication by verifying the token's signature, or use the OIDC IDP's token introspection endpoint
* ✅ OpenCloud Groupware with master authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* Stalwart detects and supports clear text password master authentication regardless of the Directory that is being used, and verifies it against the shared secret password that is configured in the server
* ✅ OpenCloud Groupware with generated token authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* in the worst case, the External Directory plugin in Stalwart would also perform a forwarding of the authentication operation to OpenCloud, which would obviously be able to verify a token it has created
* an optimization might be possible here, if the External Directory implementation permits for the configuration of specific issuers which should then be verifying against a JWK set directly, whereas the fallback behaviour would be to query the OpenCloud Authentication API
### Stalwart with Nauthilus and LDAP
```mermaid
flowchart LR
c(client)
s(Stalwart)
n(Nauthilus)
l(LDAP)
c -- IMAP/SMTP --> s
c -- JMAP --> s
s -- REST --> n
n -- LDAP --> l
```
In this scenario, we introduce the [Nauthilus authentication service](https://nauthilus.org/), which has its own API but also a KeyCloak integration plugin.
It supports various backends and can also be scripted for more complex combinations.
⚠️ It would require the implementation of a Stalwart Nauthilus Directory, **that is yet to be developed**.
We do not make use of any OpenCloud Authentication API but, instead, attempt to have everything go through Nauthilus instead, backed by an LDAP server that then contains the users, groups, and user passwords.
The upside of using Nauthilus is that it does brute force attack detection and can provide metrics across multiple protocols and clients in a centralized fashion.
* ✅ MUAs with basic authentication
* MUAs authenticate directly against Stalwart
* Stalwart's Nauthilus Directory supports plain text authentication by relaying the authentication operation to Nauthilus, e.g. using its JSON API
* Nauthilus provides a response that contains user attributes from LDAP (display name, email addresses, ...)
* ❓ MUAs with application password authentication
* Nauthilus has no support for application passwords in itself
* a Lua plugin could potentially be used in Nauthilus to detect whether the clear text password matches a regular expression for application passwords and, if that is the case, first attempt to verify it through an API call (that does not exist yet) to the OpenCloud backend, but that would definitely be more complex and less elegant than having a single API
* ❓ MUAs with SASL bearer token authentication
* it is currently unclear whether Nauthilus supports OIDC token authentication
* ✅ JMAP clients with basic authentication
* Stalwart's Nauthilus Directory supports plain text authentication by relaying the authentication operation to Nauthilus, e.g. using its JSON API
* Nauthilus provides a response that contains user attributes from LDAP (display name, email addresses, ...)
* ❓ JMAP clients with bearer token authentication
* it is currently unclear whether Nauthilus supports OIDC token authentication
* ✅ OpenCloud Groupware with master authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* Stalwart detects and supports clear text password master authentication regardless of the Directory that is being used, and verifies it against the shared secret password that is configured in the server
* ❓ OpenCloud Groupware with generated token authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* it is currently unclear whether Nauthilus supports OIDC token authentication
* an optimization might be possible here, if the Nauthilus Directory implementation permits for the configuration of specific issuers which should then be verifying against a JWK set directly, whereas the fallback behaviour would be to query the Nauthilus API, but that does sound like a stretch to fit into the concept
### Stalwart with Nauthilus and an OpenCloud Authentication API
```mermaid
flowchart LR
c(client)
j(client)
s(Stalwart)
n(Nauthilus)
o(OpenCloud)
l(LDAP)
k(Keycloak)
c -- IMAP/SMTP --> s
j -- JMAP --> s
s -- REST --> n
subgraph internal auth
n -- REST --> o
o -- LDAP --> l
o -- OIDC --> k
end
```
This option also makes use of the [Nauthilus authentication service](https://nauthilus.org/), but instead of it using LDAP to resolve users, we would either make use of its Lua scripting abilities to implement a backend that performs HTTP calls to an OpenCloud Authentication API, or implement an additional Nauthilus backend that uses the Nauthilus API to delegate to another instance, which would then be the OpenCloud Authentication API with support for the Nauthilus API.
⚠️ As with the previous option, it would require the implementation of a Stalwart Nauthilus Directory, **that is yet to be developed**.
Interestingly, if the OpenCloud Authentication API follows the Nauthilus API, this scenario can easily be degraded by dropping Nauthilus and, instead, having all services talk to the OpenCloud Authentication API directly.
* ✅ MUAs with basic authentication
* MUAs authenticate directly against Stalwart
* Stalwart's Nauthilus Directory supports plain text authentication by relaying the authentication operation to Nauthilus, e.g. using its JSON API
* Nauthilus provides a response that contains user attributes from LDAP (display name, email addresses, ...)
* ✅ MUAs with application password authentication
* Nauthilus would forward the authentication request to the OpenCloud Authentication API, which would support application passwords
* ❓ MUAs with SASL bearer token authentication
* it is currently unclear whether Nauthilus supports OIDC token authentication and whether it would be able to forward such requests to the OpenCloud Authentication API
* ✅ JMAP clients with basic authentication
* Stalwart's Nauthilus Directory supports plain text authentication by relaying the authentication operation to Nauthilus, e.g. using its JSON API
* Nauthilus then forwards that request to the OpenCloud Authentication API
* the OpenCloud Authentication API, and then Nauthilus, provides a response that contains user attributes from LDAP (display name, email addresses, ...) or claims from the JWT
* ❓ JMAP clients with bearer token authentication
* it is currently unclear whether Nauthilus supports OIDC token authentication and whether it would be able to forward such requests to the OpenCloud Authentication API
* ✅ OpenCloud Groupware with master authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* Stalwart detects and supports clear text password master authentication regardless of the Directory that is being used, and verifies it against the shared secret password that is configured in the server
* ❓ OpenCloud Groupware with generated token authentication
* the OpenCloud Groupware backend authenticates directly against Stalwart
* it is currently unclear whether Nauthilus supports OIDC token authentication and whether it would be able to forward such requests to the OpenCloud Authentication API
> [!IMPORTANT]
> We need to clarify whether the Nauthilus API allows for a JWT to be submitted for the authentication request, and not only username and password not to secure the request in itself, but to forward an OIDC token based authentication attempt as part of the payload.
### Comparing Options
| | MUA basic | MUA app password | MUA sasl | JMAP clients with basic auth | JMAP clients with JWT auth | Groupware Middleware with master auth | Groupware Middleware with JWT auth |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Stalwart 0.12 with LDAP Directory | ✅ MUA → Stalwart | ❌ not supported with LDAP | ❌ not supported with LDAP | ✅ | ❌ | ✅ | ❌ |
| Stalwart 0.12 with OIDC Directory | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
| Stalwart 0.12 with Internal Directory | ✅ MUA → Stalwart, must be provisioned in Stalwart | ✅ MUA → Stalwart, must be provisioned in Stalwart | ❌ | ❌ | ❌ unless using Stalwart as IDP | ✅ | ❌ |
| Stalwart + OpenCloud Authentication API | ✅ MUA → Stalwart → OpenCloud | ✅ MUA → Stalwart → OpenCloud | ✅ MUA → Stalwart → OpenCloud | ✅ MUA → Stalwart → OpenCloud | ✅ MUA → Stalwart → OpenCloud | ✅ | ✅ |
| Stalwart + Nauthilus + LDAP | ✅ MUA → IMAP proxy → Nauthilus → LDAP | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ |
| Stalwart + Nauthilus + OpenCloud Authentication API | ✅ MUA → IMAP proxy → Nauthilus → OpenCloud | ✅ MUA → IMAP proxy → Nauthilus → OpenCloud | ✅ MUA → IMAP proxy → Nauthilus → OpenCloud | ✅ MUA → IMAP proxy → Nauthilus → OpenCloud | ✅ MUA → IMAP proxy → Nauthilus → OpenCloud | ✅ | ✅ |
| Stalwart + Nauthilus-like OpenCloud Authentication API | ✅ MUA → Stalwart → OpenCloud | ✅ MUA → Stalwart → OpenCloud | ✅ MUA → Stalwart → OpenCloud | ✅ MUA → Stalwart → OpenCloud | ✅ MUA → Stalwart → OpenCloud | ✅ | ✅ |
@@ -1,64 +0,0 @@
---
status: proposed
date: 2025-06-24
author: Pascal Bleser <p.bleser@opencloud.eu>
decision-makers:
consulted:
informed:
title: "Implementing Groupware as a separate Microservice vs integrated in the OpenCloud Stack"
template: https://raw.githubusercontent.com/adr/madr/refs/tags/4.0.0/template/adr-template.md
---
* Status: draft
## Context
Should the Groupware backend be an independent microservice or be part of the OpenCloud single binary framework?
The OpenCloud backend is built on a framework that
* implements token based authentication between services
* allows for a "single binary" deployment mode that runs all services within that one binary
* integrates services such as a NATS event bus
This decision is about whether the Groupware backend service should be implemented within that framework or, instead, be implemented as a standalone backend service.
## Decision Drivers
* single binary deployment strategy is potentially important (TODO how important is it really? stakeholders:?)
## Considered Options
* have the Groupware Middleware as an independent microservice
* have the Groupware Middleware implemented within the existing OpenCloud framework
## Decision Outcome
TODO
### Consequences
TODO
### Confirmation
TODO
## Pros and Cons of the Options
### Independent Microservice
* (potentially) good: be free from technical decisions made for the existing OpenCloud stack, to avoid carrying potential technical baggage
* (potentially) good: make use of a framework that is more fitting for the tasks the Groupware backend needs to accomplish
* bad: re-implement framework components that already exist, with the need to maintain those in two separate codebases, or the added complexity of a shared library repository
* bad: not have the ability to include the Groupware backend in the single binary deployment
* neutral: a separate code repository and delivery for the Groupware backend, which might or might not be of advantage
* neutral: may be implemented on a completely different technology stack, including the programming language
### Part of the framework
* good: fit into the opinionated choices that were made for the OpenCloud framework so far
* good: many aspects are already implemented in the current framework and can be made use of, potentially enhanced for the needs of the Groupware backend
* good: the ability to include the Groupware backend in the single binary deployment
* neutral: be in the same code repository and part of the same delivery as other services in OpenCloud
* neutral: must be implemented in Go on top of the same technology stack
-294
View File
@@ -1,294 +0,0 @@
---
status: proposed
date: 2025-06-24
author: Pascal Bleser <p.bleser@opencloud.eu>
decision-makers:
consulted:
informed:
title: "Resource Linking"
template: https://raw.githubusercontent.com/adr/madr/refs/tags/4.0.0/template/adr-template.md
---
* Status: draft
## Context
Which semantic and technical approach to take in order to provide strong integration of the various products and capabilities of OpenCloud, OpenTalk, and potentially other products as well?
## Decision Drivers
* a strong integration that allows users to access resources and relationships without having to switch views, which translates into a "mental switch" as well
* an innovative approach that differs from the traditional way groupware applications have been designed in the past
* TODO more decision drivers from PM
* a model that is open and generic enough to integrate many different types of resources and relationships
* a model that allows for independent and incremental upgrades to the resources and relationships that can be contributed by each service
## Considered Options
* resource linking
* application launchers
* TODO? can we come up with more ideas?
## Decision Outcome
TODO
### Consequences
TODO
### Confirmation
TODO
## Pros and Cons of the Options
### Resource Linking
This concept primarily resides on the idea of having resources, which have attributes, and relations between them, pretty much as [RDF (Resource Description Framework)](https://www.w3.org/RDF/) does, where the Groupware backend provides services to explore relations of a given resource.
* good: decoupling of UI, backends as well as other participants, as backends can gradually evolve the relationships and resources they understand and can contribute to over time, as well as for the UI that may just silently ignore resources it does not support yet or does not want to present to the user
* good: potential for an asynchronous architecture that would enable the UI to present some resources early without having to wait for those that require more processing time or are provided by services that happen to be under heavier load
* good: it should provide ammunition for a modern and original UI that is centered around resources and relationships rather than the usual visual paradigms
* bad: it might be a challenge to implement this approach in a performant way with rapid response times, as it could cause additional complexity and storage services (e.g. to denormalize reverse indexes, cache expensive resource graphs, etc...)
#### URNs
Each resource has a unique identifier, for which [URNs (Uniform Resource Names)](https://www.rfc-editor.org/rfc/rfc1737) seem the best representation.
URNs are composed of
* a namespace identifier
* a namespace-specific string
As a convention, we will use the following:
<table>
<thead>
<tr>
<th><code>urn:</code></th>
<th>ns</th>
<th colspan="2">namespace specific string</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>urn:</code></td>
<td><code>oc:</code></td>
<td><code>&lt;type&gt;:</code></td>
<td><code>&lt;unique identifier&gt;:</code></td>
</tr>
</tbody>
</table>
##### Examples
<table>
<thead>
<tr>
<th><code>urn:</code></th>
<th>namespace</th>
<th>type</th>
<th>unique id</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>urn:</code></td>
<td><code>oc:</code></td>
<td><code>user:</code></td>
<td><code>camina.drummer</code></td>
</tr>
<tr>
<td><code>urn:</code></td>
<td><code>oc:</code></td>
<td><code>contact:</code></td>
<td><code>klaes.ashford</code></td>
</tr>
<tr>
<td><code>urn:</code></td>
<td><code>oc:</code></td>
<td><code>event:</code></td>
<td><code>dd4ea520-e414-41e1-b545-b1c7d4ce57e7</code></td>
</tr>
<tr>
<td><code>urn:</code></td>
<td><code>oc:</code></td>
<td><code>mail:</code></td>
<td><code>&lt;1e8074e8-cd56-4358-9f9e-f17cb701b950@opa.org&gt;</code></td>
</tr>
</tbody>
</table>
#### Exploration API
Whenever the user puts a resource into focus in the OpenCloud Groupware UI (i.e. by selecting/clicking that resource, e.g. the sender of an email), it may send a request to the Groupware service API to inquire about related resources.
What those related resources are still stands to be determined, but examples could be along the lines of
* unread emails from the same sender
* emails exchanged with that sender in the last 7 days
* files recently shared with that user
* spaces or groups in common with that user
* OpenTalk meetings planned within the next 3 days
In order to decouple the Groupware service from which resources and relations are supported,
* whenever such an exploration request is received, the Groupware service forwards it to all known services, in a "fan-out" model
* each service can understand the focused resource, or not, but if it does it may return related resources that it is capable of providing using its data model (e.g. OpenTalk providing related meeting resources, OpenCloud Groupware providing related calendar events, contacts, mails, etc...)
* ideally, that happens in an asynchronous fashion, using e.g. [SSE (Server Side Events)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) to push results to the OpenCloud UI to avoid having to wait for the slowest contributor, although that pushes the "reduce" part of this ["map-reduce" operation](https://en.wikipedia.org/wiki/MapReduce) to the client
```mermaid
graph LR
c(client)
subgraph backend
a(opencloud api)
g(groupware)
ot(opentalk)
u(users)
s(stalwart)
k(keycloak)
end
subgraph storage
ots@{ shape: cyl, label: "opentalk\nstorage"}
ss@{ shape: cyl, label: "stalwart\nstorage"}
l@{ shape: cyl, label: "ldap"}
end
c-->|/related/urn:oc:user:camina.drummer|a
a-->|/related/urn:oc:user:camina.drummer|g
g-->s
s-->ss
a-->|/related/urn:oc:user:camina.drummer|ot
ot-->ots
a-->|/related/urn:oc:user:camina.drummer|u
u-->k
k-->l
ot-.->|urn:oc:meeting:232403bc-b98f-4643-a917-80bdcfc7aaba|a
g-.->|urn:oc:event:e5193ad3-8f1c-4162-8593-69fe659bcc08|a
```
This allows a decoupling of all the participants, enabling each service to add, remove or alter relationships that it is able to contribute for a given resource type.
Obviously, the UI needs to be able to understand resource types to know how to represent them, but if it silently ignores resource types that it does not know of, backends can evolve independently from the UI.
#### JSON-LD
[JSON-LD (JSON for Linking Data)](https://json-ld.org/) seems like a potent representation format for those relationships in a REST environment.
It could look something like this:
```json
{
"@context": {
"@user": "https://schema.opencloud.eu/user.jsonld",
"link": "https://schema.opencloud.eu/linked.jsonld"
},
"@type": "urn:oc:type:user",
"@id": "urn:oc:user:cdrummer",
"name": "Camina Drummer",
"email": "camina@opa.org",
"roles": ["admin", "pirate"],
"link:rooms": [
{
"@context": {
"@room": "https://meta.opencloud.eu/room.jsonld",
"link": "https://schema.opencloud.eu/linked.jsonld"
},
"@id": "urn:oc:room:a3f19df6-6c7d-45fa-b16c-6e168e2a2a43",
"name": "OPA Leadership Standup 2355-02-27",
"start": "2355-02-27T10:58:15.918Z",
"end": "2355-02-27T13:52:59.010Z",
"started_by": {
"@context": "https://meta.opencloud.eu/user.jsonld",
"@type": "urn:oc:type:user",
"@id": "urn:oc:user:adawes",
"name": "Anderson Dawes",
"email": "anderson@opa.org"
},
"link:events": [
{
"@context": "https://meta.opencloud.eu/event.jsonld",
"@type": "urn:oc:type:event",
"@id": "urn:oc:event:3e041c88-088c-4015-a32e-5560561f6e26",
"start": "2355-02-27T11:09:15.918Z",
"end": "2355-02-27T13:52:59.010Z",
"status": "confirmed",
"invited": [
{
"@context": "https://meta.opencloud.eu/user.jsonld",
"@type": "urn:oc:type:user",
"@id": "urn:oc:user:adawes",
"name": "Anderson Dawes",
"email": "anderson@opa.org"
},
{
"@context": "https://meta.opencloud.eu/user.jsonld",
"@type": "urn:oc:type:user",
"@id": "urn:oc:user:kashford",
"name": "Klaes Ashford",
"email": "klaes@opa.org"
}
]
}
],
"members": [
{
"@context": "https://meta.opencloud.eu/contact.jsonld",
"@type": "urn:oc:type:contact",
"@id": "urn:oc:contact:9ccb247d-a728-4d8f-9259-c28cf6cef567",
"name": "Naomi Nagata",
"email": "naomo@opa.org"
},
{
"@context": "https://meta.opencloud.eu/user.jsonld",
"@type": "urn:oc:type:user",
"@id": "urn:oc:user:adawes",
"name": "Anderson Dawes",
"email": "anderson@opa.org"
},
{
"@context": "https://meta.opencloud.eu/user.jsonld",
"@type": "urn:oc:type:user",
"@id": "urn:oc:user:kashford",
"name": "Klaes Ashford",
"email": "klaes@opa.org"
}
],
"chat": {
"@context": "https://meta.opencloud.eu/file.jsonld",
"@type": "urn:oc:type:file",
"@id": "urn:oc:file:OPA:chatlogs/2355/02/27/a3f19df6-6c7d-45fa-b16c-6e168e2a2a43.md",
"href": "https://cloud.opencloud.eu/spaces/OPA/chatlogs/2355/02/27/a3f19df6-6c7d-45fa-b16c-6e168e2a2a43.md"
}
}
],
"link:mails": [
{
"@context": "https://meta.opencloud.eu/mail.jsonld",
"@type": "urn:oc:type:mail",
"@id": "583b9b66-c0b3-41ba-bf6c-a02ec5f4a638@smtp-07.opa.org",
"subject": "About bosmang Fred Johnson",
"date": "2355-01-03T09:39:44.919Z"
},
...
],
"link:shares": [
{
"@context": "https://meta.opencloud.eu/share.jsonld",
"@type": "urn:oc:type:share",
"@id": "841ef259-584d-4ce6-827f-b53f900c988d",
"filename": "remember the cant.jpg"
}
]
}
```
### Application Launchers
Have a UI that is comprised of multiple more-or-less separate applications, with an application launcher bar, with each application being an icon in itself in that launcher.
Similar to what e.g. Google does, or Open-Xchange App Suite.
* bad: does not make for an integrated application paradigm since users still have to context switch between those applications/views to perform tasks
-62
View File
@@ -1,62 +0,0 @@
---
status: proposed
date: 2025-06-25
author: Pascal Bleser <p.bleser@opencloud.eu>
decision-makers:
consulted:
informed:
title: "Groupware Software Stack"
template: https://raw.githubusercontent.com/adr/madr/refs/tags/4.0.0/template/adr-template.md
---
* Status: draft
## Context
Which software stack to choose for the implementation of the OpenCloud Groupware service?
## Considered Options
* [Go](https://go.dev/) with the OpenCloud framework, as it is used in OpenCloud
* Rust, as it is a similarly modern language, with know-how in Opentalk
* Java with an opinionated microservice framework (e.g. [Micronaut](https://micronaut.io/))
## Decision Outcome
The decision was taken to go with the existing Go technology stack used in OpenCloud, since it allows for
* everyone in the Groupware backend team to contribute
* having a single technology stack across all OpenCloud backend features
* having the option of a single binary deployment
### Consequences
TODO
### Confirmation
TODO
## Pros and Cons of the Options
### Go
* good: established in the OpenCloud team, with expertise, potentially broadening the team that can contribute to Groupware development
* good: make use of the existing infrastructure and framework, including the single binary deployment option
* bad: less mature and capable technology stack, potentially problematic with regards to lack of asynchronous I/O and streamed HTTP processing
### Rust
* good: shared knowledge with the team of developers at OpenTalk
* bad: little to no experience in the current OpenCloud team
### Java
* bad: little to no experience in the current OpenCloud team, with exception of the Groupware members
* good: extensive experience with Micronaut with one OpenCloud developer
* good: opinionated and well documented
* good: cloud native
* good: mature technology stack
* good: asynchronous I/O and virtual threads make for efficient resource usage
* potentially bad: likely to not fit well into low resource environments (although native compilation using GraalVM is possible)
* potentially bad: prevents the single binary deployment option from including Groupware
@@ -0,0 +1,226 @@
---
title: "5. Unified Search Index Mapping"
---
* Status: accepted
* Deciders: @aduffeck, @butonic, @dschmidt, @fschade
* Date: 2026-04-23, accepted and updated to the implemented state 2026-08-31
Reference: implemented by https://github.com/opencloud-eu/opencloud/pull/3345 (reflection-based mapping, search siblings, shared query lowering) and https://github.com/opencloud-eu/opencloud/pull/3197 (schema versioning and startup checks). https://github.com/opencloud-eu/opencloud/pull/2659 was the original proof-of-concept.
## Context and Problem Statement
This section describes the state at decision time (April 2026); the implementation has since resolved the problems listed here.
The search service supports two backends, bleve (embedded) and
OpenSearch (external). Each backend currently carries its own,
independently maintained description of the index layout:
- The bleve backend hand-builds a document mapping that explicitly
declares only Name, Tags, Favorites and Content. Everything else,
including the entire facet block (audio, image, photo, location),
is left to bleve's dynamic mapping.
- The OpenSearch backend ships a static JSON template that covers a
similar but not identical subset, plus a few OpenSearch-specific
primitives (path_hierarchy analyzer, wildcard MimeType). It does
not list the facet sub-fields either; they are produced by
OpenSearch's dynamic templating at first write.
- The graph DriveItem assembly path keeps its own private copy of a
reflection-based walker to turn CS3 ArbitraryMetadata back into
typed libregraph facets, parallel to the search service's
reflection helpers but maintained separately.
- The bleve KQL compiler keeps a hand-maintained set of field names
whose query values need to be pre-lowercased, with a comment that
literally says "Keep in sync with index.go".
The current implementation has three concrete problems:
1. **The two backends do not behave the same.** Both rely on their
own implicit defaults for fields that are not explicitly
declared. The inferred shapes differ: bleve produces keyword-
analyzed text, OpenSearch produces a `text + keyword` multi-field
with auto-detected dates. Nobody has written down which behavior
is the intended one. Two concrete instances surfaced while building
#2659:
- **mtime** is stored as an RFC3339 string. OpenSearch's dynamic
mapping auto-detects it as `date`; bleve leaves it `keyword`. So
`mtime:>...` is a chronological range on OpenSearch but a
lexicographic string compare on bleve.
- **name/tags**: bleve indexes a single lowercase token (exact or
wildcard match only); OpenSearch word-tokenizes, so a bare
`name:report` matches "My Report.txt" on OpenSearch but not on
bleve.
2. **Drift risk.** The OpenSearch JSON template is a subset of what
actually gets indexed. Even where it overlaps with the bleve
mapping it diverges on analyzer choices. Because the facet
fields were not reachable from user queries at the time (no dot
syntax in the KQL compilers, no facet exposure on the hit and
REPORT paths), the divergence has been invisible, but it would
surface the moment the first working cross-backend facet query
landed.
3. **Per-facet cost.** Adding a new facet (motionPhoto, etc.)
requires coordinated edits across the proto message, both backend
mappings, the bleve hit converters, the OpenSearch convert
closures, the search service's metadata persistence, the graph
DriveItem assembly, and the KQL compiler's lowercasing set. Most
of those edits are boilerplate following a copy-paste pattern.
Adding a genuinely new index capability (geopoint, wildcard,
...) means wiring it in at every one of those sites, and there
is no single place to hook a type-specific adapter.
### A note on backwards compatibility
That the facet fields were unreachable at decision time has a
useful corollary for this ADR: **changing the indexed shape of the
facet fields cannot break any existing client of the search
service**, because no client could successfully read them. The behavior changes
discussed below are therefore additive in a literal sense; nothing
that works today stops working as a result.
## Decision Drivers
* **Predictable OpenCloud API behavior independent of backend.**
Consumers of the search service should be able to rely on the
documented behavior of the API, not on which backend happens to
be configured. Today the same query can give different results
depending on whether bleve or OpenSearch is wired in (bleve's
dynamic default is `keyword`, exact match; OpenSearch's dynamic
default is `text + keyword`, also matches sub-tokens of a
string). That is backend-implementation leakage, and trying to
keep the two implicit defaults synchronized has not worked.
* Single source of truth for the indexed schema, so the two backends
cannot drift silently again.
* Reduce the per-facet cost so future facets (motionPhoto and
whatever comes next) can be added with minimal boilerplate.
* Establish a single place to hook index-type-specific behavior, so
a new capability needs to be implemented at most once per backend
and then becomes available for any field uniformly.
* A one-time reindex is an acceptable upgrade path. Both bleve and
OpenSearch store their mapping alongside the data; existing
indexes keep serving queries against their stored shape without
any automatic reshaping. Benefiting from the new behavior is done
by creating a fresh index and re-ingesting, which is the normal
reindex flow, rather than by inventing migration tooling.
## Considered Options
### Option 1: Do nothing, keep relying on implicit backend defaults
Accept that bleve and OpenSearch each fall back to their own
dynamic-mapping defaults for whatever is not explicitly declared,
and treat the observable search behavior of OpenCloud as "whatever
the configured backend happens to do". Adding a facet stays a
copy-paste coordination across half a dozen sites; the existing
divergence between bleve (keyword) and OpenSearch (`text + keyword`
multi-field plus auto-date detection) stays silently in place
until a working query actually reaches the diverging field and
returns different answers on the two backends.
Low upfront work, but it makes the OpenCloud API behavior a
function of the backend rather than a contract, and it keeps the
per-facet boilerplate cost for every new field.
### Option 2: Generate one backend's mapping from the other
Treat one backend as canonical (likely bleve, because Go types) and
derive the other. Partial answer; it still does not help the reader
path or the graph walker, and still leaves per-facet boilerplate in
non-mapping code.
### Option 3: A struct-driven mapping (chosen)
Let the Go struct that represents an indexed document, together
with a small overrides map, be the single source of truth. A
reflection-based helper walks the struct via json tags and emits
each backend's index mapping. The same definition drives the
write-time path, the hit-decoding path, and the query compiler's
case-folding rules. Any future field follows one declaration in
one place and falls through the whole pipeline consistently.
## Decision Outcome
Adopt Option 3. The Go struct that represents an indexed document,
together with a small overrides map, becomes the single source of
truth for the search index. The bleve and OpenSearch index
mappings, the write-time conversion, the hit-decoding path, and
the query compiler's case-folding rules are all derived from that
same definition. Drift between backends is prevented by
construction, because there is no second place to edit.
The overrides surface stays small. Each entry declares one of a
handful of things per field: a semantic type for fields whose
intent cannot be inferred from the Go type (for example a path-
analyzed field, a fulltext field, a geopoint field), or search-
behavior flags (case-insensitivity, word breaking, inclusion in
the catch-all field). Any field that needs something beyond the
inferred defaults gets one line in the overrides map and that one
line flows through every derived piece. Overrides are validated at
startup so a typo fails loudly instead of silently disabling a
setting.
A practical consequence of having one place to hook things: when a
new capability is needed (a geopoint representation, a sibling
field for a different aggregation behavior, a different analyzer
for a class of fields, ...) it can be implemented once per backend
in the central pipeline. After that, turning the capability on for
a specific field is a single override entry, and both backends
adopt it the same way. This ADR does not decide which capabilities
to add, only that they will land in this uniform shape rather than
through coordinated per-site edits.
### Facet values are indexed as case-preserving keywords
All facet sub-fields, meaning any leaf inside `audio`, `photo`, `image`, `location` and the facets that followed (`video`, `motionPhoto`, `livePhoto`), keep a case-preserving keyword as their stored base field on both backends. The raw value the extractor saw, or the CS3 ArbitraryMetadata string, is what lands in the index, and it is what returning, sorting and aggregations read.
This is the single intended semantic for facets across bleve and
OpenSearch, and it is driven by what aggregations need.
Aggregation buckets ("group all files by `audio.artist`", "list
distinct `photo.cameraMake`") return bucket keys drawn from the
indexed terms. If the indexing analyzer lowercases (OpenSearch's
default `text + keyword` multi-field against the text leg, or a
`lowercaseKeyword`-style analyzer), the buckets come back lower-
cased: a distinct-artists query would answer `motörhead` and
`queen` instead of the original display casings, and two tag
writers using `Motörhead` versus `MOTÖRHEAD` would collapse into a
single bucket labelled `motörhead`. For a metadata display use
case (thumbnails, facet filters in the UI, distinct lists) that
behavior is not what we want.
Searching is layered on top as exactly the strict superset the proposal reserved for later, and it shipped with the implementation: every keyword field additionally gets search-only sibling fields derived from the same definition, a `_lowercase` keyword sibling (doc values disabled; serves wildcards and `=` whole-value matches) and a `_words` text sibling (`words` analyzer: dots to spaces, unicode tokenization, lowercasing, no stemming; serves token and phrase matches). Case-insensitive, word-broken search is the default for every keyword field including facets; fields opt out per override where that is wrong: opaque ids (`ID`, `RootID`, `ParentID`, `Favorites`, `livePhoto.contentId`), the POSIX `Path`, the normalized `MimeType`, and `Content`, which is a fulltext field of its own. Aggregation buckets keep their display casing because they read the base field, never the siblings.
The query side derives from the same source: the shared lowering pass resolves field names case-insensitively, folds values and routes each match to the right sibling (wildcards to `_lowercase`, tokens and phrases to `_words`, `=` as a whole-value term on `_lowercase`), and both backend compilers consume that one decision. The engine parity suite pins the resulting behavior against bleve and OpenSearch, so a divergence fails CI instead of surfacing in production. The case-sensitivity alignment started in #2633 is completed by deriving both sides from the same source.
### Schema versioning and upgrades
Index names carry a schema version derived from the single `search.SchemaVersion` constant (`opencloud-resource-v4`, `bleve-v4`). On startup the service classifies the stored mapping against the code: additive changes (new fields, unchanged analyzers) are reconciled in place without a version bump, breaking changes make the service refuse to start and name the reindex steps. The upgrade path is a plain reindex (`opencloud search index --all-spaces`) into the new versioned index; older indexes stay untouched and can be deleted afterwards (services/search/MIGRATION.md). Golden mapping tests on both backends pin the rendered mappings and reuse the same classifier to tell a contributor whether a change needs only a golden regeneration or a version bump.
### Known trade-off
The write-time pipeline produces the document as a generic map via
a json round-trip. The OpenSearch write path already does the
equivalent today via the same json-based conversion helper, so
that path is unchanged. The bleve write path, which previously
handed the struct directly to bleve's reflective indexer, now goes
through the same map-producing step and pays roughly the same
cost. On hot paths (initial indexing of a large space) this is
measurable but not significant; if it ever matters, a direct
reflection walker can replace the json round-trip without changing
any call site.
### Follow-ups out of scope for this ADR
- **WebDAV REPORT facet exposure.** The current webdav search
endpoint renders none of the facet fields back to the client.
This is a missing feature, not a regression of the proposal;
its natural resolution is to let the graph-search endpoint
(proposed in #3211) take over once graph search lands.
- **Graph search hit conversion.** Graph search (#3211) translates
proto hits back into libregraph DriveItems with the same
facet-copy helper the search service uses internally.
- **reva's PROPFIND facet listing** uses its own hand-maintained
per-facet key lists. reva deliberately does not depend on the
libregraph Go types, so unifying those key sets is a reva-side
decision tracked separately.
- **Write-path performance.** The json round-trip in the bleve
write path is an optional optimisation target with no call-site
impact when it lands.
@@ -1,73 +0,0 @@
---
status: proposed
date: 2025-06-25
author: Pascal Bleser <p.bleser@opencloud.eu>
decision-makers:
consulted:
informed:
title: "Stalwart as Groupware Backend"
template: https://raw.githubusercontent.com/adr/madr/refs/tags/4.0.0/template/adr-template.md
---
* Status: draft
## Context
Which Groupware backend should be used?
## Considered Options
* [Stalwart](https://stalw.art/), contains not only mail but also collaborative features in an integrated package
* traditional IMAP/POP/SMTP stacks (e.g. [Dovecot](https://www.dovecot.org/) + [Postfix](https://www.postfix.org/))
## Decision Outcome
The decision was made to go with Stalwart, as it reduces the implementation effort on our end, allowing us for a much faster time-to-market with a significantly smaller team of developers.
### Consequences
We will most probably not need to develop much of a calendar or contact stack ourselves, as Stalwart is planning to implement those as part of the upcoming [JMAP](https://jmap.io/spec-core.html) specifications for [contacts](https://jmap.io/spec-contacts.html) and [calendars](https://jmap.io/spec-calendars.html).
The Groupware API will largely consist of a translation of high-level operations for the UI into JMAP operations sent to Stalwart.
#### Risks
On the flip side, there are a number of risks associated with that decision.
* Stalwart underdelivers on its promises
* calendaring provides insufficient features for our implementation (e.g. event series handling being too basic)
* not scaling for large deployments
* necessary adaptations (e.g. for authentication integration) are rejected upstream
* etc...
### Confirmation
TODO
## Pros and Cons of the Options
### Stalwart
* good: integrated package that contains IMAP/POP, SMTP, anti-spam, AI, encryption at rest and many other features in one
* good: modern stack
* good: capable of fault tolerance in large deployments through its use of [FoundationDB](https://www.foundationdb.org/)
* bad: relatively new project with few to no large scale productive deployments (yet)
* bad: significant human [SPoF](https://en.wikipedia.org/wiki/Single_point_of_failure)/[bus factor](https://en.wikipedia.org/wiki/Bus_factor) issue as the development team currently consists of one
* good: supports and drives the JMAP protocol ([JMAP Core](https://jmap.io/spec-core.html), [JMAP Mail](https://jmap.io/spec-mail.html), [JMAP Contacts](https://jmap.io/spec-contacts.html), [JMAP Calendars](https://jmap.io/spec-calendars.html), [JMAP Tasks](https://jmap.io/spec-tasks.html), ...),
* which provides more high-level operations that we don't need to implement ourselves,
* as well as a much cleaner specification that reduces efforts too,
* and additionally can be implemented with an efficient stateless HTTP I/O stack
* bad: no viable broad JMAP implementation alternatives in case Stalwart does not deliver ([Apache James](https://james.apache.org/) only seems to support a basic subset of JMAP)
* good: implements a lot of Groupware "business logic" on its own, reducing the implementation effort on our end,
* most notably by not having to deal with IMAP extensions and quirks,
* or the complexity of calendar events
### IMAP/SMTP
* good: there are a number of alternatives in case a specific implementation does not deliver
* good: the best implementation candidates are well-established, used in large amounts of productive deployments, supported by teams of developers
* bad: more complex stack composed of numerous components as opposed to an all-in-one implementation
* bad: the effort and complexity of having to deal with IMAP,
* its complexity due to its extensions and its many quirks,
* as well as a significantly less efficient I/O stack that requires stateful session handling
* bad: requires the complete implementation of contacts, calendars and tasks in our own stack, as none of those services are provided by IMAP/SMTP backends
-512
View File
@@ -1,512 +0,0 @@
---
status: accepted
date: 2025-07-22
author: pbleser-oc
consulted: AlexAndBear, butonic, dragotin, fschade, JammingBen, kulmann, martinherfurth, micbar, rhafer
title: "API for the Groupware Web UI"
---
<!-- markdownlint-disable-file MD024 MD033 -->
## Context
We need a comprehensive HTTP API for the OpenCloud Web UI to provide access to the following (upcoming) modules and Groupware functionalities:
* Mail
* Contacts
* Calendar
* Tasks
* Chat
* Configuration
```mermaid
graph LR
subgraph clients
ui(OpenCloud UI)
muas(Other<br>MUAs)
end
subgraph Backend
subgraph OpenCloud
direction TB
groupware("OpenCloud<br>Groupware")
drive("OpenCloud<br>Drive")
end
stalwart(Stalwart)
end
subgraph Storage
drive_storage[(drive<br>storage)]
stalwart_metadata[(metadata<br>storage)]
stalwart_storage[(object<br>storage)]
end
ui x@==>|?|groupware
x@{ animate: true }
ui-->|Graph|drive
muas-->|IMAP,SMTP,*DAV|stalwart
groupware-->drive
groupware-->|JMAP|stalwart
drive-->drive_storage
stalwart-->stalwart_metadata
stalwart-->stalwart_storage
```
Additionally, the API must also be able to provide information about related resources and their relationships, as outlined in [the Resource Linking ADR](./0003-groupware-resource-linking.md).
For the OpenCloud Drive services, the communication between UI client and backend services is performed via the [LibreGraph API](https://github.com/opencloud-eu/libre-graph-api), which is based on [Microsoft Graph](https://developer.microsoft.com/en-us/graph). The goal of this ADR is **not** to question or change that decision, and the choice of an option is merely for the communication with the Groupware backend.
Communication between the OpenCloud Groupware and Stalwart will make use of the [JMAP (JSON Meta Application Protocol) protocol](https://jmap.io/spec-mail.html).
The API for the OpenCloud Web UI is **not** supposed to be an abstraction of that and thus may use JMAP data formats.
Other [MUAs (Mail User Agents)](https://en.wikipedia.org/wiki/Email_client) converse directly with Stalwart using [IMAP](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol) or [POP3](https://en.wikipedia.org/wiki/Post_Office_Protocol), [SMTP](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol), [CalDAV](https://en.wikipedia.org/wiki/CalDAV), [CardDAV](https://en.wikipedia.org/wiki/CardDAV), or JMAP itself.
This ADR concerns the decision regarding which API approach/process/technology/specification to use, not the details of the data model and such, which will need to be fleshed out following the requirements and priorities of the OpenCloud UI Client development, regardless of the selected approach.
## Decision Drivers
### UI Driven
The decision must be significantly driven by the OpenCloud UI Client developers, since they are the primary consumers of the API.
They will also be the sole consumers for a foreseeable while until the OpenCloud Groupware UI reaches a stable feature-complete milestone, which is the earliest point in time for the APIs to be considered stable as well and potentially be consumed by third parties.
Backend developers are stakeholders in that aspect as well though, as the choice of API approach has an impact on the complexity, costs and maintainability of the backend services as well.
### Economic Awareness
Reduction of complexity and implementation efforts, albeit not at all costs, and not only on the short run.
It is obviously of advantage when an option requires less implementation, or less complexity in its implementation.
### Efficiency
Regarding efficiency, the goal is to design an API that is tailored to providing responsiveness ([pagination](https://apisyouwonthate.com/blog/api-design-basics-pagination/), [SSEs (Server-Side Events)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events), ...) and good network performance.
The latter is achieved by minimizing the number of roundtrips between the client and the servers, which, in turn, is typically achieved through the use of higher level APIs as opposed to a granular API that provides more flexibility but also, by its very nature, requires the combination of multiple request-response roundtrips over the wire.
### Third Party Consumption
We are assuming that the APIs are public APIs (not just technically) and may be consumed by SDKs and third parties.
Implications are that care must be put into providing an API that is stable, versioned, has a changelog, and potentially provided as a product with [LTS (Long-term Support)](https://en.wikipedia.org/wiki/Long-term_support) options.
This also hints at the necessity of a capability exchange/discovery protocol between clients and the Groupware backend, as we will have different versions of clients and servers in the wild, and they need to be able to understand each other. Crucially, if locally running clients are developed, they can go a long time without being updated.
## Considered Options
* [LibreGraph](#libregraph)
* [JMAP](#jmap)
* [custom REST API](#custom-rest-api) (albeit potentially based on standards, at least partially)
## Decision Outcome
The decision was made to go with the custom REST implementation option, mainly due to
* the use of LibreGraph providing little benefits
* if would provide us with a fleshed out API for groupware
* but we would not implement it fully
* and it is really an API for Outlook and Exchange, not a generic groupware standard
* furthermore, a significant blocker is that it does not provide for a way to support multiple accounts for a user
* the experience of implementing and using the LibreGraph API for the Drive components has made light of some challenges that we would not like to repeat
* using JMAP directly
* is a very interesting option in terms of standards, as it is an RFC,
* but we currently see that approach as too risky as per the potential complexity of parsing payloads of JMAP commands and their backreferences, plugging those across commands that must be forwarded as-is to Stalwart and others that need to be handled by the Groupware middleware itself, but also the potential need to reverse engineer the high-level meaning of chained low-level JMAP commands in order to implement enrichment, caches, reverse indexes, etc...
* however, it might be a better path forward in the future, especially if JMAP becomes a viable option for replacing the current use of LibreGraph as well
### Consequences
* we will need to design an API on our own, from scratch, albeit maximally making use of JMAP data structures
* that API will need to be maintained as a product, with documentation, versioning, LTS
## Pros and Cons of the Options
* [LibreGraph](#proscons-libregraph)
* [JMAP](#proscons-jmap)
* [Custom REST API](#proscons-custom)
### <a id="proscons-libregraph"/>LibreGraph
[LibreGraph](https://github.com/opencloud-eu/libre-graph-api) is an API specification that is heavily inspired by and based on [Microsoft Graph](https://developer.microsoft.com/en-us/graph), of which it is a partial implementation, but also with modifications where necessary.
Example:
```text
GET /v1.0/me/messages?$select=sender,subject&$count=50&$orderby=received
```
#### Good
* is already in use as the API for OpenCloud Drive operations, with a small stack to use it in the OpenCloud Web UI
* provides an API and data model that has already been thought out and used in production (albeit with only few different implementations)
#### Neutral
* does not have to follow the Microsoft Graph API, can be customized to our own needs, but in which case it becomes doubtful that there is any benefit in mimicking the Graph API in the first place if we diverge from it
* there is no compatibility benefit
* the only MUA that uses the Microsoft Graph API is Microsoft Outlook, and it is not a goal to support Microsoft Outlook as a MUA beyond standard IMAP/SMTP/CalDAV/CardDAV services (and that would be Microsoft Graph, not LibreGraph nor any customizations we would require)
* we will not implement all of the Microsoft Graph API
* we will not implement parts of the Microsoft Graph API as-is either, but will require to make modifications
* if there is a requirement for considering that API as a public API for third party integrators, then the API also needs to be documented, maintained, versioned, and kept stable as much as possible (this is neutral because it is a requirement that exists with every option)
#### Bad
* not an easy API to implement
* although we have libraries that take care of some of the more complex parts, such as parsing [OData](https://www.odata.org/) expressions
* really only easy to use when backed by a relational database and an object relational mapping framework using [ASP.NET](https://dotnet.microsoft.com/en-us/apps/aspnet) or [JPA](https://en.wikipedia.org/wiki/Jakarta_Persistence)/[Hibernate](https://hibernate.org/)
* its data model and peculiar interpretation of REST are really not [idomatic](https://en.wikipedia.org/wiki/HATEOAS) at all, and are clearly the result of reverse engineering the capabilities of Microsoft SQL Server and Exchange into a "standard" from the back, and then Microsoft Outlook's features and capabilities from the front
* not tailored to our needs
* we will most probably have a lot of cases in which we have to twist the Graph API to express what the UI needs
* will require using complex filters, which then require complex parsing in the backend in order to translate them into JMAP
* as opposed to directly using an expressive and maximally matching API in the first place
* we are likely to encounter use-cases that are not covered by the Graph API (especially due to our resource linking approach)
* does not support multiple accounts per user
* would require the addition of an account parameter, as a query parameter or as part of the path, which would make every URL in the API incompatible with Microsoft Graph
* more implementation effort than JMAP
* the JMAP RFCs already provides a data model, and we would end up converting between them all the time, with incompatibilities (Graph has attributes JMAP doesn't, and the other way around)
* possibly (probably?) more implementation effort than a custom REST API, due to its complexity
#### Decision Drivers
* UI Driven
* some members the OpenCloud Web Team strongly prefers not to use LibreGraph due to its complexity and to the fact that we would have to reftrofit operations into an existing API that was designed by a third party
* one upside is that there is already a client stack for performing LibreGraph operations, which could be reused to some degree for the Groupware APIs as well; it does not amount to all that much code though
* Economic Awareness
* more complexity and more effort as the other options due to the inherent complexity of the specification
* a data model is already specified in full, which might save us some time on that front
* although probably not really either since the actual data model we will work with on the backend is prescribed by JMAP, and we will only be looking to map attributes betsween JMAP and LibreGraph
* the data model is not necessarily thorougly documented either, which will leave room for interpretation, also due to incompatibilities between JMAP and Graph
* there will be attributes that are defined in JMAP and that we will receive from Stalwart that will not have a corresponding attribute in Graph (or be a list of values as opposed to a single value), and those will require to either lose some data by squashing it into the Graph data model, or extending the Graph data model which renders us incompatible with it
* Efficiency
* since the API is not tailored to our needs, we are much more likely to end up performing multiple roundtrips for single high level operations
* Third Party Consumption
* for some of the operations, we could point to the Microsoft Graph documentation, although that would not make for a great experience either, we would probably need to replicate it
* our deviations and extensions will have to be maintained just like the other options
* LibreGraph doesn't help with API stability either since
* we don't implement all of it, and need to document what we implement and what we don't,
* won't be compatible either due to modifications (additional parameters, unsupported parameters, different interpretations),
* and will just as equally need to evolve it as the other options, requiring the documentation of changes as well
* will be required to be maintained as a public API
* documentation
* LTS
* versioning
### <a id="proscons-jmap"/>JMAP
[JMAP (JSON Meta Application Protocol)](https://jmap.io/spec.html) is a set of specifications that are codified in RFCs:
* [RFC 8620](https://tools.ietf.org/html/rfc8620): core JMAP protocol
* [RFC 8261](https://tools.ietf.org/html/rfc8621): JMAP Mail
* [RFC 8887](https://www.rfc-editor.org/rfc/rfc8887.html): JMAP subprotocol for WebSocket
* [RFC 9404](https://www.rfc-editor.org/rfc/rfc9404.html): JMAP Blob Management Extension
* [RFC 9425](https://www.rfc-editor.org/rfc/rfc9425.html): JMAP Quotas
* [RFC 9553](https://www.rfc-editor.org/rfc/rfc9553.html): uses JSContact
* [RFC 8984](https://www.rfc-editor.org/rfc/rfc8984.html): uses JSCalendar
of which some are still in development at the time of writing:
* [JMAP Contacts](https://jmap.io/spec-contacts.html)
* [JMAP Calendars](https://jmap.io/spec-calendars.html)
* [JMAP Sharing](https://jmap.io/spec-sharing.html)
* [JMAP Tasks](https://jmap.io/spec-tasks.html)
To exemplify the JMAP protocol, the following code block is a JMAP request that
* fetches the 30 last received emails from a mailbox (folder)
* the threads of those emails
* email metadata of all of those threads, including a preview
<details open>
<summary>Click here to toggle the display of this example.</summary>
```json
[[ "Email/query", {
"accountId": "ue150411c",
"filter": {
"inMailbox": "fb666a55"
},
"sort": [{
"isAscending": false,
"property": "receivedAt"
}],
"collapseThreads": true,
"position": 0,
"limit": 30,
"calculateTotal": true
}, "0" ],
[ "Email/get", {
"accountId": "ue150411c",
"#ids": {
"resultOf": "0",
"name": "Email/query",
"path": "/ids"
},
"properties": [
"threadId"
]
}, "1" ],
[ "Thread/get", {
"accountId": "ue150411c",
"#ids": {
"resultOf": "1",
"name": "Email/get",
"path": "/list/*/threadId"
}
}, "2" ],
[ "Email/get", {
"accountId": "ue150411c",
"#ids": {
"resultOf": "2",
"name": "Thread/get",
"path": "/list/*/emailIds"
},
"properties": [
"threadId",
"mailboxIds",
"keywords",
"hasAttachment",
"from",
"subject",
"receivedAt",
"size",
"preview"
]
}, "3" ]]
```
</details>
#### Good
* flexible protocol that can easily be implemented by clients
* potentially does not require implementation efforts on the backend side
* would obviously support the full potential of JMAP and Stalwart
* we could potentially extend JMAP with our own data models and operations based on the [JMAP Core Protocol](https://jmap.io/spec-core.html), possibly even propose them as RFCs
* we can start with JMAP request objects that contain only a few or even only one JMAP methods (indicated by the [maxCallsInRequest capability](https://datatracker.ietf.org/doc/html/rfc8620#section-2)), allowing more calls as we need
* clients could implement the funtionality they need using multiple requests in the beginning, then we implement missing functionality on the server
* this would allow us to speed up requests that we need while at the same time giving clients the ability to make any necessary individual calls
* probably only a partially useful approach since chaining JMAP requests is necessary for even the most mundane operations, to avoid the inefficiency of multiple roundtrips
#### Neutral
* the [existing JMAP specifications](https://jmap.io/spec.html) will not cover 100% of the Web UI API needs (e.g. configuration settings[^config], [resource linking](./0003-groupware-resource-linking.md), ...), but that does not prevent us from implementing additional custom APIs, either as non-JMAP REST APIs, or as extensions of JMAP
* we would need to gauge whether JMAP communication
* should occur directly between the OpenCloud UI and Stalwart,
* or whether an OpenCloud Groupware service should be used as an intermediary and as an [anti-corruption layer](https://ddd-practitioners.com/home/glossary/bounded-context/bounded-context-relationship/anticorruption-layer/)
* if there is a requirement for considering that API as a public API for third party integrators, then the API also needs to be documented, maintained, versioned, and kept stable as much as possible (this is neutral because it is a requirement that exists with every option)
[^config]: although Stalwart will most likely have a [JMAP API for application configuration settings as well](https://matrix.to/#/!blIcSTIPwfKMtOEWcg:matrix.org/$CD9C6IZN28bbmN0Arb_Y-RapgsS4XqAqnDgf15yJahM?via=matrix.org&via=mozilla.org&via=chat.opencloud.eu)
> Message from [Mauro](https://github.com/mdecimus):
>
> Hi everyone, I'm curious what you think about standardizing a simple protocol/extension for users to easily manage certain account settings directly from their email clients. For instance, such a protocol could handle:
>
> * Passwords, app passwords, and MFA settings
> * Locale preferences
> * Timezone configuration
> * Basic email forwarding (without needing custom Sieve scripts)
> * Vacation/auto-responses
> * Blocking specific email addresses
> * Spam reporting (though not strictly a setting)
> * Calendar-related preferences
> * Encryption-at-rest settings
> * Mail auto-expunge policies
> * ... and potentially more.
>
> My initial thought is to implement this as a JMAP extension rather than inventing another protocol similar to ManageSieve, which feels somewhat like a "Frankenstein" IMAP extension.
>
> Many mailbox providers already offer some or all of these settings through their web interfaces, but a standardized JMAP-based extension could let users adjust these directly within their preferred email clients or via APIs.
#### Bad
* potentially bad: most probably too flexible for its own good, as it makes it difficult to reverse-engineer the high-level meaning of a set of JMAP requests in order to capture its semantics, e.g. to implement caching or reverse indexes for performance
* since the OpenCloud Drive backends use the LibreGraph API, using a JMAP based API for Groupware bears the risk of having multiple APIs to do the same thing, which we need to be careful about, and avoid if possible
> [!NOTE]
> This seems like a mild "bad" item, but the risk risk here is significant: if it turns out that we need to capture the semantics of API requests to perform additional operations (e.g. caching or indexing for performance reasons, or to decorate the data from Stalwart with information from other services), then we would have to re-implement the whole API as JMAP is too complex to parse to extract semantics from.
#### Two Approaches
There are two approaches as to how to implement our protocol based on JMAP:
* either our clients must split JMAP operations and send some to Stalwart, and others to the Groupware backend (depending on which endpoint is in charge of which API)
* or our clients send all the JMAP operations to the Groupware backend, which is then in charge to relay JMAP commands that are to be handled by Stalwart to Stalwart
##### Directly to Stalwart
If the OpenCloud UI Client communicates directly with Stalwart (using JMAP), then
* good: we don't need to implement any sort of "bridge" in the OpenCloud Groupware service (although the implementation effort is likely to be low)
* good: we avoid an additional hop in the network, gaining on performance and potentially on throughput
* bad: it will have to perform additional API requests for data and features that are not provided by Stalwart with the OpenCloud Groupware service (e.g. [Resource Linking](./0003-groupware-resource-linking.md)) as well, which is likely to lead to an increase in the number of network roundtrips
* bad: would be unable to extend the protocol with OpenCloud Groupware specific models and data
* bad: would be unable to implement caching or similar performance improvements if necessary
* bad: prevents us from implementing infrastructure features that are not present in Stalwart and might never be in the way we would need them, e.g. sharding across multi-site redundancy
```mermaid
graph LR
subgraph clients
ui(OpenCloud UI)
muas(Other<br>MUAs)
end
subgraph Backend
subgraph OpenCloud
direction TB
groupware("OpenCloud<br>Groupware")
drive("OpenCloud<br>Drive")
end
stalwart(Stalwart)
end
subgraph Storage
drive_storage[(drive<br>storage)]
stalwart_metadata[(metadata<br>storage)]
stalwart_storage[(object<br>storage)]
end
ui x@==>|JMAP|stalwart
x@{ animate: true }
ui y@==>|JMAP or REST|groupware
y@{ animate: true }
ui-->|Graph|drive
muas-->|IMAP,SMTP,*DAV|stalwart
groupware-->drive
groupware-->|JMAP|stalwart
drive-->drive_storage
stalwart-->stalwart_metadata
stalwart-->stalwart_storage
```
##### Groupware intermediary
Alternatively, if the OpenCloud UI Client exclusively communicates with the OpenCloud Groupware service (using JMAP), then
* good: the OpenCloud Groupware service acts as a anti-corruption layer, which would allow us to
* implement caching and similar performance improvement measures if necessary (e.g. reverse indexing of costly data)
* implement infrastructure features that are not present in Stalwart and might never be in the way we would need them, e.g. sharding across multi-site redundancy
* extend the JMAP protocol
* good: it enables us to minimize network roundtrips between the OpenCloud UI Client and the OpenCloud Groupware backend as there is no need to perform additional requests elsewhere
* bad: we have an additional intermediary hop that "just" relays operations to Stalwart most of the time
* due to Go HTTP stack limitations (lack of zero-copy asynchronous I/O),
* that might incur a cost of "needlessly" copying data in memory
* as well as performing blocking I/O (at the very least since JMAP requests first need to be read in full by te OpenCloud Groupware before they then can be sent to Stalwart more or less as-is, and the same applies to the responses)
```mermaid
graph LR
subgraph clients
ui(OpenCloud UI)
muas(Other<br>MUAs)
end
subgraph Backend
subgraph OpenCloud
direction TB
groupware("OpenCloud<br>Groupware")
drive("OpenCloud<br>Drive")
end
stalwart(Stalwart)
end
subgraph Storage
drive_storage[(drive<br>storage)]
stalwart_metadata[(metadata<br>storage)]
stalwart_storage[(object<br>storage)]
end
ui y@==>|JMAP|groupware
y@{ animate: true }
ui-->|Graph|drive
muas-->|IMAP,SMTP,*DAV|stalwart
groupware-->drive
groupware-->|JMAP|stalwart
drive-->drive_storage
stalwart-->stalwart_metadata
stalwart-->stalwart_storage
```
#### Decision Drivers
* UI Driven
* the UI team did not express any particular preference for this option, but the JMAP protocol is simple to implement on any client
* Economic Awareness
* there would be less of a need to develop an API, but that doesn't put much into the balance
* developing a generic inbound JMAP command processing engine that is capable of resolving backreferences with requests that can be sent out to different backends (Stalwart, Drive, Groupware, OpenTalk, ...) seems risky in terms of complexity, also since Go doesn't have much of a [well-supported Reactive framework](https://github.com/ReactiveX/RxGo)
* Efficiency
* the ability of the JMAP protocol to chain multiple low-level commands provides for a very efficient way to compose higher-level operations without the need for multiple round-trips
* Third Party Consumption
* for some of the operations, we could point to the JMAP documentation and RFCs, although that would not make for a great experience either, we would probably need to replicate it
* our protocol extensions will have to be maintained just like the other options
* will be required to be maintained as a public API
* documentation
* LTS
* versioning
### <a id="proscons-custom"/>Custom REST API
A custom REST API would implement the resources and semantics as they are needed by the UI, and would be strongly if not fully UI-driven.
The data model should remain close or equal to JMAP's, to avoid data loss by converting back and forth.
We might look into existing specifications for formatting JSON payloads, such as [JSON:API](https://jsonapi.org/) or partial ones such as such as [JSON-LD](https://json-ld.org/) for relationships between resources, but this is currently outside of the scope of this ADR.
```mermaid
graph LR
subgraph clients
ui(OpenCloud UI)
muas(Other<br>MUAs)
end
subgraph Backend
subgraph OpenCloud
direction TB
groupware("OpenCloud<br>Groupware")
drive("OpenCloud<br>Drive")
end
stalwart(Stalwart)
end
subgraph Storage
drive_storage[(drive<br>storage)]
stalwart_metadata[(metadata<br>storage)]
stalwart_storage[(object<br>storage)]
end
ui y@==>|REST|groupware
y@{ animate: true }
ui-->|Graph|drive
muas-->|IMAP,SMTP,*DAV|stalwart
groupware-->drive
groupware-->|JMAP|stalwart
drive-->drive_storage
stalwart-->stalwart_metadata
stalwart-->stalwart_storage
```
Example:
```text
GET /groupware/startup/1/?mails=50
```
#### Good
* completely tailored to the needs of the OpenCloud UI
* a higher-level API allows for easily understanding the semantic of each operation, which enables the potential for keeping track of data in order to implement reverse indexes and caching, if necessary to achieve functional or performance goals, as opposed to using a lower-level API such as JMAP which is maximally flexible and difficult to reverse-engineer the meaning of the operation and data
* can also be tailored to the capabilities of JMAP without exposing all of its flexibility
* provides the potential for expanding upon what JMAP provides
* would support the full potential of JMAP and Stalwart since the API would be designed accordingly
* allows learning how to use and cache individual JMAP method call responses, allowing to make a better decision in the future if JMAP should be used by clients
#### Neutral
* if there is a requirement for considering that API as a public API for third party integrators, then the API also needs to be documented, maintained, versioned, and kept stable as much as possible (this is neutral because it is a requirement that exists with every option)
#### Bad
* only partially follows any standards (REST, JSON, JMAP for data models)
* requires designing the API from scratch, as opposed to using the Graph API which already prescribes one
* although it probably makes sense to re-use the data model of JMAP, which is prescribed in RFCs, also to avoid data loss and copying things around needlessly
* since the OpenCloud Drive backends use the LibreGraph API, using a custom REST API for Groupware bears the risk of having multiple APIs to do the same thing, which we need to be careful about, and avoid if possible
#### Decision Drivers
* UI Driven
* favoured solution for the OpenCloud Web UI team
* Economic Awareness
* designing a new custom API is not much effort since it is UI requirements driven
* maintaining a new custom API or JMAP extensions is not more effort either, since the exact same thing needs to be done with LibreGraph, as it will have numerous exceptions and will require documenting those, as well as which parts of the Microsoft Graph API are implemented and which aren't
* Efficiency
* the most efficient approach since it is tailored to what is actually needed for the OpenCloud UI, which will allow us to reduce the roundtrips to a minimum
* Third Party Consumption
* a custom API will be required to be maintained as a public API
* documentation
* LTS
* versioning
-52
View File
@@ -1,52 +0,0 @@
---
status: proposed
date: 2025-07-07
author: Pascal Bleser <p.bleser@opencloud.eu>
decision-makers:
consulted:
informed:
title: "Groupware Configuration Settings"
template: https://raw.githubusercontent.com/adr/madr/refs/tags/4.0.0/template/adr-template.md
---
* Status: draft
## Context
User Preferences need to be configurable through the UI and persisted in a backend service in order to be reliably available and backed up.
Such configuration options have default values that need to be set on multiple levels:
* globally
* by tenant
* by sub-tenant
* by group of users
* by user
Some options might even be client-specific, e.g. differ between the OpenCloud Web UI on desktop and the OpenCloud Web UI on mobile.
Furthermore, some options might be enforced and may not be overridden on every level (e.g. only globally or by tenant, by not modifiable by users.)
Ideally, the configuration settings have an architecture that permits pluggable sources.
This level of necessary complexity has a few drawbacks, the primary one being that it can become difficult to find out why a user sees this or that behavior in their UI, and thus to trace down where a given configuration setting is made (globally, on tenant level, etc...). It is thus critical to include tooling that allows to debug them.
## Considered Options
TODO
## Decision Outcome
TODO
### Consequences
TODO
### Confirmation
TODO
## Pros and Cons of the Options
TODO
+100 -116
View File
@@ -1,32 +1,28 @@
module github.com/opencloud-eu/opencloud
go 1.25.8
go 1.25.9
require (
dario.cat/mergo v1.0.2
github.com/CiscoM31/godata v1.0.11
github.com/KimMachineGun/automemlimit v0.7.5
github.com/KimMachineGun/automemlimit v1.0.0
github.com/Masterminds/semver v1.5.0
github.com/MicahParks/keyfunc/v2 v2.1.0
github.com/Nerzal/gocloak/v13 v13.9.0
github.com/ProtonMail/go-crypto v1.1.6
github.com/bbalet/stopwords v1.0.0
github.com/beevik/etree v1.6.0
github.com/blevesearch/bleve/v2 v2.6.0
github.com/brianvoe/gofakeit/v7 v7.7.3
github.com/beevik/etree v1.7.1
github.com/blevesearch/bleve/v2 v2.6.1
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/coreos/go-oidc/v3 v3.19.0
github.com/coreos/go-oidc/v3 v3.20.0
github.com/cs3org/go-cs3apis v0.0.0-20260424072047-8d9ef7076ae9
github.com/davidbyttow/govips/v2 v2.18.0
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
github.com/emersion/go-imap/v2 v2.0.0-beta.5
github.com/gabriel-vasile/mimetype v1.4.13
github.com/gabriel-vasile/mimetype v1.4.15
github.com/ggwhite/go-masker v1.1.0
github.com/go-chi/chi/v5 v5.3.1
github.com/go-chi/chi/v5 v5.3.2
github.com/go-chi/render v1.0.3
github.com/go-jose/go-jose/v3 v3.0.5
github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-ldap/ldap/v3 v3.4.14
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3
github.com/go-micro/plugins/v4/client/grpc v1.2.1
github.com/go-micro/plugins/v4/logger/zerolog v1.2.0
@@ -46,86 +42,77 @@ require (
github.com/google/uuid v1.6.0
github.com/gookit/config/v2 v2.2.9
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0
github.com/invopop/validation v0.8.0
github.com/jellydator/ttlcache/v2 v2.11.1
github.com/jellydator/ttlcache/v3 v3.4.1
github.com/jhillyerd/enmime/v2 v2.2.0
github.com/jinzhu/now v1.1.5
github.com/justinas/alice v1.2.0
github.com/kovidgoyal/imaging v1.8.22
github.com/kovidgoyal/imaging v1.8.23
github.com/leonelquinteros/gotext v1.7.3-0.20260422134830-b012b4ccae69
github.com/libregraph/idm v0.5.0
github.com/libregraph/lico v0.67.0
github.com/microcosm-cc/bluemonday v1.0.27
github.com/miekg/dns v1.1.68
github.com/mna/pigeon v1.3.0
github.com/moby/moby/api v1.54.2
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/nats-io/nats-server/v2 v2.14.3
github.com/nats-io/nats.go v1.52.0
github.com/oklog/run v1.2.0
github.com/nats-io/nats-server/v2 v2.14.5
github.com/nats-io/nats.go v1.53.1
github.com/olekukonko/tablewriter v1.1.4
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.32.0
github.com/onsi/ginkgo/v2 v2.32.1
github.com/onsi/gomega v1.42.1
github.com/open-policy-agent/opa v1.18.2
github.com/open-policy-agent/opa v1.19.1
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d
github.com/opencloud-eu/reva/v2 v2.46.4-0.20260625152426-8cff2a7032ec
github.com/opensearch-project/opensearch-go/v4 v4.6.0
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902170011-45af3945a067
github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774
github.com/opensearch-project/opensearch-go/v4 v4.7.3
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
github.com/pkg/xattr v0.4.12
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_golang v1.24.1
github.com/prometheus/client_model v0.6.2
github.com/r3labs/sse/v2 v2.10.0
github.com/riandyrn/otelchi v0.12.3
github.com/rogpeppe/go-internal v1.15.0
github.com/rogpeppe/go-internal v1.16.0
github.com/rs/cors v1.11.1
github.com/rs/zerolog v1.35.1
github.com/sethvargo/go-password v0.3.1
github.com/sirupsen/logrus v1.9.4
github.com/shamaton/msgpack/v2 v2.4.1
github.com/sirupsen/logrus v1.10.1
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.12.1
github.com/test-go/testify v1.1.4
github.com/testcontainers/testcontainers-go v0.43.0
github.com/testcontainers/testcontainers-go/modules/opensearch v0.43.0
github.com/theckman/yacspin v0.13.12
github.com/testcontainers/testcontainers-go v0.44.0
github.com/testcontainers/testcontainers-go/modules/opensearch v0.44.0
github.com/thejerf/suture/v4 v4.0.6
github.com/tidwall/gjson v1.19.0
github.com/tidwall/pretty v1.2.1
github.com/tidwall/sjson v1.2.5
github.com/tus/tusd/v2 v2.10.0
github.com/unrolled/secure v1.16.0
github.com/vmihailenco/msgpack/v5 v5.4.1
github.com/wk8/go-ordered-map v1.0.0
github.com/xhit/go-simple-mail/v2 v2.16.0
go-micro.dev/v4 v4.11.0
go.etcd.io/bbolt v1.5.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/contrib/zpages v0.69.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
golang.org/x/crypto v0.53.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0
go.opentelemetry.io/contrib/zpages v0.71.0
go.opentelemetry.io/otel v1.46.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.45.0
go.opentelemetry.io/otel/sdk v1.46.0
go.opentelemetry.io/otel/trace v1.46.0
golang.org/x/crypto v0.55.0
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/image v0.43.0
golang.org/x/net v0.56.0
golang.org/x/image v0.45.0
golang.org/x/net v0.58.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.21.0
golang.org/x/term v0.44.0
golang.org/x/text v0.39.0
golang.org/x/tools v0.47.0
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa
google.golang.org/grpc v1.82.0
google.golang.org/protobuf v1.36.11
golang.org/x/sync v0.22.0
golang.org/x/term v0.45.0
golang.org/x/text v0.41.0
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d
google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
@@ -139,28 +126,28 @@ require (
github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/semver/v3 v3.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/alexedwards/argon2id v1.0.0 // indirect
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 // indirect
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
github.com/antithesishq/antithesis-sdk-go v0.7.2 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/bits-and-blooms/bitset v1.24.2 // indirect
github.com/blevesearch/bleve_index_api v1.3.11 // indirect
github.com/blevesearch/geo v0.2.5 // indirect
github.com/blevesearch/go-faiss v1.1.0 // indirect
github.com/blevesearch/bleve_index_api v1.4.1 // indirect
github.com/blevesearch/geo v0.2.6 // indirect
github.com/blevesearch/go-faiss v1.1.5 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.2.0 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.4.7 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.4.10 // 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
@@ -171,17 +158,16 @@ require (
github.com/blevesearch/zapx/v14 v14.4.3 // indirect
github.com/blevesearch/zapx/v15 v15.4.3 // indirect
github.com/blevesearch/zapx/v16 v16.3.4 // indirect
github.com/blevesearch/zapx/v17 v17.1.2 // indirect
github.com/blevesearch/zapx/v17 v17.2.3 // 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/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/ceph/go-ceph v0.40.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cevaris/ordered_map v0.0.0-20190319150403-3adeae072e73 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
@@ -203,42 +189,41 @@ require (
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc // indirect
github.com/emersion/go-message v0.18.1 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/emvi/iso-639-1 v1.1.1 // indirect
github.com/evanphx/json-patch/v5 v5.5.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gdexlab/go-render v1.0.1 // indirect
github.com/go-acme/lego/v4 v4.4.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-git/go-git/v5 v5.19.1 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-git/go-git/v5 v5.19.2 // indirect
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-micro/plugins/v4/events/natsjs v1.2.2 // indirect
github.com/go-micro/plugins/v4/store/nats-js v1.2.1 // indirect
github.com/go-micro/plugins/v4/store/redis v1.2.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
@@ -247,7 +232,6 @@ require (
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/snappy v1.0.0 // indirect
@@ -256,7 +240,6 @@ require (
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
github.com/google/renameio/v2 v2.0.2 // indirect
github.com/gookit/goutil v0.8.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/schema v1.4.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
@@ -267,14 +250,13 @@ require (
github.com/huandu/xstrings v1.5.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/inbucket/html2text v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/juliangruber/go-intersect v1.1.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/compress v1.19.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
@@ -290,27 +272,29 @@ require (
github.com/libregraph/oidc-go v1.1.0 // indirect
github.com/longsleep/go-metrics v1.0.0 // indirect
github.com/longsleep/rndm v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.42 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mattn/go-sqlite3 v1.14.49 // indirect
github.com/maxymania/go-system v0.0.0-20170110133659-647cc364bf0b // indirect
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mileusna/useragent v1.3.5 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/highwayhash v1.0.4 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.1.0 // indirect
github.com/minio/minio-go/v7 v7.2.1 // 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
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/client v0.4.0 // indirect
github.com/moby/moby/api v1.55.0 // indirect
github.com/moby/moby/client v0.5.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/sequential v0.7.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
@@ -322,6 +306,7 @@ require (
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.6 // indirect
@@ -332,17 +317,15 @@ require (
github.com/pablodz/inotifywaitgo v0.0.12 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/pquerna/cachecontrol v0.2.0 // indirect
github.com/prometheus/alertmanager v0.31.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/prometheus/statsd_exporter v0.22.8 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/rs/xid v1.6.0 // indirect
@@ -354,34 +337,33 @@ require (
github.com/samber/slog-common v0.21.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.2 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/kafka-go v0.4.50 // indirect
github.com/segmentio/kafka-go v0.4.51 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/sercand/kuberesolver/v5 v5.1.1 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/sethvargo/go-diceware v0.5.0 // indirect
github.com/shamaton/msgpack/v2 v2.4.1 // indirect
github.com/shirou/gopsutil/v4 v4.26.5 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
github.com/sethvargo/go-diceware v0.6.0 // indirect
github.com/sethvargo/go-password v0.4.0 // indirect
github.com/shirou/gopsutil/v4 v4.26.6 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spacewander/go-suffix-tree v0.0.0-20191010040751-0865e368c784 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/studio-b12/gowebdav v0.9.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/tklauser/go-sysconf v0.4.0 // indirect
github.com/tklauser/numcpus v0.12.0 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/trustelem/zxcvbn v1.0.1 // indirect
github.com/urfave/cli/v2 v2.27.7 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
github.com/vektah/gqlparser/v2 v2.5.34 // indirect
github.com/vektah/gqlparser/v2 v2.5.36 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map v1.0.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
@@ -389,24 +371,26 @@ require (
github.com/yashtewari/glob-intersection v0.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.etcd.io/etcd/api/v3 v3.6.11 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.11 // indirect
go.etcd.io/etcd/client/v3 v3.6.11 // indirect
go.etcd.io/etcd/api/v3 v3.6.13 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.13 // indirect
go.etcd.io/etcd/client/v3 v3.6.13 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.46.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.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.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/sys v0.46.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/ini.v1 v1.67.3 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
+193 -224
View File
@@ -70,14 +70,14 @@ github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CiscoM31/godata v1.0.11 h1:w7y8twuW02LdH6mak3/GJ5i0GrCv2IoZUJVqa/g5Yeo=
github.com/CiscoM31/godata v1.0.11/go.mod h1:ZMiT6JuD3Rm83HEtiTx4JEChsd25YCrxchKGag/sdTc=
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/KimMachineGun/automemlimit v1.0.0 h1:+MqlvDE/pkJNjk1rU+O14QsH8k10nJAD0frB0lsxyvw=
github.com/KimMachineGun/automemlimit v1.0.0/go.mod h1:n+BSXxQWDFS1DKh67Rqo0lgTsowsg6x65ak5uyngML0=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
@@ -115,8 +115,8 @@ github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/antithesishq/antithesis-sdk-go v0.7.2 h1:oEEedg1Xgi8drRjqB0f9tfjhLoInE0IYZfZ6zAhQUbY=
github.com/antithesishq/antithesis-sdk-go v0.7.2/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
@@ -130,12 +130,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.37.27/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bbalet/stopwords v1.0.0 h1:0TnGycCtY0zZi4ltKoOGRFIlZHv0WqpoIGUsObjztfo=
github.com/bbalet/stopwords v1.0.0/go.mod h1:sAWrQoDMfqARGIn4s6dp7OW7ISrshUD8IP2q3KoqPjc=
github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
github.com/beevik/etree v1.7.1 h1:xBlSFAjlMfZkrCNKCEdZ2MSN9pfQYeRBaISDnAVi4Ek=
github.com/beevik/etree v1.7.1/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -148,22 +146,22 @@ github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngE
github.com/bits-and-blooms/bitset v1.24.2 h1:M7/NzVbsytmtfHbumG+K2bremQPMJuqv1JD3vOaFxp0=
github.com/bits-and-blooms/bitset v1.24.2/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.6.0 h1:Cyd3dd4q5tCbOV8MnKUVRUDYMHOir9xn12NZzXVSEd4=
github.com/blevesearch/bleve/v2 v2.6.0/go.mod h1:gLmI8lWgHgrIYf7UpUX7JISI1CaqC6VScu46mHThuAY=
github.com/blevesearch/bleve_index_api v1.3.11 h1:x29vbV8OjWfLcrDVd7Lr1q+BkLNS0JWNEig0MCVnKH4=
github.com/blevesearch/bleve_index_api v1.3.11/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.5 h1:yJg9FX1oRwLnjXSXF+ECHfXFTF4diF02Ca/qUGVjJhE=
github.com/blevesearch/geo v0.2.5/go.mod h1:Jhq7WE2K6mJTx1xS44M2pUO6Io+wjCSHh1+co3YOgH4=
github.com/blevesearch/go-faiss v1.1.0 h1:xM7Jc0ZUCv5lssG9Ohj3Jv0SdTpxcUABU1dDt9XVsc4=
github.com/blevesearch/go-faiss v1.1.0/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/bleve/v2 v2.6.1 h1:47vLskRTqxvQEtxVPYHjf5KpOgzD2msslXFjvUQCgWQ=
github.com/blevesearch/bleve/v2 v2.6.1/go.mod h1:Dvvx6ZoEBTOj6RSzfk0lEz0wce/qhe2yOUubXeuzd2c=
github.com/blevesearch/bleve_index_api v1.4.1 h1:CYIyecFlI+/RYjzUm+NmDjYbSvk870Bb7f+Vl4b12q8=
github.com/blevesearch/bleve_index_api v1.4.1/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.6 h1:7K1oyQKYlauC+mJuo2AfNPyjN/4mihEoJMfyClVH1Mo=
github.com/blevesearch/geo v0.2.6/go.mod h1:6qzVUiB4BK47QkSZcRqiXEP2W3EeXuzM5XFTF8AdZ8A=
github.com/blevesearch/go-faiss v1.1.5 h1:/IU5lkOahH9Ghfk9n3F6N0XD7PYVXZJWmNDc9TtXuco=
github.com/blevesearch/go-faiss v1.1.5/go.mod h1:w3W9AiWsFRGVaMG+/cmJi7iHEAuGyC6blsgO1EzCK/M=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.2.0 h1:l33nNKPFcBjJUMwem6sAYJPUzhUCABoK9FxZDGiFNBI=
github.com/blevesearch/mmap-go v1.2.0/go.mod h1:Vd6+20GBhEdwJnU1Xohgt88XCD/CTWcqbCNxkZpyBo0=
github.com/blevesearch/scorch_segment_api/v2 v2.4.7 h1:GlMzW08hcsM3DnLUxhyF/1PcDal1qtvvIuytuph5djw=
github.com/blevesearch/scorch_segment_api/v2 v2.4.7/go.mod h1://IJ7tG3QCf0cWW/aVSXqy77tc1AvLu3fcJLYEvOAFs=
github.com/blevesearch/scorch_segment_api/v2 v2.4.10 h1:C3873+iWZ0YJM2ijaSHhJJzSvD4x1k+5UaQdGygZVhM=
github.com/blevesearch/scorch_segment_api/v2 v2.4.10/go.mod h1:WUUkAocbkDlNK/kgAE13NvS9oxe+u618mYZ8sOvcCc4=
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=
@@ -184,8 +182,8 @@ github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2l
github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0=
github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg=
github.com/blevesearch/zapx/v17 v17.1.2 h1:avbOk2igaASNoiy0BE/jPgcxAnRI2PGeydeP4hg7Ikk=
github.com/blevesearch/zapx/v17 v17.1.2/go.mod h1:WQObxKrqUX7cd0G1GMvDfc/bmZzQvoy7APOPimx7DiI=
github.com/blevesearch/zapx/v17 v17.2.3 h1:UYYJPAt5b2tVxldx5h0jmv23RMsg8/UZKFVya7v92po=
github.com/blevesearch/zapx/v17 v17.2.3/go.mod h1:r7mb4QWbDQSkbAnOjCb9iCfkcrzajB4yBdJpuBIo/fE=
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=
@@ -193,14 +191,10 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR
github.com/bombsimon/logrusr/v3 v3.1.0 h1:zORbLM943D+hDMGgyjMhSAz/iDz86ZV72qaak/CA0zQ=
github.com/bombsimon/logrusr/v3 v3.1.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/brianvoe/gofakeit/v7 v7.7.3 h1:RWOATEGpJ5EVg2nN8nlaEyaV/aB4d6c3GqYrbqQekss=
github.com/brianvoe/gofakeit/v7 v7.7.3/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3 h1:h8Z0hBv5tg/uZMKu8V47+DKWYVQg0lYP8lXDQq7uRpE=
github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3/go.mod h1:eE/tD53n3KbVrzrWxKLxdkGw45Fg1qaNLWjpJMvIUF4=
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0 h1:WRZXnLPIer/TWs5aYPaMlmVcOlzmR6Ur6wjLRIQOhTQ=
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0/go.mod h1:GP93piU+39CoFVCQ5xfHrPOUtL0APlMnkbblJ2d3YY0=
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
@@ -211,8 +205,6 @@ 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/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/ceph/go-ceph v0.40.0 h1:Wz9WOX6i73Hz74mpwhTO9S6IyX3eFPv88VUc7FRMPRk=
github.com/ceph/go-ceph v0.40.0/go.mod h1:1oFtT/x/4y+teLsNiogdd/Kj81Gmrw6JM5w1bWnZoc4=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -226,10 +218,10 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304=
@@ -244,8 +236,8 @@ github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6a
github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I=
github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE=
github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
@@ -287,8 +279,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjY
github.com/deepmap/oapi-codegen v1.3.11/go.mod h1:suMvK7+rKlx3+tpa8ByptmvoXbAV70wERKTOGH3hLp0=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE=
github.com/dgraph-io/badger/v4 v4.9.2 h1:Wb5qw8gElqwV1a8msHTeQKova9b1V10heFKMIiPd80E=
github.com/dgraph-io/badger/v4 v4.9.2/go.mod h1:nJjaJTUOSsQEBhsq209FmwCvMJzEA3e74RjZw6V2pQI=
github.com/dgraph-io/badger/v4 v4.9.4 h1:bcw+waCpzRZ2nmcSPbnPvDVhiEsn98TKmvnAhK7r7LM=
github.com/dgraph-io/badger/v4 v4.9.4/go.mod h1:nJjaJTUOSsQEBhsq209FmwCvMJzEA3e74RjZw6V2pQI=
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
@@ -306,12 +298,12 @@ github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/dnsimple/dnsimple-go v0.63.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -321,18 +313,12 @@ github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e/go.mod h1:Byz
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc h1:6IxmRbXV8WXVkcYcTzkU219A3UZeNMX/e6X2sve1wXA=
github.com/egirna/icap v0.0.0-20181108071049-d5ee18bd70bc/go.mod h1:FdVN2WHg7zOHhJ7kZQdDorfFhIfqZaHttjAzDDvAXHE=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA=
github.com/emersion/go-imap/v2 v2.0.0-beta.5/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E=
github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/emvi/iso-639-1 v1.1.1 h1:7jrl1Sqw9ZYWmCOaH+cpQotLbGr/khwlLPXlBvE8WXU=
@@ -346,12 +332,12 @@ github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2Vvl
github.com/exoscale/egoscale v0.46.0/go.mod h1:mpEXBpROAa/2i5GC0r33rfxG+TxSEka11g1PIXt9+zc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
@@ -362,8 +348,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.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
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=
@@ -382,11 +368,11 @@ github.com/go-acme/lego/v4 v4.4.0 h1:uHhU5LpOYQOdp3aDU+XY2bajseu8fuExphTL1Ss6/Fc
github.com/go-acme/lego/v4 v4.4.0/go.mod h1:l3+tFUFZb590dWcqhWZegynUthtaHJbG2fevUpoOOE0=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
@@ -397,13 +383,11 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
@@ -416,8 +400,8 @@ github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA=
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 h1:sfz1YppV05y4sYaW7kXZtrocU/+vimnIWt4cxAYh7+o=
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3/go.mod h1:ZXFhGda43Z2TVbfGZefXyMJzsDHhCh0go3bZUcwTx7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@@ -426,8 +410,8 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-micro/plugins/v4/client/grpc v1.2.1 h1:7xAwZRCO6mdUtBHsYIQs1/eCTdhCrnjF70GB+AVd6L0=
@@ -452,8 +436,9 @@ github.com/go-micro/plugins/v4/wrapper/monitoring/prometheus v1.2.0 h1:UWBUYtMXC
github.com/go-micro/plugins/v4/wrapper/monitoring/prometheus v1.2.0/go.mod h1:8BYxs/wEE4ZJayHZQffw4A8s9rcPumyoNms0hYoNocM=
github.com/go-micro/plugins/v4/wrapper/trace/opentelemetry v1.2.0 h1:e2hgtWMNqJ3DmbMt9ZxzmH/BkVAw9Xg23l6CHrXQfKw=
github.com/go-micro/plugins/v4/wrapper/trace/opentelemetry v1.2.0/go.mod h1:BBqL7ckGNb7rFfk3vU2Yj/CILVsz/WF19CkAyveQl8A=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -475,8 +460,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
@@ -503,8 +488,6 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@@ -565,7 +548,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -610,8 +592,6 @@ github.com/gophercloud/gophercloud v0.16.0/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae/go.mod h1:wx8HMD8oQD0Ryhz6+6ykq75PJ79iPyEqYHfwZ4l7OsA=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -621,16 +601,14 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
@@ -674,8 +652,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inbucket/html2text v0.9.0 h1:ULJmVcBEMAcmLE+/rN815KG1Fx6+a4HhbUxiDiN+qks=
github.com/inbucket/html2text v0.9.0/go.mod h1:QDaumzl+/OzlSVbNohhmg+yAy5pKjUjzCKW2BMvztKE=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@@ -702,8 +678,6 @@ github.com/jellydator/ttlcache/v2 v2.11.1/go.mod h1:RtE5Snf0/57e+2cLWFYWCCsLas2H
github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ=
github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jhillyerd/enmime/v2 v2.2.0 h1:Pe35MB96eZK5Q0XjlvPftOgWypQpd1gcbfJKAt7rsB8=
github.com/jhillyerd/enmime/v2 v2.2.0/go.mod h1:SOBXlCemjhiV2DvHhAKnJiWrtJGS/Ffuw4Iy7NjBTaI=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -739,8 +713,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -756,8 +730,8 @@ github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811ui
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
github.com/kovidgoyal/imaging v1.8.22 h1:CtpoRXQpS79xxJsKu8+LUJJE/0i4FLquJZy0QH+QNlM=
github.com/kovidgoyal/imaging v1.8.22/go.mod h1:y8wo4JTv4D+skbtQf6fHg8nA1qtagvCcn8J2Nu5k2Jg=
github.com/kovidgoyal/imaging v1.8.23 h1:4WsboyQ/E8tic2kX49P93Rvscg33SPsCwTBG5EZZF54=
github.com/kovidgoyal/imaging v1.8.23/go.mod h1:k3Iot3H0v2rSWXIxQISfT3e9wIryqzAHLHCuhG7z9lM=
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=
@@ -807,8 +781,8 @@ github.com/longsleep/go-metrics v1.0.0 h1:o2A6Dbu4MhLpZuL444WFoZzM7X7igewrj2Mouw
github.com/longsleep/go-metrics v1.0.0/go.mod h1:w6QO1LBkVla70FZrrF6XcB0YN+jTEYugjkn3+6RYTSM=
github.com/longsleep/rndm v1.2.0 h1:wPl+kIMyIUTUFW5+2b327DmM1Rlj+gmexsiyOTB7rzM=
github.com/longsleep/rndm v1.2.0/go.mod h1:5qyvM6CXNteKgz6djqqwZOP4+KcPsewrfKyLWd1dCFY=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
@@ -825,23 +799,23 @@ github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -851,8 +825,6 @@ github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 h1:Z/i1e+gTZrmcGeZy
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103/go.mod h1:o9YPB5aGP8ob35Vy6+vyq3P3bWe7NQWzf+JLiXCiMaE=
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
@@ -865,8 +837,8 @@ github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clg
github.com/minio/highwayhash v1.0.4/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.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8=
github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA=
github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
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=
@@ -887,14 +859,14 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
@@ -920,10 +892,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.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
github.com/nats-io/nats-server/v2 v2.14.3 h1:+xjydPt7rkit67G+04TN0mcO2n+8nveZE7tK/PPV53A=
github.com/nats-io/nats-server/v2 v2.14.3/go.mod h1:5IlCtBzfwyzQzPMjmoJ9W2/LKmnJRtNyuOs/OT+NHDY=
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc=
github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
github.com/nats-io/nats-server/v2 v2.14.5 h1:M6yeo/Xb7khi97RSEVELof3DForDqmYza3P4tHCPFWw=
github.com/nats-io/nats-server/v2 v2.14.5/go.mod h1:1D3iocrisKvWaD1B/imqarTqmaGrWMqALMLbEDo3v7Q=
github.com/nats-io/nats.go v1.53.1 h1:Otsq3uLc/kLdjmkNHkXH0jBqwUquwdKFoe3fq6/3/Xo=
github.com/nats-io/nats.go v1.53.1/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -955,31 +927,31 @@ 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.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E=
github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw=
github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
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=
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
github.com/open-policy-agent/opa v1.18.2 h1:VBiLJpioTuk7XTW1JoQi4ILo+FVxD2/8uD8iP9/OcxY=
github.com/open-policy-agent/opa v1.18.2/go.mod h1:9GY+hER4ZEXtxPlMjftVbqJJY9xLtCD3Q0oufRCfAKo=
github.com/open-policy-agent/opa v1.19.1 h1:aB1nOncChnTbQurjRQVJnjTJxditt8VqszlbaM3GGKU=
github.com/open-policy-agent/opa v1.19.1/go.mod h1:pb6Y6klyf7X7X8uXNDflruA9dQC2gMqWROXI5w/kvv0=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a h1:Sakl76blJAaM6NxylVkgSzktjo2dS504iDotEFJsh3M=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY=
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+lP5lUUIzjRGDg93WrQfZJZCaV1ZP3KeyXi8bzY=
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.20260310090739-853d972b282d h1:JcqGDiyrcaQwVyV861TUyQgO7uEmsjkhfm7aQd84dOw=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.46.4-0.20260625152426-8cff2a7032ec h1:kL9P87cVpe8GKvo5vzLpJstpiVXFvkZn2+Dco53nfuM=
github.com/opencloud-eu/reva/v2 v2.46.4-0.20260625152426-8cff2a7032ec/go.mod h1:l4CG5u8tdWZj6IyavXO9xDdnNTV85oaFudGwEDPvsnk=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902170011-45af3945a067 h1:UkNMKauyJAzY6RE6mmthz9bQZLYkbvBuApm7ZDCparE=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902170011-45af3945a067/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU=
github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774 h1:8N42Pwzyafdxq66zYP6kJ1CH/QBRpxovQXq/0Kt6bJY=
github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774/go.mod h1:Frg+UWnVcSy+412UB3l2LcD0KY8ZNu1samimwkNywbg=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
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=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opensearch-project/opensearch-go/v4 v4.6.0 h1:Ac8aLtDSmLEyOmv0r1qhQLw3b4vcUhE42NE9k+Z4cRc=
github.com/opensearch-project/opensearch-go/v4 v4.6.0/go.mod h1:3iZtb4SNt3IzaxavKq0dURh1AmtVgYW71E4XqmYnIiQ=
github.com/opensearch-project/opensearch-go/v4 v4.7.3 h1:JzETy7bYnnSDj4gueUh8t4EYBhs9rhKsgeVsoul77rA=
github.com/opensearch-project/opensearch-go/v4 v4.7.3/go.mod h1:+iikkyLrVC8ZvyfKv2sua1Ze1LbpWL7eZe4IkHqNGtg=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
@@ -999,15 +971,15 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2D
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pierrec/xxHash v0.1.5 h1:n/jBpwTHiER4xYvK3/CdPVnLDPchj8eTJFFLUb4QHBo=
github.com/pierrec/xxHash v0.1.5/go.mod h1:w2waW5Zoa/Wc4Yqe0wgrIYAGKqRMf7czn2HNKXmuL+I=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
@@ -1029,8 +1001,6 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/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/prometheus/alertmanager v0.31.1 h1:eAmIC42lzbWslHkMt693T36qdxfyZULswiHr681YS3Q=
github.com/prometheus/alertmanager v0.31.1/go.mod h1:zWPQwhbLt2ybee8rL921UONeQ59Oncash+m/hGP17tU=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
@@ -1042,8 +1012,8 @@ github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqr
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.0.0-20170216185247-6f3806018612/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -1063,8 +1033,8 @@ github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
github.com/prometheus/procfs v0.0.0-20170703101242-e645f4e5aaa8/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -1075,8 +1045,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=
github.com/prometheus/statsd_exporter v0.22.8 h1:Qo2D9ZzaQG+id9i5NYNGmbf1aa/KxKbB9aKfMS+Yib0=
github.com/prometheus/statsd_exporter v0.22.8/go.mod h1:/DzwbTEaFTE0Ojz5PqcSk6+PFHOPWGxdXVr6yC8eFOM=
@@ -1091,8 +1061,8 @@ github.com/riandyrn/otelchi v0.12.3 h1:KW9gA+97d6mExk8vbh0FRwb2biUvpyYlc8YuxP1Oa
github.com/riandyrn/otelchi v0.12.3/go.mod h1:weZZeUJURvtCcbWsdb7Y6F8KFZGedJlSrgUjq9VirV8=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
@@ -1120,35 +1090,31 @@ github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/kafka-go v0.4.50 h1:mcyC3tT5WeyWzrFbd6O374t+hmcu1NKt2Pu1L3QaXmc=
github.com/segmentio/kafka-go v0.4.50/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
github.com/segmentio/kafka-go v0.4.51 h1:JgDPPG75tC1rWIS2Me6MwcvXJ6f49UQ4HjAOef71Hno=
github.com/segmentio/kafka-go v0.4.51/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY=
github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sethvargo/go-diceware v0.5.0 h1:exrQ7GpaBo00GqRVM1N8ChXSsi3oS7tjQiIehsD+yR0=
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/sethvargo/go-diceware v0.6.0 h1:B3nhMhbBP7KwtTQ7hHRIOmv5FqeD8bJs77RFrV24iWk=
github.com/sethvargo/go-diceware v0.6.0/go.mod h1:lHmdB0xuWaJ06KCraW6bztRT+71Dp+lsXQvborhhsBc=
github.com/sethvargo/go-password v0.4.0 h1:eSidVKQw5C7CmTDAtH3RipBTSjdU1ZRxQaynD2GWLVU=
github.com/sethvargo/go-password v0.4.0/go.mod h1:PO3nYHwUpcHPR0F9woy7a4abZPvzRuqJr0GaeIYTm3k=
github.com/shamaton/msgpack/v2 v2.4.1 h1:JtJ141QoQ3NqgPDsjq2v9VXlaON8SiQOwEaoNLEK/MQ=
github.com/shamaton/msgpack/v2 v2.4.1/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.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M=
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 h1:OfRzdxCzDhp+rsKWXuOO2I/quKMJ/+TQwVbIP/gltZg=
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92/go.mod h1:7/OT02F6S6I7v6WXb+IjhMuZEYfH/RJ5RwEWnEo5BMg=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
@@ -1186,13 +1152,12 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -1205,8 +1170,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -1215,14 +1182,14 @@ github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhg
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A=
github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo=
github.com/testcontainers/testcontainers-go/modules/opensearch v0.43.0 h1:a1ipjF7d/VxPX1dgVPIk4F+t6YkgMbE2OtBuRQCHJt8=
github.com/testcontainers/testcontainers-go/modules/opensearch v0.43.0/go.mod h1:OWSeUDiGMUy30iMsAltIJIo9uh/CleLv6KyxjYOsgR8=
github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI=
github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE=
github.com/testcontainers/testcontainers-go/modules/opensearch v0.44.0 h1:iuBDo8U8Nwt0z+bxEmU+c7jS9hw81nPL9uGkkFOcXTo=
github.com/testcontainers/testcontainers-go/modules/opensearch v0.44.0/go.mod h1:8Qw29ffsCPzhomxqoeGhpj3J3Lbsrv0FsOUWeOjTl+I=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o=
github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=
github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=
github.com/thejerf/suture/v4 v4.0.6 h1:QsuCEsCqb03xF9tPAsWAj8QOAJBgQI1c0VqJNaingg8=
github.com/thejerf/suture/v4 v4.0.6/go.mod h1:gu9Y4dXNUWFrByqRt30Rm9/UZ0wzRSt9AJS6xu/ZGxU=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -1237,10 +1204,10 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
@@ -1259,16 +1226,16 @@ github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADT
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/vektah/gqlparser/v2 v2.5.34 h1:MEea5P0qhdcqfBL45ghKE+qr9laidVHTMHjav5h7ckk=
github.com/vektah/gqlparser/v2 v2.5.34/go.mod h1:mFdHLGCio7OGX1fby9ZjTW6FN+qxgmbnBcRIeeScE5s=
github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s=
github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo=
github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/vultr/govultr/v2 v2.0.0/go.mod h1:2PsEeg+gs3p/Fo5Pw8F9mv+DUBEOlrNZ8GmCTGmhOhs=
github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=
github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=
github.com/wI2L/jsondiff v0.7.1 h1:Fg9+yj+1/x3UtPBJhR91TKEzRkrEEWcAcLbg9dzEaNM=
github.com/wI2L/jsondiff v0.7.1/go.mod h1:yAt2W7U6Jd4HK0RA8DGSGk0zDtfEtOUUJVnH/xICpjo=
github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8=
github.com/wk8/go-ordered-map v1.0.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -1306,12 +1273,12 @@ github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
go.etcd.io/etcd/api/v3 v3.6.11 h1:XFGTgrJ8nak3kB4NgMG8t7NT+lEeuuvKQAqUHKVgkWQ=
go.etcd.io/etcd/api/v3 v3.6.11/go.mod h1:HYfTh0jyh+uFgp6gMbxJteIDYY97yMuYz85Rnw6Gy9o=
go.etcd.io/etcd/client/pkg/v3 v3.6.11 h1:e41mp315Yn3QMGPmEzCyLsMINgJXTY/dX8kM++1csxU=
go.etcd.io/etcd/client/pkg/v3 v3.6.11/go.mod h1:DysuMe/inqRyC/1tjRR6hReH/VV9Lufs27YKSKBWWJg=
go.etcd.io/etcd/client/v3 v3.6.11 h1:LAByD96VmmeuairkvdAcE0RZnrmGz/q3ceeWePo9bwc=
go.etcd.io/etcd/client/v3 v3.6.11/go.mod h1:vOTDMCo+fGPEClJqcFEFSqZ+8e7WKV7AyqJjX//HR2w=
go.etcd.io/etcd/api/v3 v3.6.13 h1:AvHPZv15LYEe7tZDyFglv7xnbiuF6GMZpZqKpIzXTt0=
go.etcd.io/etcd/api/v3 v3.6.13/go.mod h1:X9+3gaKwzjlOxzo6TZ2u3b7HcHBcAL+Ph7EBPjI/VWk=
go.etcd.io/etcd/client/pkg/v3 v3.6.13 h1:7QeMOisYByx8dBA7/CKcwCaPWfjb5C0xpmrIov/8WyY=
go.etcd.io/etcd/client/pkg/v3 v3.6.13/go.mod h1:Dn2zUBOCu/6xYcd6iAjB7LgoY16OTQjDZfWHLwvuQj4=
go.etcd.io/etcd/client/v3 v3.6.13 h1:0E+9ZYGpMsi9KlOJVoCdONh9PUDawKDTy5mSNY8wOEI=
go.etcd.io/etcd/client/v3 v3.6.13/go.mod h1:rtVI3vwobljb8xlTGcp1Yhz7hBIuBWULXwB848kqJGw=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -1324,30 +1291,30 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/contrib/zpages v0.69.0 h1:YQC1PumJq6lUGQNrLW14ID9a0dXSBcbC/aC6VRSIARU=
go.opentelemetry.io/contrib/zpages v0.69.0/go.mod h1:FGvUcMGN5atRzUIgUsqOi+MiMvlQsfbuYATBHPP4cGs=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D/gAmznQTd4dxxMA93aKbcpKqiTCs=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 h1:oECp5f+hN7nkwjU/8BxQ/q23bGPb8FIrD839owX222E=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0/go.mod h1:DqEFwLumhzMBDQv9PcWbyoDxHI/4lAk6CM4nJBH39sc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
go.opentelemetry.io/contrib/zpages v0.71.0 h1:OrQSEmdlfknIusEyGj/t30uBR7MnbmGcCRBt5ov8/rA=
go.opentelemetry.io/contrib/zpages v0.71.0/go.mod h1:C3IIpqkQbjswcSREqxQLyEapF0SI9bqwJpxpjwtzFrE=
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.45.0 h1:lsA/S1bxgdbyFGkTj+3meEdJ6ADVU7QoFstV6MXgE68=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.45.0/go.mod h1:L7u+MirGoB1bjeLH66+xDykF4RC8C3RN7lIFpBiewUo=
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
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=
@@ -1365,8 +1332,9 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1386,8 +1354,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1402,8 +1370,8 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJk
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
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.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
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=
@@ -1425,8 +1393,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1477,8 +1445,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1502,8 +1470,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1577,6 +1545,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1584,8 +1553,8 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1594,8 +1563,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1608,8 +1577,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1670,10 +1639,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1735,10 +1702,10 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -1754,8 +1721,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e h1:m7aQHHqd0q89mRwhwS9Bx2rjyl/hsFAeta+uGrHsQaU=
google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e/go.mod h1:gID3PKrg7pWKntu9Ss6zTLJ0ttC0X9IHgREOCZwbCVU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@@ -1772,8 +1739,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
@@ -1790,6 +1757,8 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw=
gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/ns1/ns1-go.v2 v2.4.4/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
@@ -0,0 +1,44 @@
package eventstest
import (
"encoding/json"
"reflect"
"github.com/google/uuid"
rev "github.com/opencloud-eu/reva/v2/pkg/events"
microevents "go-micro.dev/v4/events"
)
func NewTestBus() TestBus {
return TestBus(make(chan rev.Event))
}
type TestBus chan rev.Event
func (tb TestBus) Consume(_ string, _ ...microevents.ConsumeOption) (<-chan microevents.Event, error) {
ch := make(chan microevents.Event)
go func() {
for ev := range tb {
b, _ := json.Marshal(ev.Event)
ch <- microevents.Event{
Payload: b,
Metadata: map[string]string{
rev.MetadatakeyEventID: ev.ID,
rev.MetadatakeyEventType: ev.Type,
},
}
}
}()
return ch, nil
}
func (tb TestBus) Publish(e any) string {
ev := rev.Event{
ID: uuid.New().String(),
Type: reflect.TypeOf(e).String(),
Event: e,
}
tb <- ev
return ev.ID
}
@@ -0,0 +1,143 @@
package metricstest
import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// copied and adapted from Prometheus testutil.ToFloat64(), since we don't import that package
func collect(c prometheus.Collector) []prometheus.Metric {
result := []prometheus.Metric{}
ch := make(chan prometheus.Metric)
done := make(chan struct{})
go func() {
for m := range ch {
result = append(result, m)
}
close(done)
}()
c.Collect(ch)
close(ch)
<-done
return result
}
func RequireIsNotSet(t require.TestingT, c prometheus.Collector, msgAndArgs ...any) {
if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
if !IsNotSet(t, c, msgAndArgs) {
t.FailNow()
}
}
func IsNotSet(t assert.TestingT, c prometheus.Collector, msgAndArgs ...any) bool {
if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
m := collect(c)
if len(m) > 0 {
return assert.Fail(t, "Metric exists while expected to not exist", msgAndArgs)
} else {
return true
}
}
func RequireEqual(t require.TestingT, expected float64, c prometheus.Collector, msgAndArgs ...any) {
if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
if !Equal(t, expected, c, msgAndArgs) {
t.FailNow()
}
}
// copied and adapted from Prometheus testutil.ToFloat64(), since we don't import that package
func Equal(t assert.TestingT, expected float64, c prometheus.Collector, msgAndArgs ...any) bool {
if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
m := collect(c)
if !assert.Len(t, m, 1, msgAndArgs...) {
return false
}
pb := &dto.Metric{}
err := m[0].Write(pb)
if !assert.NoError(t, err, msgAndArgs...) {
return false
}
if pb.Gauge != nil {
return assert.Equal(t, expected, pb.Gauge.GetValue(), msgAndArgs...)
} else if pb.Counter != nil {
return assert.Equal(t, expected, pb.Counter.GetValue(), msgAndArgs...)
} else if pb.Untyped != nil {
return assert.Equal(t, expected, pb.Untyped.GetValue(), msgAndArgs...)
} else {
return assert.Fail(t, fmt.Sprintf("collected a non-gauge/counter/untyped metric: %s", pb), msgAndArgs...)
}
}
func RequireEqualWithLabels(t require.TestingT, expectedValue float64, expectedLabels map[string]string, c prometheus.Collector, msgAndArgs ...any) {
if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
if !EqualWithLabels(t, expectedValue, expectedLabels, c, msgAndArgs) {
t.FailNow()
}
}
func EqualWithLabels(t assert.TestingT, expectedValue float64, expectedLabels map[string]string, c prometheus.Collector, msgAndArgs ...any) bool {
if h, ok := t.(interface{ Helper() }); ok {
h.Helper()
}
m := collect(c)
if !assert.Len(t, m, 1, "collected %d metrics instead of exactly 1", len(m)) {
return false
}
pb := &dto.Metric{}
err := m[0].Write(pb)
if !assert.NoError(t, err) {
return false
}
if pb.Gauge != nil {
if !assert.Equal(t, expectedValue, pb.Gauge.GetValue()) {
return false
}
} else if pb.Counter != nil {
if !assert.Equal(t, expectedValue, pb.Counter.GetValue()) {
return false
}
} else if pb.Untyped != nil {
if !assert.Equal(t, expectedValue, pb.Untyped.GetValue()) {
return false
}
} else {
return assert.Fail(t, "collected a non-gauge/counter/untyped metric: %s", pb)
}
if !assert.NotNil(t, pb.Label) {
return false
}
actualLabels := map[string]string{}
for _, label := range pb.Label {
if !assert.NotNil(t, label) {
return false
}
if !assert.NotNil(t, label.Name) {
return false
}
if !assert.NotNil(t, label.Value) {
return false
}
actualLabels[*label.Name] = *label.Value
}
return assert.Equal(t, expectedLabels, actualLabels, msgAndArgs)
}
+138
View File
@@ -0,0 +1,138 @@
[env]
GO_VERSION = "{{ exec(command='./.mise-go-version.sh') }}"
[tools]
go = "{{ env.GO_VERSION }}"
node = "24"
pnpm = "11.1.3"
"go:github.com/go-delve/delve/cmd/dlv" = "1.27.1"
"aqua:nats-io/natscli" = "0.4.0"
k6 = "2.2.0"
ginkgo = "latest"
[tasks.build]
description = "build"
run = "make -C opencloud build"
[tasks."build:debug"]
description = "build with debug symbols"
run = "make -C opencloud build-debug"
[tasks."docker:build"]
description = "docker image opencloudeu/opencloud:dev"
run = "make -C opencloud dev-docker"
[tasks."docker:build:multiarch"]
description = "docker image for amd64 + arm64"
depends = ["gen"]
run = "make -C opencloud dev-docker-multiarch"
[tasks."docker:build:debug"]
description = "docker image with delve"
run = "make -C opencloud debug-docker"
[tasks.serve]
description = "start the server"
depends = ["build"]
run = "opencloud/bin/opencloud server"
[tasks."serve:init"]
description = "create the local config"
depends = ["build"]
run = "opencloud/bin/opencloud init"
[tasks."serve:debug"]
description = "start the server under delve"
depends = ["build:debug"]
run = "dlv exec opencloud/bin/opencloud-debug -- server"
[tasks.test]
description = "go test"
run = "go test -tags disable_crypt ./..."
[tasks."test:changed"]
description = "go test, changed packages"
run = """
base=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo origin/main)
files=$(git diff --name-only --diff-filter=ACM $(git merge-base "$base" HEAD) -- '*.go' | grep -v vendor/ || true)
[ -n "$files" ] || exit 0
go test -tags disable_crypt $(echo "$files" | xargs -n1 dirname | sort -u | sed 's|^|./|')
"""
[tasks."test:race"]
description = "go test + race"
run = "go test -race -tags disable_crypt ./..."
[tasks."test:coverage"]
description = "go test + coverage"
run = ["make test", "make go-coverage"]
[tasks.check]
description = "all checks"
depends = ["check:fmt", "check:lint", "check:vendor", "check:env-vars", "test"]
[tasks."check:fmt"]
description = "gofmt"
run = """
base=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo origin/main)
files=$(git diff --name-only --diff-filter=ACM $(git merge-base "$base" HEAD) -- '*.go' | grep -v vendor/ || true)
[ -n "$files" ] || exit 0
out=$(gofmt -s -l $files)
[ -z "$out" ] || { echo "$out"; exit 1; }
"""
[tasks."check:lint"]
description = "golangci-lint"
run = """
set -e
base=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo origin/main)
gobin=$(go env GOBIN)
[ -n "$gobin" ] || gobin="$(go env GOPATH)/bin"
bin="$gobin/golangci-lint-$(awk '/golangci-lint v/ {print $3}' .bingo/golangci-lint.mod)"
[ -x "$bin" ] || make -s golangci-lint >/dev/null
exec "$bin" run --modules-download-mode vendor --timeout 15m0s --new-from-merge-base "$base"
"""
[tasks."check:vendor"]
description = "vendor matches go.mod"
run = "GOWORK=off go list -mod=vendor ./... >/dev/null"
[tasks."check:env-vars"]
description = "env var annotations"
run = "make check-env-var-annotations"
[tasks.fix]
description = "gofmt + lint fixes"
run = [{ task = "fix:fmt" }, { task = "fix:lint" }]
[tasks."fix:fmt"]
description = "gofmt -w"
run = """
base=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo origin/main)
files=$(git diff --name-only --diff-filter=ACM $(git merge-base "$base" HEAD) -- '*.go' | grep -v vendor/ || true)
[ -z "$files" ] || gofmt -s -w $files
"""
[tasks."fix:lint"]
description = "golangci-lint --fix"
run = "make golangci-lint-fix"
[tasks."pre:commit"]
description = "before commit"
run = [{ task = "fix:fmt" }, { task = "check:lint" }, { task = "test:changed" }]
[tasks."pre:push"]
description = "before push"
run = [{ task = "tidy" }, { task = "fix:fmt" }, { task = "check" }]
[tasks.gen]
description = "assets & mocks"
run = "make generate"
[tasks."gen:protobuf"]
description = "protobuf"
run = "make protobuf"
[tasks.tidy]
description = "tidy + vendor"
run = ["GOWORK=off go mod tidy", "GOWORK=off go mod vendor"]
-1
View File
@@ -1 +0,0 @@
/__debug_bin*
+4 -1
View File
@@ -11,7 +11,10 @@ RUN --mount=type=bind,target=/build,rw \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache \
GOOS="${TARGETOS:-linux}" GOARCH="${TARGETARCH:-amd64}" ; \
make -C ${SRCDIR:-.}/opencloud release-linux-docker-${TARGETARCH} ENABLE_VIPS=true DIST=/dist
make -C ${SRCDIR:-.}/opencloud release-linux-docker-${TARGETARCH} \
ENABLE_VIPS=true DIST=/dist \
VERSION="${VERSION}" EDITION="${EDITION}" \
${STRING:+STRING="${STRING}"}
FROM alpine:3.24
ARG VERSION
+273 -446
View File
@@ -1,17 +1,16 @@
package command
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/opencloud-eu/opencloud/opencloud/pkg/register"
"github.com/opencloud-eu/opencloud/pkg/config"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
"github.com/opencloud-eu/opencloud/pkg/config/parser"
"github.com/opencloud-eu/opencloud/pkg/x/path/filepathx"
storageUsersParser "github.com/opencloud-eu/opencloud/services/storage-users/pkg/config/parser"
"github.com/opencloud-eu/opencloud/services/storage-users/pkg/event"
"github.com/opencloud-eu/opencloud/services/storage-users/pkg/revaconfig"
@@ -19,27 +18,10 @@ import (
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/ignore"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes"
"github.com/pkg/xattr"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"github.com/theckman/yacspin"
"github.com/vmihailenco/msgpack/v5"
)
// Define the names of the extended attributes we are working with.
const (
parentIDAttrName = "user.oc.parentid"
idAttrName = "user.oc.id"
nameAttrName = "user.oc.name"
spaceIDAttrName = "user.oc.space.id"
ownerIDAttrName = "user.oc.owner.id"
)
var (
spinner *yacspin.Spinner
restartRequired = false
ignorer *ignore.Ignorer
)
type IDCacher interface {
@@ -73,11 +55,23 @@ func init() {
// scanCmd performs a posixfs id cache warmup scan
func scanCmd(ocCfg *config.Config) *cobra.Command {
cmd := &cobra.Command{
Use: "scan",
scanCmd := &cobra.Command{
Use: "scan [path ...]",
Short: "Perform a filesystem scan and update the ID and filemetadata cache",
PreRunE: func(cmd *cobra.Command, args []string) error {
Long: `Perform a filesystem scan and update the ID and filemetadata cache.
You can specify one or more paths to limit the scope of the scan.
If no path is provided, the whole storage is checked, starting at the storage root directory.
The provided arguments determines the scope of the check:
- a storage root: the whole storage (all personal and project spaces) is scanned
- a space root: only that space is scanned
- a file or directory: only that single resource is scanned (and its children, if it is a directory)
Any specified file or directory must be underneath the storage root directory and if that is not the case,
the command is aborted with an error before performing any scanning.`,
Args: cobra.ArbitraryArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
if err := parser.ParseConfig(ocCfg, true); err != nil {
return configlog.ReturnError(err)
}
@@ -94,448 +88,281 @@ func scanCmd(ocCfg *config.Config) *cobra.Command {
os.Exit(1)
}
// We want to initialize the driver but disable scanfs on boot, so we can trigger it manually afterwards
drivers := revaconfig.StorageProviderDrivers(cfg)
drivers["posix"] = revaconfig.Posix(cfg, false, false)
var fsStream events.Stream
var err error
fsStream, err = event.NewStream(cfg)
haltOnError, err := cmd.Flags().GetBool("halt-on-error")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create event stream for posix driver: %v\n", err)
os.Exit(1)
}
f, ok := registry.NewFuncs["posix"]
if !ok {
fmt.Fprintf(os.Stderr, "posix driver not found in registry\n")
os.Exit(1)
}
fs, err := f(drivers["posix"].(map[string]any), fsStream, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to initialize filesystem driver '%s': %v\n", cfg.Driver, err)
return err
}
cacher, ok := fs.(IDCacher)
if !ok {
fmt.Fprintf(os.Stderr, "The posix driver does not expose WarmupIDCache.\n")
os.Exit(1)
storageRoot := cfg.Drivers.Posix.Root
paths := []string{storageRoot}
if len(args) > 0 {
paths = []string{}
for _, v := range args {
path := v
if !filepath.IsAbs(path) {
if v, err := filepath.Abs(path); err != nil {
fmt.Fprintf(os.Stderr, "Failed to make the specified path %q absolute: %v\n", v, err)
os.Exit(1)
} else {
path = v
}
}
// not ensuring whether the path is under the storage root here, will be done when iterating over them
path = filepath.Clean(path)
paths = append(paths, path)
}
}
fmt.Println("Starting posixfs scan...")
err = cacher.WarmupIDCache(cfg.Drivers.Posix.Root, true, false)
if err != nil {
fmt.Fprintf(os.Stderr, "Scan failed: %v\n", err)
return err
var scan func(path string) error = nil
{
// We want to initialize the driver but disable scanfs on boot, so we can trigger it manually afterwards
drivers := revaconfig.StorageProviderDrivers(cfg)
drivers["posix"] = revaconfig.Posix(cfg, false, false)
var fsStream events.Stream
var err error
fsStream, err = event.NewStream(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create event stream for posix driver: %v\n", err)
os.Exit(1)
}
log := logger("posixfs")
f, ok := registry.NewFuncs["posix"]
if !ok {
fmt.Fprintf(os.Stderr, "posix driver not found in registry\n")
os.Exit(1)
}
fs, err := f(drivers["posix"].(map[string]any), fsStream, &log)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to initialize filesystem driver '%s': %v\n", cfg.Driver, err)
return err
}
cacher, ok := fs.(IDCacher)
if !ok {
fmt.Fprintf(os.Stderr, "The posix driver does not expose WarmupIDCache.\n")
os.Exit(1)
}
scan = func(path string) error {
err := cacher.WarmupIDCache(path, true, false)
if err != nil {
logFailure("Error scanning path '%s': %v", path, err)
}
return err
}
}
fmt.Println("Scan completed successfully.")
return nil
errors := processPosixFsResources(paths, !haltOnError,
func(path string) error {
fmt.Println("Scanning personal spaces...")
return scan(path)
},
func(path string) error {
fmt.Println("Scanning project spaces...")
return scan(path)
},
func(path string) error {
fmt.Printf("Scanning space '%s'...\n", path)
return scan(path)
},
func(path string) error {
fmt.Printf("Scanning '%s'...\n", path)
return scan(path)
},
)
if len(errors) == 0 {
fmt.Println("Scan completed successfully.")
return nil
} else {
plural := "s"
if len(errors) == 1 {
plural = ""
}
verb := "completed"
if haltOnError {
verb = "aborted"
}
return fmt.Errorf("scan %s with %d error%s", verb, len(errors), plural)
}
},
}
return cmd
scanCmd.Flags().BoolP("halt-on-error", "E", false, "Halt at once when an error occurs when processing one of the paths (default behaviour is to keep going and attempt to process all paths).")
return scanCmd
}
// consistencyCmd returns a command to check the consistency of the posixfs storage.
func consistencyCmd(cfg *config.Config) *cobra.Command {
func consistencyCmd(ocCfg *config.Config) *cobra.Command {
consCmd := &cobra.Command{
Use: "consistency",
Short: "check the consistency of the posixfs storage",
Use: "consistency [path ...]",
Short: "Check the consistency of the posixfs storage",
Long: `Check the consistency of the posixfs storage.
You can specify one or more paths to limit the scope of the check.
If no path is provided, the whole storage is checked.
The provided arguments determines the scope of the check:
- a storage root: the whole storage (all personal and project spaces) is checked
- a space root: only that space is checked
- a file or directory: only that single entity is checked (and its children, if it is a directory)`,
Args: cobra.ArbitraryArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
if err := parser.ParseConfig(ocCfg, true); err != nil {
return configlog.ReturnError(err)
}
// Parse storage users config
ocCfg.StorageUsers.Commons = ocCfg.Commons
return configlog.ReturnFatal(storageUsersParser.ParseConfig(ocCfg.StorageUsers))
},
RunE: func(cmd *cobra.Command, args []string) error {
return checkPosixfsConsistency(cmd, cfg)
cfg := ocCfg.StorageUsers
if len(args) == 0 {
args = []string{cfg.Drivers.Posix.Root}
}
log := logger("posixfs")
recalculateChecksums, err := cmd.Flags().GetBool("fix-checksums")
if err != nil {
return err
}
drivers := revaconfig.StorageProviderDrivers(cfg)
drivers["posix"] = revaconfig.Posix(cfg, false, false)
opts, err := options.New(drivers["posix"].(map[string]any))
if err != nil {
return err
}
ignorer := ignore.NewIgnorer(opts, &log)
checker := &consistencyChecker{
cfg: cfg,
ignorer: ignorer,
recalculateChecksums: recalculateChecksums,
}
return checker.Check(args)
},
}
consCmd.Flags().StringP("root", "r", "", "Path to the root directory of the posixfs storage")
_ = consCmd.MarkFlagRequired("root")
consCmd.Flags().Bool("fix-checksums", false, "Recalculate and fix the file checksums. This reads every file and can be slow on large storages.")
return consCmd
}
// checkPosixfsConsistency checks the consistency of the posixfs storage.
func checkPosixfsConsistency(cmd *cobra.Command, cfg *config.Config) error {
rootPath, _ := cmd.Flags().GetString("root")
indexesPath := filepath.Join(rootPath, "indexes")
opt, _ := options.New(map[string]interface{}{
"root": rootPath,
})
log := zerolog.Nop()
ignorer = ignore.NewIgnorer(opt, &log)
_, err := os.Stat(indexesPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("consistency check failed: '%s' is not a posixfs root", rootPath)
}
return fmt.Errorf("error accessing '%s': %w", indexesPath, err)
}
spinnerCfg := yacspin.Config{
Frequency: 100 * time.Millisecond,
CharSet: yacspin.CharSets[11],
StopCharacter: "✓",
StopColors: []string{"fgGreen"},
StopFailCharacter: "✗",
StopFailColors: []string{"fgRed"},
}
spinner, err = yacspin.New(spinnerCfg)
err = spinner.Start()
if err != nil {
return fmt.Errorf("error creating spinner: %w", err)
}
checkSpaces(filepath.Join(rootPath, "users"))
spinner.Suffix(" Personal spaces check ")
spinner.StopMessage("completed\n")
spinner.Stop()
checkSpaces(filepath.Join(rootPath, "projects"))
spinner.Suffix(" Project spaces check ")
spinner.StopMessage("completed")
spinner.Stop()
if restartRequired {
fmt.Println("\n\n ⚠️ Please restart your openCloud instance to apply changes.")
}
return nil
}
func checkSpaces(basePath string) {
dirEntries, err := os.ReadDir(basePath)
if err != nil {
spinner.Message(fmt.Sprintf("Error reading spaces directory '%s'\n", basePath))
spinner.StopFail()
return
}
for _, entry := range dirEntries {
if entry.IsDir() {
fullPath := filepath.Join(basePath, entry.Name())
checkSpace(fullPath)
}
}
}
func checkSpace(spacePath string) {
spinner.Message("")
spinner.Suffix(fmt.Sprintf(" Checking space '%s'", spacePath))
info, err := os.Stat(spacePath)
if err != nil {
logFailure("Error accessing path '%s': %v", spacePath, err)
return
}
if !info.IsDir() {
logFailure("Error: The provided path '%s' is not a directory\n", spacePath)
return
}
spaceID, err := xattr.Get(spacePath, spaceIDAttrName)
if err != nil || len(spaceID) == 0 {
logFailure("Error: The directory '%s' does not seem to be a space root, it's missing the '%s' attribute\n", spacePath, spaceIDAttrName)
return
}
checkSpaceID(spacePath)
checkNodeIDs(spacePath)
}
func checkSpaceID(spacePath string) {
spinner.Message(" - checking space ID uniqueness")
entries, uniqueIDs, oldestEntry, err := gatherAttributes(spacePath)
if err != nil {
logFailure("Failed to gather attributes: %v", err)
return
}
if len(entries) == 0 {
return
}
if len(uniqueIDs) > 1 {
spinner.Pause()
fmt.Println("\n ⚠ Multiple space IDs found:")
for id := range uniqueIDs {
fmt.Printf(" - %s\n", id)
}
fmt.Printf("\n ⏳ Oldest entry is '%s' (modified on %s).\n",
filepath.Base(oldestEntry.Path), oldestEntry.ModTime.Format(time.RFC1123))
targetID := oldestEntry.ParentID
fmt.Printf(" ✅ Proposed target Parent ID: %s\n", targetID)
fmt.Printf("\n Do you want to unify all parent IDs to '%s'? This will modify %d entries, the directory, and the user index. (y/N): ", targetID, len(entries))
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(strings.ToLower(input))
if input != "y" {
spinner.Unpause()
logFailure("Operation cancelled by user.")
return
}
restartRequired = true
obsoleteIDs := []string{}
for id := range uniqueIDs {
if id != targetID {
obsoleteIDs = append(obsoleteIDs, id)
}
}
fixSpaceID(spacePath, obsoleteIDs, targetID, entries)
spinner.Unpause()
}
}
func walkNodes(dir string, parentID string) int {
fixes := 0
entries, err := os.ReadDir(dir)
if err != nil {
logFailure("Error reading directory '%s': %v", dir, err)
return 0
}
for _, entry := range entries {
fullPath := filepath.Join(dir, entry.Name())
if ignorer.IsIgnored(fullPath) {
continue
}
// Check if the parent ID attribute matches the expected parent ID, if not, fix it.
actualParentID, err := xattr.Get(fullPath, parentIDAttrName)
if err != nil || string(actualParentID) != parentID {
err = xattr.Set(fullPath, parentIDAttrName, []byte(parentID))
if err != nil {
logFailure("Failed to fix parent ID for '%s': %v", fullPath, err)
} else {
spinner.Pause()
fmt.Printf(" + Fixed parent ID for '%s'", fullPath)
spinner.Unpause()
fixes++
restartRequired = true
}
}
// Check that the name attribute matches the actual name of the file/directory, if not, fix it.
nameAttr, err := xattr.Get(fullPath, nameAttrName)
if err != nil || string(nameAttr) != entry.Name() {
err = xattr.Set(fullPath, nameAttrName, []byte(entry.Name()))
if err != nil {
logFailure("Failed to fix name attribute for '%s': %v", fullPath, err)
} else {
spinner.Pause()
fmt.Printf(" + Fixed name attribute for '%s'", fullPath)
spinner.Unpause()
fixes++
restartRequired = true
}
}
if entry.IsDir() {
nodeID, err := xattr.Get(fullPath, idAttrName)
if err != nil || len(nodeID) == 0 {
logFailure("Directory '%s' missing '%s', skipping its children", fullPath, idAttrName)
continue
}
walkNodes(fullPath, string(nodeID))
}
}
return fixes
}
func checkNodeIDs(spacePath string) {
spinner.Message(" - checking nodes")
rootID, err := xattr.Get(spacePath, idAttrName)
if err != nil || len(rootID) == 0 {
logFailure("Space root '%s' missing '%s' attribute", spacePath, idAttrName)
return
}
fixes := walkNodes(spacePath, string(rootID))
if fixes > 0 {
spinner.Pause()
fmt.Printf("\n ✓ Fixed %d incorrect node attributes in %s\n", fixes, filepath.Base(spacePath))
spinner.Unpause()
}
}
func fixSpaceID(spacePath string, obsoleteIDs []string, targetID string, entries []EntryInfo) {
// Set all parentid attributes to the proper space ID
err := setAllParentIDAttributes(entries, targetID)
if err != nil {
logFailure("an error occurred during file attribute update: %v", err)
return
}
// Update space ID itself
fmt.Printf(" Updating directory '%s' with attribute '%s' -> %s\n", filepath.Base(spacePath), idAttrName, targetID)
err = xattr.Set(spacePath, idAttrName, []byte(targetID))
if err != nil {
logFailure("Failed to set attribute on directory '%s': %v", spacePath, err)
return
}
err = xattr.Set(spacePath, spaceIDAttrName, []byte(targetID))
if err != nil {
logFailure("Failed to set attribute on directory '%s': %v", spacePath, err)
return
}
// update the index
err = updateOwnerIndexFile(spacePath, obsoleteIDs)
if err != nil {
logFailure("Could not update the owner index file: %v", err)
}
}
func gatherAttributes(path string) ([]EntryInfo, map[string]struct{}, EntryInfo, error) {
dirEntries, err := os.ReadDir(path)
if err != nil {
return nil, nil, EntryInfo{}, fmt.Errorf("failed to read directory: %w", err)
}
var allEntries []EntryInfo
uniqueIDs := make(map[string]struct{})
var oldestEntry EntryInfo
oldestTime := time.Now().Add(100 * 365 * 24 * time.Hour) // Set to a future date to find the oldest entry
for _, entry := range dirEntries {
fullPath := filepath.Join(path, entry.Name())
if ignorer.IsIgnored(fullPath) {
continue
}
info, err := os.Stat(fullPath)
if err != nil {
fmt.Printf(" - Warning: could not stat %s: %v\n", entry.Name(), err)
continue
}
parentID, err := xattr.Get(fullPath, parentIDAttrName)
if err != nil {
continue // Skip if attribute doesn't exist or can't be read
}
entryInfo := EntryInfo{
Path: fullPath,
ModTime: info.ModTime(),
ParentID: string(parentID),
}
allEntries = append(allEntries, entryInfo)
uniqueIDs[string(parentID)] = struct{}{}
if entryInfo.ModTime.Before(oldestTime) {
oldestTime = entryInfo.ModTime
oldestEntry = entryInfo
}
}
return allEntries, uniqueIDs, oldestEntry, nil
}
func setAllParentIDAttributes(entries []EntryInfo, targetID string) error {
fmt.Printf(" Setting all parent IDs to '%s':\n", targetID)
for _, entry := range entries {
if entry.ParentID == targetID {
fmt.Printf(" - Skipping '%s' (already has target ID).\n", filepath.Base(entry.Path))
continue
}
fmt.Printf(" - Removing all attributes from '%s'. It will be re-assimilated\n", filepath.Base(entry.Path))
filepath.WalkDir(entry.Path, func(path string, d os.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("error walking path '%s': %w", path, err)
}
// Remove all attributes from the file.
if err := removeAttributes(path); err != nil {
fmt.Printf("failed to remove attributes from '%s': %v", path, err)
}
return nil
})
}
return nil
}
// updateOwnerIndexFile handles the logic of reading, modifying, and writing the MessagePack index file.
func updateOwnerIndexFile(basePath string, obsoleteIDs []string) error {
fmt.Printf(" Rewriting index file '%s'\n", basePath)
ownerID, err := xattr.Get(basePath, ownerIDAttrName)
if err != nil {
return fmt.Errorf("could not get owner ID from oldest entry '%s' to find index: %w", basePath, err)
}
indexPath := filepath.Join(basePath, "../../indexes/by-user-id", string(ownerID)+".mpk")
indexPath = filepath.Clean(indexPath)
// Read the MessagePack file
fileData, err := os.ReadFile(indexPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("index file does not exist, skipping update")
}
return fmt.Errorf("could not read index file: %w", err)
}
var indexMap map[string]string
if err := msgpack.Unmarshal(fileData, &indexMap); err != nil {
return fmt.Errorf("failed to parse MessagePack index file (is it corrupt?): %w", err)
}
// Remove obsolete IDs from the map
itemsRemoved := 0
for _, id := range obsoleteIDs {
if _, exists := indexMap[id]; exists {
fmt.Printf(" - Removing obsolete ID '%s' from index.\n", id)
delete(indexMap, id)
itemsRemoved++
} else {
fmt.Printf(" - Obsolete ID '%s' not found in index\n", id)
}
}
if itemsRemoved == 0 {
return nil
}
// Write the data back to the file
updatedData, err := msgpack.Marshal(&indexMap)
if err != nil {
return fmt.Errorf("failed to marshal updated index map: %w", err)
}
if err := os.WriteFile(indexPath, updatedData, 0644); err != nil {
return fmt.Errorf("failed to write updated index file: %w", err)
}
fmt.Printf(" ✓ Successfully removed %d item(s) and saved index file.\n", itemsRemoved)
return nil
}
func removeAttributes(path string) error {
attrNames, err := xattr.List(path)
if err != nil {
return fmt.Errorf("failed to list attributes for '%s': %w", path, err)
}
for _, attrName := range attrNames {
if err := xattr.Remove(path, attrName); err != nil {
return fmt.Errorf("failed to remove attribute '%s' from '%s': %w", attrName, path, err)
}
}
return nil
}
func logFailure(message string, args ...any) {
spinner.StopFailMessage(fmt.Sprintf("\n"+message, args...))
spinner.StopFail()
spinner.Start()
fmt.Fprintf(os.Stderr, message+"\n", args...)
}
// findStorageRoot walks up the directory tree starting at path until it finds a
// directory that contains an "indexes" subdirectory which marks the root of a
// posixfs storage. A user directory inside a space might also be named "indexes",
// so to disambiguate we require that the "indexes" directory is an internal
// directory: the storage's own indexes directory is skipped during assimilation
// and therefore never receives a node ID attribute, whereas a regular user
// directory would have one.
func findStorageRoot(path string) (string, error) {
current := path
for {
indexesPath := filepath.Join(current, "indexes")
if info, err := os.Stat(indexesPath); err == nil && info.IsDir() {
if id, err := xattr.Get(indexesPath, prefixes.IDAttr); err != nil || len(id) == 0 {
return current, nil
}
}
parent := filepath.Dir(current)
if parent == current {
return "", fmt.Errorf("'%s' does not appear to be inside a posixfs storage (no 'indexes' directory found)", path)
}
current = parent
}
}
// isSpaceRoot reports whether the given path is a space root, which is
// identified by the presence of the space ID attribute.
func isSpaceRoot(path string) bool {
spaceID, err := xattr.Get(path, prefixes.SpaceIDAttr)
return err == nil && len(spaceID) > 0
}
// iterates over a list of paths and processes them all, using the appropriate function
// depending on the type of resource
//
// note that whenever an error occurs, it collects that error and continues processing
// subsequent paths, and then returns a slice of errors at the end (or an empty slice
// if no errors occured)
func processPosixFsResources(paths []string,
keepGoing bool,
personalSpaceDir func(string) error,
projectSpaceDir func(string) error,
spaceRoot func(string) error,
entity func(string) error,
) []error {
// no need to guard this with a mutex for now, since the implementation is not parallelized
errors := []error{}
for _, path := range paths {
rootPath, err := findStorageRoot(path)
if err != nil {
errors = append(errors, err)
logFailure("error: %s", err)
if keepGoing {
continue
} else {
return errors
}
}
path = filepath.Clean(path)
if _, err := os.Stat(path); err != nil {
errors = append(errors, err)
logFailure("error accessing '%s': %w", path, err)
if keepGoing {
continue
} else {
return errors
}
}
contained, _ := filepathx.IsSameOrContainedBy(rootPath, path)
switch {
case path == rootPath:
if err := personalSpaceDir(filepath.Join(path, "users")); err != nil {
errors = append(errors, err)
if !keepGoing {
return errors
}
}
if err := projectSpaceDir(filepath.Join(path, "projects")); err != nil {
errors = append(errors, err)
if !keepGoing {
return errors
}
}
case isSpaceRoot(path):
if err := spaceRoot(path); err != nil {
errors = append(errors, err)
if !keepGoing {
return errors
}
}
case contained:
if err := entity(path); err != nil {
errors = append(errors, err)
if !keepGoing {
return errors
}
}
default:
err := fmt.Errorf("error: the provided path '%s' is neither a space root nor contained by the storage root '%s'", path, rootPath)
errors = append(errors, err)
logFailure(err.Error())
if !keepGoing {
return errors
}
}
}
return errors
}
@@ -0,0 +1,489 @@
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
// SPDX-License-Identifier: Apache-2.0
package command
import (
"bufio"
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
storageUsersConfig "github.com/opencloud-eu/opencloud/services/storage-users/pkg/config"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/ignore"
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes"
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node"
"github.com/pkg/xattr"
"github.com/shamaton/msgpack/v2"
)
type consistencyChecker struct {
cfg *storageUsersConfig.Config
ignorer *ignore.Ignorer
recalculateChecksums bool
restartRequired bool
}
// checkPosixfsConsistency checks the consistency of the posixfs storage. The
// given path determines the scope of the check: the whole storage, a single
// space or a single entity within a space.
func (c *consistencyChecker) Check(paths []string) error {
_ = processPosixFsResources(paths, true,
func(path string) error {
fmt.Println("Checking personal spaces...")
c.checkSpaces(path)
return nil
},
func(path string) error {
fmt.Println("Checking project spaces...")
c.checkSpaces(path)
return nil
},
func(path string) error {
fmt.Printf("Checking space '%s'...\n", path)
c.checkSpace(path)
return nil
},
func(path string) error {
if c.ignorer.IsIgnored(path) {
return nil
}
fmt.Printf("Checking '%s'...\n", path)
c.checkEntity(path)
return nil
},
)
if c.restartRequired {
fmt.Println("\n\n ⚠️ Please restart your openCloud instance to apply changes.")
}
return nil
}
func (c *consistencyChecker) checkSpaces(basePath string) {
dirEntries, err := os.ReadDir(basePath)
if err != nil {
logFailure("Error reading spaces directory '%s': %v", basePath, err)
return
}
for _, entry := range dirEntries {
if entry.IsDir() {
fullPath := filepath.Join(basePath, entry.Name())
c.checkSpace(fullPath)
}
}
}
func (c *consistencyChecker) checkSpace(spacePath string) {
info, err := os.Stat(spacePath)
if err != nil {
logFailure("Error accessing path '%s': %v", spacePath, err)
return
}
if !info.IsDir() {
logFailure("Error: The provided path '%s' is not a directory\n", spacePath)
return
}
spaceID, err := xattr.Get(spacePath, prefixes.SpaceIDAttr)
if err != nil || len(spaceID) == 0 {
logFailure("Error: The directory '%s' does not seem to be a space root, it's missing the '%s' attribute\n", spacePath, prefixes.SpaceIDAttr)
return
}
c.checkSpaceID(spacePath)
c.checkNodes(spacePath)
}
func (c *consistencyChecker) checkSpaceID(spacePath string) {
entries, uniqueIDs, oldestEntry, err := c.gatherAttributes(spacePath)
if err != nil {
logFailure("Failed to gather attributes: %v", err)
return
}
if len(entries) == 0 {
return
}
if len(uniqueIDs) > 1 {
fmt.Println("\n ⚠ Multiple space IDs found:")
for id := range uniqueIDs {
fmt.Printf(" - %s\n", id)
}
fmt.Printf("\n ⏳ Oldest entry is '%s' (modified on %s).\n",
filepath.Base(oldestEntry.Path), oldestEntry.ModTime.Format(time.RFC1123))
targetID := oldestEntry.ParentID
fmt.Printf(" ✅ Proposed target Parent ID: %s\n", targetID)
fmt.Printf("\n Do you want to unify all parent IDs to '%s'? This will modify %d entries, the directory, and the user index. (y/N): ", targetID, len(entries))
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(strings.ToLower(input))
if input != "y" {
logFailure("Operation cancelled by user.")
return
}
c.restartRequired = true
obsoleteIDs := []string{}
for id := range uniqueIDs {
if id != targetID {
obsoleteIDs = append(obsoleteIDs, id)
}
}
c.fixSpaceID(spacePath, obsoleteIDs, targetID, entries)
}
}
func (c *consistencyChecker) walkNodes(dir string, parentID string) int {
fixes := 0
entries, err := os.ReadDir(dir)
if err != nil {
logFailure("Error reading directory '%s': %v", dir, err)
return 0
}
for _, entry := range entries {
fullPath := filepath.Join(dir, entry.Name())
if c.ignorer.IsIgnored(fullPath) {
continue
}
fixes += c.checkNodeAttributes(fullPath, entry.Name(), parentID, entry.IsDir())
if entry.IsDir() {
nodeID, err := xattr.Get(fullPath, prefixes.IDAttr)
if err != nil || len(nodeID) == 0 {
logFailure("Directory '%s' missing '%s', skipping its children", fullPath, prefixes.IDAttr)
continue
}
fixes += c.walkNodes(fullPath, string(nodeID))
}
}
return fixes
}
// checkNodeAttributes checks and fixes the parent ID and name attributes of a
// single node. For files it additionally checks the blobsize and, when
// requested, the checksums. It returns the number of fixes applied.
func (c *consistencyChecker) checkNodeAttributes(path, name, parentID string, isDir bool) int {
fixes := 0
// Check if the parent ID attribute matches the expected parent ID, if not, fix it.
actualParentID, err := xattr.Get(path, prefixes.ParentidAttr)
if err != nil || string(actualParentID) != parentID {
if err := xattr.Set(path, prefixes.ParentidAttr, []byte(parentID)); err != nil {
logFailure("Failed to fix parent ID for '%s': %v", path, err)
} else {
fmt.Printf(" + Fixed parent ID for '%s'\n", path)
fixes++
c.restartRequired = true
}
}
// Check that the name attribute matches the actual name of the file/directory, if not, fix it.
nameAttr, err := xattr.Get(path, prefixes.NameAttr)
if err != nil || string(nameAttr) != name {
if err := xattr.Set(path, prefixes.NameAttr, []byte(name)); err != nil {
logFailure("Failed to fix name attribute for '%s': %v", path, err)
} else {
fmt.Printf(" + Fixed name attribute for '%s'\n", path)
fixes++
c.restartRequired = true
}
}
if !isDir {
fixes += c.checkBlobsize(path)
if c.recalculateChecksums {
fixes += c.fixChecksums(path)
}
}
return fixes
}
// checkEntity checks a single file or directory within a space, including its own
// parent ID, name and (for files) blobsize/checksums. If the entity is a directory
// its children are checked recursively.
func (c *consistencyChecker) checkEntity(path string) {
info, err := os.Stat(path)
if err != nil {
logFailure("Error accessing path '%s': %v", path, err)
return
}
// The expected parent ID is the ID attribute of the containing directory.
parentDir := filepath.Dir(path)
parentID, err := xattr.Get(parentDir, prefixes.IDAttr)
if err != nil || len(parentID) == 0 {
logFailure("Parent directory '%s' is missing the '%s' attribute", parentDir, prefixes.IDAttr)
return
}
fixes := c.checkNodeAttributes(path, info.Name(), string(parentID), info.IsDir())
if info.IsDir() {
nodeID, err := xattr.Get(path, prefixes.IDAttr)
if err != nil || len(nodeID) == 0 {
logFailure("Directory '%s' missing '%s' attribute", path, prefixes.IDAttr)
} else {
fixes += c.walkNodes(path, string(nodeID))
}
}
if fixes > 0 {
fmt.Printf(" ✓ Fixed %d incorrect node attributes for %s\n", fixes, filepath.Base(path))
}
}
// checkBlobsize verifies that the stored blobsize attribute matches the actual
// file size and fixes it if it doesn't. It returns the number of fixes applied.
func (c *consistencyChecker) checkBlobsize(path string) int {
info, err := os.Stat(path)
if err != nil {
logFailure("Error accessing file '%s': %v", path, err)
return 0
}
expectedSize := strconv.FormatInt(info.Size(), 10)
blobsize, err := xattr.Get(path, prefixes.BlobsizeAttr)
if err == nil && string(blobsize) == expectedSize {
return 0
}
if err := xattr.Set(path, prefixes.BlobsizeAttr, []byte(expectedSize)); err != nil {
logFailure("Failed to fix blobsize for '%s': %v", path, err)
return 0
}
fmt.Printf(" + Fixed blobsize for '%s'\n", path)
c.restartRequired = true
return 1
}
// fixChecksums recalculates the sha1, md5 and adler32 checksums of the file and
// updates the stored attributes if they differ. It returns the number of fixes applied.
func (c *consistencyChecker) fixChecksums(path string) int {
sha1h, md5h, adler32h, err := node.CalculateChecksums(context.Background(), path)
if err != nil {
logFailure("Failed to calculate checksums for '%s': %v", path, err)
return 0
}
checksums := map[string][]byte{
prefixes.ChecksumPrefix + "sha1": sha1h.Sum(nil),
prefixes.ChecksumPrefix + "md5": md5h.Sum(nil),
prefixes.ChecksumPrefix + "adler32": adler32h.Sum(nil),
}
fixes := 0
for attrName, sum := range checksums {
current, err := xattr.Get(path, attrName)
if err == nil && bytes.Equal(current, sum) {
continue
}
if err := xattr.Set(path, attrName, sum); err != nil {
logFailure("Failed to fix checksum '%s' for '%s': %v", attrName, path, err)
continue
}
fmt.Printf(" + Fixed checksum '%s' for '%s'\n", attrName, path)
c.restartRequired = true
fixes++
}
return fixes
}
func (c *consistencyChecker) checkNodes(spacePath string) {
rootID, err := xattr.Get(spacePath, prefixes.IDAttr)
if err != nil || len(rootID) == 0 {
logFailure("Space root '%s' missing '%s' attribute", spacePath, prefixes.IDAttr)
return
}
fixes := c.walkNodes(spacePath, string(rootID))
if fixes > 0 {
fmt.Printf(" ✓ Fixed %d incorrect node attributes in %s\n", fixes, filepath.Base(spacePath))
}
}
// fixSpaceID updates the parentid attributes of all entries in a space to a new target ID,
// updates the space's own ID attributes, and removes obsolete IDs from the user index file.
func (c *consistencyChecker) fixSpaceID(spacePath string, obsoleteIDs []string, targetID string, entries []EntryInfo) {
// Set all parentid attributes to the proper space ID
err := setAllParentIDAttributes(entries, targetID)
if err != nil {
logFailure("an error occurred during file attribute update: %v", err)
return
}
// Update space ID itself
fmt.Printf(" Updating directory '%s' with attribute '%s' -> %s\n", filepath.Base(spacePath), prefixes.IDAttr, targetID)
err = xattr.Set(spacePath, prefixes.IDAttr, []byte(targetID))
if err != nil {
logFailure("Failed to set attribute on directory '%s': %v", spacePath, err)
return
}
err = xattr.Set(spacePath, prefixes.SpaceIDAttr, []byte(targetID))
if err != nil {
logFailure("Failed to set attribute on directory '%s': %v", spacePath, err)
return
}
// update the index
err = c.updateOwnerIndexFile(spacePath, obsoleteIDs)
if err != nil {
logFailure("Could not update the owner index file: %v", err)
}
}
func (c *consistencyChecker) gatherAttributes(path string) ([]EntryInfo, map[string]struct{}, EntryInfo, error) {
dirEntries, err := os.ReadDir(path)
if err != nil {
return nil, nil, EntryInfo{}, fmt.Errorf("failed to read directory: %w", err)
}
var allEntries []EntryInfo
uniqueIDs := make(map[string]struct{})
var oldestEntry EntryInfo
oldestTime := time.Now().Add(100 * 365 * 24 * time.Hour) // Set to a future date to find the oldest entry
for _, entry := range dirEntries {
fullPath := filepath.Join(path, entry.Name())
if c.ignorer.IsIgnored(fullPath) {
continue
}
info, err := os.Stat(fullPath)
if err != nil {
fmt.Printf(" - Warning: could not stat %s: %v\n", entry.Name(), err)
continue
}
parentID, err := xattr.Get(fullPath, prefixes.ParentidAttr)
if err != nil {
continue // Skip if attribute doesn't exist or can't be read
}
entryInfo := EntryInfo{
Path: fullPath,
ModTime: info.ModTime(),
ParentID: string(parentID),
}
allEntries = append(allEntries, entryInfo)
uniqueIDs[string(parentID)] = struct{}{}
if entryInfo.ModTime.Before(oldestTime) {
oldestTime = entryInfo.ModTime
oldestEntry = entryInfo
}
}
return allEntries, uniqueIDs, oldestEntry, nil
}
func setAllParentIDAttributes(entries []EntryInfo, targetID string) error {
fmt.Printf(" Setting all parent IDs to '%s':\n", targetID)
for _, entry := range entries {
if entry.ParentID == targetID {
fmt.Printf(" - Skipping '%s' (already has target ID).\n", filepath.Base(entry.Path))
continue
}
fmt.Printf(" - Removing all attributes from '%s'. It will be re-assimilated\n", filepath.Base(entry.Path))
filepath.WalkDir(entry.Path, func(path string, d os.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("error walking path '%s': %w", path, err)
}
// Remove all attributes from the file.
if err := removeAttributes(path); err != nil {
fmt.Printf("failed to remove attributes from '%s': %v", path, err)
}
return nil
})
}
return nil
}
// updateOwnerIndexFile handles the logic of reading, modifying, and writing the MessagePack index file.
func (c *consistencyChecker) updateOwnerIndexFile(basePath string, obsoleteIDs []string) error {
fmt.Printf(" Rewriting index file '%s'\n", basePath)
ownerID, err := xattr.Get(basePath, prefixes.OwnerIDAttr)
if err != nil {
return fmt.Errorf("could not get owner ID from oldest entry '%s' to find index: %w", basePath, err)
}
indexPath := filepath.Join(c.cfg.Drivers.Posix.Root, "indexes", "by-user-id", string(ownerID)+".mpk")
indexPath = filepath.Clean(indexPath)
// Read the MessagePack file
fileData, err := os.ReadFile(indexPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("index file does not exist, skipping update")
}
return fmt.Errorf("could not read index file: %w", err)
}
var indexMap map[string]string
if err := msgpack.Unmarshal(fileData, &indexMap); err != nil {
return fmt.Errorf("failed to parse MessagePack index file (is it corrupt?): %w", err)
}
// Remove obsolete IDs from the map
itemsRemoved := 0
for _, id := range obsoleteIDs {
if _, exists := indexMap[id]; exists {
fmt.Printf(" - Removing obsolete ID '%s' from index.\n", id)
delete(indexMap, id)
itemsRemoved++
} else {
fmt.Printf(" - Obsolete ID '%s' not found in index\n", id)
}
}
if itemsRemoved == 0 {
return nil
}
// Write the data back to the file
updatedData, err := msgpack.Marshal(&indexMap)
if err != nil {
return fmt.Errorf("failed to marshal updated index map: %w", err)
}
if err := os.WriteFile(indexPath, updatedData, 0644); err != nil {
return fmt.Errorf("failed to write updated index file: %w", err)
}
fmt.Printf(" ✓ Successfully removed %d item(s) and saved index file.\n", itemsRemoved)
return nil
}
func removeAttributes(path string) error {
attrNames, err := xattr.List(path)
if err != nil {
return fmt.Errorf("failed to list attributes for '%s': %w", path, err)
}
for _, attrName := range attrNames {
if err := xattr.Remove(path, attrName); err != nil {
return fmt.Errorf("failed to remove attribute '%s' from '%s': %w", attrName, path, err)
}
}
return nil
}
+46 -17
View File
@@ -25,6 +25,8 @@ var (
_nodesGlobPattern = "spaces/*/*/nodes/"
)
const posixDriver = "posix"
// RevisionsCommand is the entrypoint for the revisions command.
func RevisionsCommand(cfg *config.Config) *cobra.Command {
revCmd := &cobra.Command{
@@ -86,23 +88,28 @@ func PurgeRevisionsCommand(cfg *config.Config) *cobra.Command {
mechanism = "glob"
}
var posix = cfg.StorageUsers.Driver == posixDriver
var ch <-chan string
switch mechanism {
default:
fallthrough
case "glob":
p := generatePath(basePath, rid)
p := generatePath(basePath, rid, posix)
if rid.GetOpaqueId() == "" {
p = filepath.Join(p, "*/*/*/*/*")
}
ch = revisions.Glob(p)
case "workers":
p := generatePath(basePath, rid)
p := generatePath(basePath, rid, posix)
ch = revisions.GlobWorkers(p, "/*", "/*/*/*/*")
case "list":
p := filepath.Join(basePath, "spaces")
p := basePath
if !posix {
p = filepath.Join(basePath, "spaces")
}
if rid != nil {
p = generatePath(basePath, rid)
p = generatePath(basePath, rid, posix)
}
ch = revisions.List(p, 10)
}
@@ -144,22 +151,44 @@ func printResults(countFiles, countBlobs, countRevisions int, dryRun bool) {
}
}
func generatePath(basePath string, rid *provider.ResourceId) string {
if rid == nil {
return filepath.Join(basePath, _nodesGlobPattern)
}
func generatePath(basePath string, rid *provider.ResourceId, posix bool) string {
// decomposedfs and posix store the revisions of a node at different
// locations on disk, so the path has to be built per driver:
// - decomposedfs: <basePath>/spaces/<pathified spaceID>/nodes/<pathified nodeID>.REV.<ts>
// - posix: <basePath>/<users|projects>/<spaceID>/.oc-nodes/<pathified nodeID>.REV.<ts>
if posix {
if rid == nil {
return filepath.Join(basePath, "*", "*", ".oc-nodes")
}
sid := lookup.Pathify(rid.GetSpaceId(), 1, 2)
if sid == "" {
return ""
}
nid := lookup.Pathify(rid.GetOpaqueId(), 4, 2)
if nid != "" {
return filepath.Join(basePath, "*", "*", ".oc-nodes", nid+"*")
}
nid := lookup.Pathify(rid.GetOpaqueId(), 4, 2)
if nid == "" {
return filepath.Join(basePath, "spaces", sid, "nodes")
}
if rid.GetSpaceId() == "" {
return ""
}
return filepath.Join(basePath, "*", rid.GetSpaceId(), ".oc-nodes")
} else {
// decomposedfs
if rid == nil {
return filepath.Join(basePath, "spaces", sid, "nodes", nid+"*")
return filepath.Join(basePath, _nodesGlobPattern)
}
sid := lookup.Pathify(rid.GetSpaceId(), 1, 2)
if sid == "" {
return ""
}
nid := lookup.Pathify(rid.GetOpaqueId(), 4, 2)
if nid == "" {
return filepath.Join(basePath, "spaces", sid, "nodes")
}
return filepath.Join(basePath, "spaces", sid, "nodes", nid+"*")
}
}
func init() {
+12 -2
View File
@@ -6,11 +6,13 @@ import (
"os/signal"
"syscall"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"github.com/opencloud-eu/opencloud/opencloud/pkg/register"
"github.com/opencloud-eu/opencloud/pkg/clihelper"
"github.com/opencloud-eu/opencloud/pkg/config"
"github.com/spf13/cobra"
oclog "github.com/opencloud-eu/opencloud/pkg/log"
)
// Execute is the entry point for the opencloud command.
@@ -38,3 +40,11 @@ func Execute() error {
ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP)
return app.ExecuteContext(ctx)
}
func logger(name string) zerolog.Logger {
return oclog.NewLogger(
oclog.Name(name),
oclog.Level("info"),
oclog.Pretty(true),
oclog.Color(true)).Logger
}
-12
View File
@@ -12,7 +12,6 @@ import (
appprovider "github.com/opencloud-eu/opencloud/services/app-provider/pkg/command"
appregistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/command"
audit "github.com/opencloud-eu/opencloud/services/audit/pkg/command"
authapi "github.com/opencloud-eu/opencloud/services/auth-api/pkg/command"
authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/command"
authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/command"
authbearer "github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/command"
@@ -25,7 +24,6 @@ import (
gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/command"
graph "github.com/opencloud-eu/opencloud/services/graph/pkg/command"
groups "github.com/opencloud-eu/opencloud/services/groups/pkg/command"
groupware "github.com/opencloud-eu/opencloud/services/groupware/pkg/command"
idm "github.com/opencloud-eu/opencloud/services/idm/pkg/command"
idp "github.com/opencloud-eu/opencloud/services/idp/pkg/command"
invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command"
@@ -140,11 +138,6 @@ var serviceCommands = []register.Command{
cfg.Groups.Commons = cfg.Commons
})
},
func(cfg *config.Config) *cobra.Command {
return ServiceCommand(cfg, cfg.Groupware.Service.Name, groupware.GetCommands(cfg.Groupware), func(c *config.Config) {
cfg.Groupware.Commons = cfg.Commons
})
},
func(cfg *config.Config) *cobra.Command {
return ServiceCommand(cfg, cfg.IDM.Service.Name, idm.GetCommands(cfg.IDM), func(c *config.Config) {
cfg.IDM.Commons = cfg.Commons
@@ -265,11 +258,6 @@ var serviceCommands = []register.Command{
cfg.Webfinger.Commons = cfg.Commons
})
},
func(cfg *config.Config) *cobra.Command {
return ServiceCommand(cfg, cfg.AuthApi.Service.Name, authapi.GetCommands(cfg.AuthApi), func(c *config.Config) {
cfg.AuthApi.Commons = cfg.Commons
})
},
}
// ServiceCommand composes a cobra command from the given inputs.
+24 -18
View File
@@ -1,7 +1,9 @@
package command
import (
"context"
"errors"
"time"
"github.com/spf13/viper"
@@ -9,7 +11,6 @@ import (
"github.com/opencloud-eu/opencloud/pkg/config"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
"github.com/opencloud-eu/opencloud/pkg/config/parser"
oclog "github.com/opencloud-eu/opencloud/pkg/log"
mregistry "github.com/opencloud-eu/opencloud/pkg/registry"
sharing "github.com/opencloud-eu/opencloud/services/sharing/pkg/config"
sharingparser "github.com/opencloud-eu/opencloud/services/sharing/pkg/config/parser"
@@ -22,6 +23,14 @@ import (
"github.com/spf13/cobra"
)
// need to be discussed, for now I will let it here
//
// Problem:
// in reva on CleanupStaleShares call there is migrations invokation, which at leat in tests takes some time,
// reva code was modified to wait until migrations are done, to prevent cases when migrations are stuck and the
// this executions is not returned this timeout is needed
const cleanupTimeout = 1 * time.Minute
// SharesCommand is the entrypoint for the groups command.
func SharesCommand(cfg *config.Config) *cobra.Command {
sharesCmd := &cobra.Command{
@@ -85,7 +94,7 @@ func cleanup(_ *cobra.Command, cfg *config.Config) error {
return configlog.ReturnError(errors.New("cleanup is only implemented for the jsoncs3 share manager"))
}
l := logger()
l := logger("migrate")
zerolog.SetGlobalLevel(zerolog.InfoLevel)
@@ -94,7 +103,7 @@ func cleanup(_ *cobra.Command, cfg *config.Config) error {
if !ok {
return configlog.ReturnError(errors.New("Unknown share manager type '" + driver + "'"))
}
mgr, err := f(rcfg[driver].(map[string]any), l)
mgr, err := f(rcfg[driver].(map[string]any), &l)
if err != nil {
return configlog.ReturnError(err)
}
@@ -121,7 +130,11 @@ func cleanup(_ *cobra.Command, cfg *config.Config) error {
}
serviceUserCtx = l.WithContext(serviceUserCtx)
mgr.(*jsoncs3.Manager).CleanupStaleShares(serviceUserCtx)
cleanupCtx, cancel := context.WithTimeout(serviceUserCtx, cleanupTimeout)
defer cancel()
if err := mgr.(*jsoncs3.Manager).CleanupStaleShares(cleanupCtx); err != nil {
return configlog.ReturnError(err)
}
return nil
}
@@ -159,20 +172,13 @@ func revaShareConfig(cfg *sharing.Config) map[string]any {
"machine_auth_apikey": cfg.UserSharingDrivers.CS3.SystemUserAPIKey,
},
"jsoncs3": map[string]any{
"gateway_addr": cfg.Reva.Address,
"provider_addr": cfg.UserSharingDrivers.JSONCS3.ProviderAddr,
"service_user_id": cfg.UserSharingDrivers.JSONCS3.SystemUserID,
"service_user_idp": cfg.UserSharingDrivers.JSONCS3.SystemUserIDP,
"machine_auth_apikey": cfg.UserSharingDrivers.JSONCS3.SystemUserAPIKey,
"gateway_addr": cfg.Reva.Address,
"provider_addr": cfg.UserSharingDrivers.JSONCS3.ProviderAddr,
"system_user_id": cfg.UserSharingDrivers.JSONCS3.SystemUserID,
"system_user_idp": cfg.UserSharingDrivers.JSONCS3.SystemUserIDP,
"machine_auth_apikey": cfg.UserSharingDrivers.JSONCS3.SystemUserAPIKey,
"service_account_id": cfg.ServiceAccount.ServiceAccountID,
"service_account_secret": cfg.ServiceAccount.ServiceAccountSecret,
},
}
}
func logger() *zerolog.Logger {
log := oclog.NewLogger(
oclog.Name("migrate"),
oclog.Level("info"),
oclog.Pretty(true),
oclog.Color(true)).Logger
return &log
}
+2 -1
View File
@@ -36,7 +36,8 @@ func TrashPurgeEmptyDirsCommand(cfg *config.Config) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
basePath, _ := cmd.Flags().GetString("basepath")
dryRun, _ := cmd.Flags().GetBool("dry-run")
if err := trash.PurgeTrashEmptyPaths(basePath, dryRun); err != nil {
posix := cfg.StorageUsers.Driver == posixDriver
if err := trash.PurgeTrashEmptyPaths(basePath, dryRun, posix); err != nil {
fmt.Println(err)
return err
}
-12
View File
@@ -32,7 +32,6 @@ type OpenCloudConfig struct {
AuthBearer AuthbearerService `yaml:"auth_bearer"`
Users UsersAndGroupsService `yaml:"users"`
Groups UsersAndGroupsService `yaml:"groups"`
Groupware GroupwareService `yaml:"groupware"`
Ocm OcmService `yaml:"ocm"`
Thumbnails ThumbnailService `yaml:"thumbnails"`
Search Search `yaml:"search"`
@@ -127,17 +126,6 @@ type GraphService struct {
ServiceAccount ServiceAccount `yaml:"service_account"`
}
// GroupwareSettings is the configuration for the groupware settings
type GroupwareSettings struct {
WebdavAllowInsecure bool `yaml:"webdav_allow_insecure"`
Cs3AllowInsecure bool `yaml:"cs3_allow_insecure"`
}
// GroupwareService is the configuration for the groupware service
type GroupwareService struct {
Groupware GroupwareSettings
}
// IdmService is the configuration for the IDM service
type IdmService struct {
ServiceUserPasswords ServiceUserPasswordsSettings `yaml:"service_user_passwords"`
+10 -15
View File
@@ -24,7 +24,6 @@ import (
appProvider "github.com/opencloud-eu/opencloud/services/app-provider/pkg/command"
appRegistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/command"
audit "github.com/opencloud-eu/opencloud/services/audit/pkg/command"
authapi "github.com/opencloud-eu/opencloud/services/auth-api/pkg/command"
authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/command"
authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/command"
authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/command"
@@ -36,7 +35,6 @@ import (
gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/command"
graph "github.com/opencloud-eu/opencloud/services/graph/pkg/command"
groups "github.com/opencloud-eu/opencloud/services/groups/pkg/command"
groupware "github.com/opencloud-eu/opencloud/services/groupware/pkg/command"
idm "github.com/opencloud-eu/opencloud/services/idm/pkg/command"
idp "github.com/opencloud-eu/opencloud/services/idp/pkg/command"
invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command"
@@ -344,16 +342,6 @@ func NewService(ctx context.Context, options ...Option) (*Service, error) {
cfg.Notifications.Commons = cfg.Commons
return notifications.Execute(cfg.Notifications)
})
areg(opts.Config.AuthApi.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.AuthApi.Context = ctx
cfg.AuthApi.Commons = cfg.Commons
return authapi.Execute(cfg.AuthApi)
})
areg(opts.Config.Groupware.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.Groupware.Context = ctx
cfg.Groupware.Commons = cfg.Commons
return groupware.Execute(cfg.Groupware)
})
return s, nil
}
@@ -401,9 +389,16 @@ func Start(ctx context.Context, o ...Option) error {
if ev.Restarting {
l = s.Log.Error()
}
l.Str("event", e.String()).Str("service", ev.ServiceName).Str("supervisor", ev.SupervisorName).
Bool("restarting", ev.Restarting).Float64("failures", ev.CurrentFailures).Float64("threshold", ev.FailureThreshold).
Interface("error", ev.Err).Msg("service terminated")
l = l.Str("event", e.String()).Str("service", ev.ServiceName).Str("supervisor", ev.SupervisorName).
Bool("restarting", ev.Restarting).Float64("failures", ev.CurrentFailures).Float64("threshold", ev.FailureThreshold)
// ev.Err is an interface{}: marshaling an error yields {} because
// its fields are unexported, so the message has to go through Err
if err, ok := ev.Err.(error); ok {
l = l.Err(err)
} else {
l = l.Interface("error", ev.Err)
}
l.Msg("service terminated")
case suture.EventBackoff:
s.Log.Warn().Str("event", e.String()).Str("supervisor", ev.SupervisorName).Msg("service backoff")
case suture.EventResume:
+45 -6
View File
@@ -10,12 +10,23 @@ import (
const (
// _trashGlobPattern is the glob pattern to find all trash items
_trashGlobPattern = "spaces/*/*/trash/*/*/*/*"
// _trashRootPattern is the glob pattern of the trash container root
_trashRootPattern = "spaces/*/*/trash"
// _posixTrashGlobPattern is the glob pattern to find all trash items on posix
_posixTrashGlobPattern = "*/*/.Trash/files/*"
// _posixTrashRootPattern is the glob pattern of the trash container root on posix
_posixTrashRootPattern = "*/*/.Trash/files"
)
// PurgeTrashEmptyPaths purges empty paths in the trash
func PurgeTrashEmptyPaths(p string, dryRun bool) error {
func PurgeTrashEmptyPaths(p string, dryRun bool, posix bool) error {
pattern := _trashGlobPattern
if posix {
pattern = _posixTrashGlobPattern
}
// we have all trash nodes in all spaces now
dirs, err := filepath.Glob(filepath.Join(p, _trashGlobPattern))
dirs, err := filepath.Glob(filepath.Join(p, pattern))
if err != nil {
return err
}
@@ -25,15 +36,26 @@ func PurgeTrashEmptyPaths(p string, dryRun bool) error {
}
for _, d := range dirs {
if err := removeEmptyFolder(d, dryRun); err != nil {
if err := removeEmptyFolder(d, dryRun, posix, p); err != nil {
return err
}
}
return nil
}
func removeEmptyFolder(path string, dryRun bool) error {
func removeEmptyFolder(path string, dryRun bool, posix bool, basePath string) error {
if dryRun {
if posix {
// on posix the ".trashitem" entries are the actual data and can be
// files, which are skipped, so we need to check here if the path
// is the actual dir, the same check for real removal part
fi, err := os.Stat(path)
if err != nil || !fi.IsDir() {
return nil
}
}
f, err := os.ReadDir(path)
if err != nil {
return err
@@ -43,6 +65,14 @@ func removeEmptyFolder(path string, dryRun bool) error {
}
return nil
}
if posix {
fi, err := os.Stat(path)
if err != nil || !fi.IsDir() {
return nil
}
}
if err := os.Remove(path); err != nil {
// we do not really care about the error here
// if the folder is not empty we will get an error,
@@ -50,8 +80,17 @@ func removeEmptyFolder(path string, dryRun bool) error {
return nil
}
nd := filepath.Dir(path)
if filepath.Base(nd) == "trash" {
if isTrashRoot(nd, basePath, posix) {
return nil
}
return removeEmptyFolder(nd, dryRun)
return removeEmptyFolder(nd, dryRun, posix, basePath)
}
func isTrashRoot(path, basePath string, posix bool) bool {
rootPattern := _trashRootPattern
if posix {
rootPattern = _posixTrashRootPattern
}
matched, _ := filepath.Match(filepath.Join(basePath, rootPattern), path)
return matched
}
+109
View File
@@ -0,0 +1,109 @@
package trash
import (
"os"
"testing"
"github.com/test-go/testify/require"
)
func TestIsTrashRootDecomposed(t *testing.T) {
storageRoot := "test_temp_" + t.Name()
defer os.RemoveAll(storageRoot)
require.True(t, isTrashRoot(storageRoot+"/spaces/id/id/trash", storageRoot, false))
require.False(t, isTrashRoot(storageRoot+"/spaces/id/id/trash/s1/s2/s3/node", storageRoot, false))
require.False(t, isTrashRoot(storageRoot+"/spaces/id/id/trash/s1/s2/trash/node", storageRoot, false))
}
func TestIsTrashRootPosix(t *testing.T) {
storageRoot := "test_temp_" + t.Name()
defer os.RemoveAll(storageRoot)
require.True(t, isTrashRoot(storageRoot+"/users/alice/.Trash/files", storageRoot, true))
require.False(t, isTrashRoot(storageRoot+"/users/alice/.Trash/files/item.trashitem", storageRoot, true))
require.False(t, isTrashRoot(storageRoot+"/users/alice/.Trash/files/item.trashitem/files", storageRoot, true))
}
func TestRemoveEmptyFolderPosix(t *testing.T) {
storageRoot := "test_temp_" + t.Name()
base := storageRoot + "/users/alice/.Trash/files"
defer os.RemoveAll(storageRoot)
emptyChain := base + "/empty.trashitem/sub/subsub"
require.NoError(t, os.MkdirAll(emptyChain, os.ModePerm))
nonEmpty := base + "/keep.trashitem"
require.NoError(t, os.MkdirAll(nonEmpty, os.ModePerm))
require.NoError(t, os.WriteFile(nonEmpty+"/file.txt", []byte("some text"), os.ModePerm))
require.NoError(t, removeEmptyFolder(emptyChain, false, true, storageRoot))
assertNoDirExists(t, emptyChain)
assertNoDirExists(t, base+"/empty.trashitem")
assertDirExists(t, base)
assertDirExists(t, nonEmpty)
}
func TestRemoveEmptyFolderPosixUserDirNamedFiles(t *testing.T) {
storageRoot := "test_temp_" + t.Name()
base := storageRoot + "/users/alice/.Trash/files"
defer os.RemoveAll(storageRoot)
nestedFilesDir := base + "/folder.trashitem/files/files"
require.NoError(t, os.MkdirAll(nestedFilesDir, os.ModePerm))
require.NoError(t, removeEmptyFolder(nestedFilesDir, false, true, storageRoot))
assertNoDirExists(t, nestedFilesDir)
assertNoDirExists(t, base+"/folder.trashitem/files")
assertNoDirExists(t, base+"/folder.trashitem")
assertDirExists(t, base)
}
func TestRemoveEmptyFolderDecomposed(t *testing.T) {
storageRoot := "test_temp_" + t.Name()
base := storageRoot + "/spaces/id/id/trash"
defer os.RemoveAll(storageRoot)
emptyChain := base + "/s1/s2/s3/node"
require.NoError(t, os.MkdirAll(emptyChain, os.ModePerm))
require.NoError(t, removeEmptyFolder(emptyChain, false, false, storageRoot))
assertNoDirExists(t, emptyChain)
assertNoDirExists(t, base+"/s1/s2/s3")
assertNoDirExists(t, base+"/s1/s2")
assertNoDirExists(t, base+"/s1")
assertDirExists(t, base)
}
func TestRemoveEmptyFolderDecomposedUserDirNamedTrash(t *testing.T) {
storageRoot := "test_temp_" + t.Name()
base := storageRoot + "/spaces/id/id/trash"
defer os.RemoveAll(storageRoot)
nestedTrashDir := base + "/s1/trash/s2/node"
require.NoError(t, os.MkdirAll(nestedTrashDir, os.ModePerm))
require.NoError(t, removeEmptyFolder(nestedTrashDir, false, false, storageRoot))
assertNoDirExists(t, nestedTrashDir)
assertNoDirExists(t, base+"/s1/trash/s2")
assertNoDirExists(t, base+"/s1/trash")
assertNoDirExists(t, base+"/s1")
assertDirExists(t, base)
}
func assertNoDirExists(t *testing.T, path string) {
t.Helper()
_, err := os.Stat(path)
require.True(t, os.IsNotExist(err))
}
func assertDirExists(t *testing.T, path string) {
t.Helper()
fi, err := os.Stat(path)
require.NoError(t, err)
require.True(t, fi.IsDir())
}
+1
View File
@@ -86,5 +86,6 @@
<property name="lineLimit" value="120" />
<property name="absoluteLineLimit" value="120" />
</properties>
<exclude-pattern>*/tests/acceptance/bootstrap/*</exclude-pattern>
</rule>
</ruleset>
+12
View File
@@ -43,6 +43,10 @@ type StringNode struct {
*Base
Key string
Value string
Exact bool
// CaseInsensitive marks a case-insensitive restriction; set by the search
// lowering pass, a backend routes it to the field's lowercased form.
CaseInsensitive bool
}
// BooleanNode represents a bool value
@@ -60,6 +64,14 @@ type DateTimeNode struct {
Value time.Time
}
// NumberNode represents a numeric value
type NumberNode struct {
*Base
Key string
Operator *OperatorNode
Value float64
}
// OperatorNode represents an operator value like
// AND, OR, NOT, =, <= ... and so on
type OperatorNode struct {
+1
View File
@@ -21,6 +21,7 @@ func DiffAst(x, y any, opts ...cmp.Option) string {
cmpopts.IgnoreFields(ast.GroupNode{}, "Base"),
cmpopts.IgnoreFields(ast.BooleanNode{}, "Base"),
cmpopts.IgnoreFields(ast.DateTimeNode{}, "Base"),
cmpopts.IgnoreFields(ast.NumberNode{}, "Base"),
)...,
)
}
+12
View File
@@ -14,5 +14,17 @@ func DefaultApp(app *cobra.Command) *cobra.Command {
// version info
app.Version = fmt.Sprintf("%s (%s <%s>) (%s)", version.String, "OpenCloud GmbH", "support@opencloud.eu", version.Compiled())
// cobra would print the error on top of what main() already prints
app.SilenceErrors = true
// keep the usage block for flag parse errors, drop it once RunE runs.
// Traversing runs the hook even below a subcommand that brings its own,
// e.g. every service below ServiceCommand.
cobra.EnableTraverseRunHooks = true
app.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
cmd.SilenceUsage = true
return nil
}
return app
}
-4
View File
@@ -7,7 +7,6 @@ import (
appProvider "github.com/opencloud-eu/opencloud/services/app-provider/pkg/config"
appRegistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/config"
audit "github.com/opencloud-eu/opencloud/services/audit/pkg/config"
authapi "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config"
authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/config"
authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/config"
authbearer "github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/config"
@@ -20,7 +19,6 @@ import (
gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/config"
graph "github.com/opencloud-eu/opencloud/services/graph/pkg/config"
groups "github.com/opencloud-eu/opencloud/services/groups/pkg/config"
groupware "github.com/opencloud-eu/opencloud/services/groupware/pkg/config"
idm "github.com/opencloud-eu/opencloud/services/idm/pkg/config"
idp "github.com/opencloud-eu/opencloud/services/idp/pkg/config"
invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/config"
@@ -89,7 +87,6 @@ type Config struct {
AppProvider *appProvider.Config `yaml:"app_provider"`
AppRegistry *appRegistry.Config `yaml:"app_registry"`
Audit *audit.Config `yaml:"audit"`
AuthApi *authapi.Config `yaml:"auth_api"`
AuthApp *authapp.Config `yaml:"auth_app"`
AuthBasic *authbasic.Config `yaml:"auth_basic"`
AuthBearer *authbearer.Config `yaml:"auth_bearer"`
@@ -102,7 +99,6 @@ type Config struct {
Gateway *gateway.Config `yaml:"gateway"`
Graph *graph.Config `yaml:"graph"`
Groups *groups.Config `yaml:"groups"`
Groupware *groupware.Config `yaml:"groupware"`
IDM *idm.Config `yaml:"idm"`
IDP *idp.Config `yaml:"idp"`
Invitations *invitations.Config `yaml:"invitations"`
-4
View File
@@ -7,7 +7,6 @@ import (
appProvider "github.com/opencloud-eu/opencloud/services/app-provider/pkg/config/defaults"
appRegistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/config/defaults"
audit "github.com/opencloud-eu/opencloud/services/audit/pkg/config/defaults"
authapi "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config/defaults"
authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/config/defaults"
authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/config/defaults"
authbearer "github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/config/defaults"
@@ -20,7 +19,6 @@ import (
gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/config/defaults"
graph "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults"
groups "github.com/opencloud-eu/opencloud/services/groups/pkg/config/defaults"
groupware "github.com/opencloud-eu/opencloud/services/groupware/pkg/config/defaults"
idm "github.com/opencloud-eu/opencloud/services/idm/pkg/config/defaults"
idp "github.com/opencloud-eu/opencloud/services/idp/pkg/config/defaults"
invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/config/defaults"
@@ -64,7 +62,6 @@ func DefaultConfig() *Config {
AppProvider: appProvider.DefaultConfig(),
AppRegistry: appRegistry.DefaultConfig(),
Audit: audit.DefaultConfig(),
AuthApi: authapi.DefaultConfig(),
AuthApp: authapp.DefaultConfig(),
AuthBasic: authbasic.DefaultConfig(),
AuthBearer: authbearer.DefaultConfig(),
@@ -77,7 +74,6 @@ func DefaultConfig() *Config {
Gateway: gateway.DefaultConfig(),
Graph: graph.DefaultConfig(),
Groups: groups.DefaultConfig(),
Groupware: groupware.DefaultConfig(),
IDM: idm.DefaultConfig(),
IDP: idp.DefaultConfig(),
Invitations: invitations.DefaultConfig(),
-169
View File
@@ -1,169 +0,0 @@
// Provides common utility functions for HTTP.
package http
import (
"bytes"
"fmt"
"io"
"net/http"
)
type readCloser struct {
io.Reader
closer func() error
}
func (c readCloser) Close() error {
if c.closer != nil {
return c.closer()
}
return nil
}
func combinedReader(peekBuffer *bytes.Buffer, body io.Reader) io.Reader {
if body != nil && body != http.NoBody {
if peekBuffer != nil {
return io.MultiReader(peekBuffer, body)
} else {
return body
}
} else {
if peekBuffer != nil {
return peekBuffer
} else {
return http.NoBody
}
}
}
// Reads up to <size> amount of bytes from the response, and returns a ReadCloser that concatenates over those
// bytes as well as the rest of the bytes streamed in <body>, to ensure that the whole response can still be
// read.
//
// Note that if <size> is 0, then this function does nothing.
//
// Furthermore, this function never closes the <body> Reader, that is left to the caller.
//
// # Parameters
// - body: the HTTP response body, which may be nil or http.NoBody
// - size: the number of bytes that will be read from the beginning of the response body,
// which may be 0 (in which case this function won't do anything)
//
// # Return values
// - a Reader that provides the full response, including the first bytes that were peeked
// up to <size> bytes of the beginning of the response body
// - the first <size> bytes of the response
// - a boolean: whether the peeked bytes contain the full response or whether it's truncated
// - an error if an error occured while reading the body response
func PeekResponse(body io.ReadCloser, size int64) (io.ReadCloser, []byte, bool, error) {
var peekBuffer *bytes.Buffer = nil
truncated := false
peek := []byte{}
if body != nil && body != http.NoBody && size > 0 {
peekBuffer = new(bytes.Buffer)
limitedReader := io.LimitReader(body, size)
tee := io.TeeReader(limitedReader, peekBuffer)
if _, err := io.Copy(io.Discard, tee); err != nil {
return nil, peek, false, err
}
if int64(peekBuffer.Len()) >= size {
truncated = true
}
peek = peekBuffer.Bytes()
return readCloser{
closer: body.Close,
Reader: combinedReader(peekBuffer, body),
}, peek, truncated, nil
} else {
return body, peek, truncated, nil
}
}
// Extracts information from an HTTP request and passes it to a function, typically for logging.
//
// It also extracts the <maxBodySize> first bytes of the request body, if maxBodySize is > 0.
func DumpHttpRequest(req *http.Request, maxBodySize int64, closure func(method string, uri string, content string, truncated bool)) error {
var logBuilder bytes.Buffer
uri := ""
if req.URL != nil {
uri = req.URL.String()
}
fmt.Fprintf(&logBuilder, "%s %s\n", req.Method, uri)
for key, values := range req.Header {
if key == "Authorization" {
continue
}
for _, value := range values {
fmt.Fprintf(&logBuilder, "%s: %s\n", key, value)
}
}
truncated := false
if maxBodySize >= 0 && req.Body != nil && req.Body != http.NoBody {
peekBuffer := new(bytes.Buffer)
limitedReader := io.LimitReader(req.Body, maxBodySize)
tee := io.TeeReader(limitedReader, peekBuffer)
if _, err := io.Copy(io.Discard, tee); err != nil {
return fmt.Errorf("failed to peek at the request body for tracing: %v", err)
} else {
logBuilder.Write(peekBuffer.Bytes())
if int64(peekBuffer.Len()) >= maxBodySize {
truncated = true
}
fullBodyReader := io.MultiReader(peekBuffer, req.Body)
req.Body = io.NopCloser(fullBodyReader)
}
}
closure(req.Method, uri, logBuilder.String(), truncated)
return nil
}
// Extracts information from an HTTP response and passes it to a function, typically for logging.
//
// It also extracts the <maxBodySize> first bytes of the response body, if maxBodySize is > 0.
func DumpHttpResponse(resp *http.Response, body io.ReadCloser, maxBodySize int64,
closure func(method string, uri string, content string, truncated bool),
) (io.ReadCloser, error) {
var logBuilder bytes.Buffer
fmt.Fprintf(&logBuilder, "%s\n", resp.Status)
for key, values := range resp.Header {
for _, value := range values {
fmt.Fprintf(&logBuilder, "%s: %s\n", key, value)
}
}
var peekBuffer *bytes.Buffer = nil
truncated := false
if body != nil && body != http.NoBody && maxBodySize > 0 {
peekBuffer = new(bytes.Buffer)
limitedReader := io.LimitReader(body, maxBodySize)
tee := io.TeeReader(limitedReader, peekBuffer)
if _, err := io.Copy(io.Discard, tee); err != nil {
return nil, err
}
if int64(peekBuffer.Len()) >= maxBodySize {
truncated = true
}
logBuilder.Write(peekBuffer.Bytes())
}
req := resp.Request
if req != nil {
closure(req.Method, req.URL.String(), logBuilder.String(), truncated)
} else {
closure("", "", logBuilder.String(), truncated)
}
if peekBuffer != nil {
return readCloser{
closer: body.Close,
Reader: combinedReader(peekBuffer, body),
}, nil
} else {
return body, nil
}
}
-92
View File
@@ -1,92 +0,0 @@
package http
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestPeekResponse(t *testing.T) {
require := require.New(t)
n := 6
text := "hello, world"
require.Less(n, len(text))
body, peek, truncated, err := PeekResponse(io.NopCloser(strings.NewReader(text)), int64(n))
require.NoError(err)
require.True(truncated)
require.Equal(text[:n], string(peek))
buf := new(strings.Builder)
_, err = io.Copy(buf, body)
require.NoError(err)
require.Equal(text, buf.String())
}
func TestDumpHttpResponse(t *testing.T) {
require := require.New(t)
n := 6
text := "hello, world"
h := http.Header{}
h.Set("testing", "true")
h.Add("aaa", "1")
h.Add("aaa", "2")
resp := &http.Response{
Status: "200 OK",
StatusCode: 200,
Header: h,
Body: nil,
}
body, err := DumpHttpResponse(resp, io.NopCloser(strings.NewReader(text)), int64(n), func(method, uri, content string, truncated bool) {
require.True(truncated)
require.Equal(fmt.Sprintf(`200 OK
Testing: true
Aaa: 1
Aaa: 2
%s`, text[:n]), content)
})
require.NoError(err)
buf := new(strings.Builder)
_, err = io.Copy(buf, body)
require.NoError(err)
require.Equal(text, buf.String())
}
func TestDumpHttpRequest(t *testing.T) {
require := require.New(t)
n := 6
text := "hello, world"
h := http.Header{}
h.Set("testing", "true")
h.Add("aaa", "1")
h.Add("aaa", "2")
req := &http.Request{
Method: "GET",
Header: h,
URL: &url.URL{Scheme: "https", Host: "example.com", Path: "/testing"},
Body: io.NopCloser(strings.NewReader(text)),
}
err := DumpHttpRequest(req, int64(n), func(method, uri, content string, truncated bool) {
require.True(truncated)
require.Equal(fmt.Sprintf(`GET https://example.com/testing
Testing: true
Aaa: 1
Aaa: 2
%s`, text[:n]), content)
})
require.NoError(err)
buf := new(strings.Builder)
_, err = io.Copy(buf, req.Body)
require.NoError(err)
require.Equal(text, buf.String())
}
-1
View File
@@ -1 +0,0 @@
/apidoc-examples.json
-179
View File
@@ -1,179 +0,0 @@
package jmap
import (
"context"
"io"
"net/url"
"time"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
)
type Context struct {
Session *Session
Context context.Context
Logger *log.Logger
AcceptLanguage string
}
func (c Context) WithLogger(newLogger *log.Logger) Context {
return Context{Session: c.Session, Context: c.Context, AcceptLanguage: c.AcceptLanguage, Logger: newLogger}
}
func (c Context) WithContext(newContext context.Context) Context {
return Context{Session: c.Session, Context: newContext, AcceptLanguage: c.AcceptLanguage, Logger: c.Logger}
}
type ApiClient interface {
Command(operation Operation, request Request, ctx Context) (io.ReadCloser, Language, Error)
io.Closer
}
type WsPushListener interface {
OnNotification(username string, stateChange StateChange)
}
type WsClient interface {
DisableNotifications() Error
io.Closer
}
type WsClientFactory interface {
EnableNotifications(ctx context.Context, pushState State, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error)
io.Closer
}
type SessionClient interface {
GetSession(ctx context.Context, baseurl *url.URL, username string, logger *log.Logger) (SessionResponse, Error)
io.Closer
}
type BlobClient interface {
UploadBinary(uploadUrl string, operation Operation, endpoint string, contentType string, content io.Reader, ctx Context) (UploadedBlob, Language, Error)
DownloadBinary(downloadUrl string, operation Operation, endpoint string, ctx Context) (*BlobDownload, Language, Error)
io.Closer
}
const (
logOperation = "operation"
logFetchBodies = "fetch-bodies"
logPosition = "position"
logLimit = "limit"
logDownloadUrl = "download-url"
logBlobId = "blob-id"
logSinceState = "since-state"
)
type ResultMetadata interface {
GetSessionState() SessionState
GetState() State
GetLanguage() Language
GetDurations() []time.Duration
}
type Result[T any] struct {
Payload T
SessionState SessionState
State State
Language Language
Durations []time.Duration
}
func RefineResultPayload[A, B any](a Result[A], refiner func(A) (B, bool, error)) (Result[B], error) {
if payloads, ok, err := refiner(a.Payload); err != nil {
return ZeroResult[B](a.Durations), err
} else if ok {
return NewResult(payloads, a.SessionState, a.State, a.Language, a.Durations), nil
} else {
return ZeroResult[B](a.Durations), nil
}
}
func RefineResult[A, B any](a Result[A], refiner func(A, SessionState, State, Language) (B, SessionState, State, Language)) Result[B] {
b, bss, bs, bl := refiner(a.Payload, a.SessionState, a.State, a.Language)
return NewResult(b, bss, bs, bl, a.Durations)
}
func RefineResultSlice[A, B any](a []*Result[A], refiner func([]*A, []*SessionState, []*State, []*Language) (B, SessionState, State, Language, error)) (Result[B], error) {
payloads := structs.Map(a, func(e *Result[A]) *A {
if e != nil {
return &e.Payload
} else {
return nil
}
})
sessionStates := structs.Map(a, func(e *Result[A]) *SessionState {
if e != nil {
return &e.SessionState
} else {
return nil
}
})
states := structs.Map(a, func(e *Result[A]) *State {
if e != nil {
return &e.State
} else {
return nil
}
})
languages := structs.Map(a, func(e *Result[A]) *Language {
if e != nil {
return &e.Language
} else {
return nil
}
})
durations := structs.Flatten(structs.Map(a, func(e *Result[A]) []time.Duration {
if e != nil {
return e.Durations
} else {
return nil
}
}))
b, bss, bs, bl, err := refiner(payloads, sessionStates, states, languages)
return NewResult(b, bss, bs, bl, durations), err
}
func (r Result[T]) GetSessionState() SessionState {
return r.SessionState
}
func (r Result[T]) GetState() State {
return r.State
}
func (r Result[T]) GetLanguage() Language {
return r.Language
}
func (r Result[T]) GetDurations() []time.Duration {
return r.Durations
}
func NewResult[T any](payload T, sessionState SessionState, state State, language Language, durations []time.Duration) Result[T] {
return Result[T]{
Payload: payload,
SessionState: sessionState,
State: state,
Language: language,
Durations: durations,
}
}
func newPartialResult[T any](sessionState SessionState, language Language, durations []time.Duration) Result[T] {
return Result[T]{
SessionState: sessionState,
Language: language,
Durations: durations,
}
}
func ZeroResult[T any](durations []time.Duration) Result[T] {
return Result[T]{Durations: durations}
}
func ZeroResultV[T any]() Result[T] {
return Result[T]{Durations: nil}
}
func ZeroResultM[T any](t Result[T]) Result[T] {
return Result[T]{Durations: t.GetDurations()}
}
-104
View File
@@ -1,104 +0,0 @@
package jmap
var NS_ADDRESSBOOKS = ns(JmapContacts)
func (j *Client) GetAddressbooks(accountId AccountId, ids []string, ctx Context) (Result[AddressBookGetResponse], error) {
return get(j, "GetAddressbooks", MailboxType,
func(accountId AccountId, ids []string) AddressBookGetCommand {
return AddressBookGetCommand{AccountId: accountId, Ids: ids}
},
AddressBookGetResponse{},
identity1,
accountId, ids,
ctx,
)
}
type AddressBookChanges ChangesTemplate[AddressBook]
var _ Changes[AddressBook] = AddressBookChanges{}
func (c AddressBookChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c AddressBookChanges) GetOldState() State { return c.OldState }
func (c AddressBookChanges) GetNewState() State { return c.NewState }
func (c AddressBookChanges) GetCreated() []AddressBook { return c.Created }
func (c AddressBookChanges) GetUpdated() []AddressBook { return c.Updated }
func (c AddressBookChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve Address Book changes since a given state.
// @apidoc addressbook,changes
func (j *Client) GetAddressbookChanges(accountId AccountId, sinceState State, maxChanges uint, ctx Context) (Result[AddressBookChanges], error) {
return changesA(j, "GetAddressbookChanges", MailboxType,
func() AddressBookChangesCommand {
return AddressBookChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
AddressBookChangesResponse{},
AddressBookGetResponse{},
func(path string, rof string) AddressBookGetRefCommand {
return AddressBookGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandAddressBookChanges,
Path: path,
ResultOf: rof,
},
}
},
func(oldState, newState State, hasMoreChanges bool, created, updated []AddressBook, destroyed []string) AddressBookChanges {
return AddressBookChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
ctx,
)
}
func (j *Client) CreateAddressBook(accountId AccountId, addressbook AddressBookChange, ctx Context) (Result[*AddressBook], error) {
return create(j, "CreateAddressBook", MailboxType,
func(accountId AccountId, create map[string]AddressBookChange) AddressBookSetCommand {
return AddressBookSetCommand{AccountId: accountId, Create: create}
},
func(accountId AccountId, ids string) AddressBookGetCommand {
return AddressBookGetCommand{AccountId: accountId, Ids: []string{ids}}
},
func(resp AddressBookSetResponse) map[string]*AddressBook {
return resp.Created
},
func(resp AddressBookGetResponse) []AddressBook {
return resp.List
},
accountId, addressbook,
ctx,
)
}
func (j *Client) DeleteAddressBook(accountId AccountId, destroyIds []string, ctx Context) (Result[map[string]SetError], error) {
return destroy(j, "DeleteAddressBook", MailboxType,
func(accountId AccountId, destroy []string) AddressBookSetCommand {
return AddressBookSetCommand{AccountId: accountId, Destroy: destroy}
},
AddressBookSetResponse{},
accountId, destroyIds,
ctx,
)
}
func (j *Client) UpdateAddressBook(accountId AccountId, id string, changes AddressBookChange, ctx Context) (Result[AddressBook], error) {
return update(j, "UpdateAddressBook", MailboxType,
func(update map[string]PatchObject) AddressBookSetCommand {
return AddressBookSetCommand{AccountId: accountId, Update: update}
},
func(id string) AddressBookGetCommand {
return AddressBookGetCommand{AccountId: accountId, Ids: []string{id}}
},
func(resp AddressBookSetResponse) map[string]SetError { return resp.NotUpdated },
func(resp AddressBookGetResponse) AddressBook { return resp.List[0] },
id, changes,
ctx,
)
}
-136
View File
@@ -1,136 +0,0 @@
package jmap
import (
"encoding/base64"
"io"
"net/url"
"strings"
"github.com/opencloud-eu/opencloud/pkg/log"
)
var NS_BLOB = ns(JmapBlob)
func (j *Client) GetBlobMetadata(accountId AccountId, ids []string, ctx Context) (Result[BlobGetResponse], error) {
get := BlobGetCommand{
AccountId: accountId,
Ids: ids,
// add BlobPropertyData to retrieve the data
Properties: []string{BlobPropertyDigestSha256, BlobPropertyDigestSha512, BlobPropertySize},
}
cmd, jerr := j.request(ctx, NS_BLOB,
invocation(get, "0"),
)
if jerr != nil {
return ZeroResultV[BlobGetResponse](), jerr
}
return command(j, Operation("GetBlobMetadata"), ctx, cmd, func(body *Response) (BlobGetResponse, State, Error) {
var response BlobGetResponse
err := retrieveGet(ctx, body, get, "0", &response)
if err != nil {
return BlobGetResponse{}, EmptyState, err
}
return response, response.State, nil
})
}
type UploadedBlobWithHash struct {
BlobId string `json:"blobId"`
Size int `json:"size,omitzero"`
Type string `json:"type,omitempty"`
Sha512 string `json:"sha:512,omitempty"`
}
func (j *Client) UploadBlobStream(accountId AccountId, contentType string, body io.Reader, ctx Context) (UploadedBlob, Language, error) {
logger := log.From(ctx.Logger.With().Str(logEndpoint, ctx.Session.UploadEndpoint))
ctx = ctx.WithLogger(logger)
uploadUrl := strings.NewReplacer(
"{accountId}", url.PathEscape(string(accountId)),
).Replace(ctx.Session.UploadUrlTemplate)
return j.blob.UploadBinary(uploadUrl, Operation("UploadBlobStream"), ctx.Session.UploadEndpoint, contentType, body, ctx)
}
func (j *Client) DownloadBlobStream(accountId AccountId, blobId string, name string, typ string, ctx Context) (*BlobDownload, Language, error) { //NOSONAR
logger := log.From(ctx.Logger.With().Str(logEndpoint, ctx.Session.DownloadEndpoint))
ctx = ctx.WithLogger(logger)
downloadUrl := strings.NewReplacer(
"{accountId}", url.PathEscape(string(accountId)),
"{blobId}", url.PathEscape(blobId),
"{name}", url.PathEscape(name),
"{type}", url.PathEscape(typ),
).Replace(ctx.Session.DownloadUrlTemplate)
logger = log.From(logger.With().Str(logDownloadUrl, downloadUrl).Str(logBlobId, blobId))
return j.blob.DownloadBinary(downloadUrl, Operation("DownloadBlobStream"), ctx.Session.DownloadEndpoint, ctx)
}
func (j *Client) UploadBlob(accountId AccountId, data []byte, contentType string, ctx Context) (Result[UploadedBlobWithHash], error) {
encoded := base64.StdEncoding.EncodeToString(data)
upload := BlobUploadCommand{
AccountId: accountId,
Create: map[string]UploadObject{
"0": {
Data: []DataSourceObject{{
DataAsBase64: encoded,
}},
Type: contentType,
},
},
}
getHash := BlobGetRefCommand{
AccountId: accountId,
IdRef: &ResultReference{
ResultOf: "0",
Name: CommandBlobUpload,
Path: "/ids",
},
Properties: []string{BlobPropertyDigestSha512},
}
cmd, jerr := j.request(ctx, ns(JmapBlob),
invocation(upload, "0"),
invocation(getHash, "1"),
)
if jerr != nil {
return ZeroResultV[UploadedBlobWithHash](), jerr
}
return command(j, Operation("UploadBlob"), ctx, cmd, func(body *Response) (UploadedBlobWithHash, State, Error) {
var uploadResponse BlobUploadResponse
err := retrieveUpload(ctx, body, upload, "0", &uploadResponse)
if err != nil {
return UploadedBlobWithHash{}, "", err
}
var getResponse BlobGetResponse
err = retrieveGet(ctx, body, getHash, "1", &getResponse)
if err != nil {
return UploadedBlobWithHash{}, "", err
}
if len(uploadResponse.Created) != 1 {
ctx.Logger.Error().Msgf("%T.Created has %v entries instead of 1", uploadResponse, len(uploadResponse.Created))
return UploadedBlobWithHash{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
upload, ok := uploadResponse.Created["0"]
if !ok {
ctx.Logger.Error().Msgf("%T.Created has no item '0'", uploadResponse)
return UploadedBlobWithHash{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
if len(getResponse.List) != 1 {
ctx.Logger.Error().Msgf("%T.List has %v entries instead of 1", getResponse, len(getResponse.List))
return UploadedBlobWithHash{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
get := getResponse.List[0]
return UploadedBlobWithHash{
BlobId: upload.Id,
Size: upload.Size,
Type: upload.Type,
Sha512: get.DigestSha512,
}, getResponse.State, nil
})
}
-75
View File
@@ -1,75 +0,0 @@
package jmap
import (
"github.com/opencloud-eu/opencloud/pkg/structs"
)
type AccountBootstrapResult struct {
Identities []Identity `json:"identities,omitempty"`
Quotas []Quota `json:"quotas,omitempty"`
}
var NS_MAIL_QUOTA = ns(JmapMail, JmapQuota)
func (j *Client) GetBootstrap(accountIds []AccountId, ctx Context) (Result[map[AccountId]AccountBootstrapResult], error) { //NOSONAR
uniqueAccountIds := structs.Uniq(accountIds)
logger := j.logger("GetBootstrap", ctx)
ctx = ctx.WithLogger(logger)
calls := make([]Invocation, len(uniqueAccountIds)*2)
for i, accountId := range uniqueAccountIds {
calls[i*2+0] = invocation(IdentityGetCommand{AccountId: accountId}, mcid(accountId, "I"))
calls[i*2+1] = invocation(QuotaGetCommand{AccountId: accountId}, mcid(accountId, "Q"))
}
cmd, err := j.request(ctx, NS_MAIL_QUOTA, calls...)
if err != nil {
return ZeroResultV[map[AccountId]AccountBootstrapResult](), err
}
return command(j, Operation("GetBootstrap"), ctx, cmd, func(body *Response) (map[AccountId]AccountBootstrapResult, State, Error) {
identityPerAccount := map[AccountId][]Identity{}
quotaPerAccount := map[AccountId][]Quota{}
identityStatesPerAccount := map[AccountId]State{}
quotaStatesPerAccount := map[AccountId]State{}
for _, accountId := range uniqueAccountIds {
var identityResponse IdentityGetResponse
err = retrieveResponseMatchParameters(ctx, body, CommandIdentityGet, mcid(accountId, "I"), &identityResponse)
if err != nil {
return nil, "", err
} else {
identityPerAccount[accountId] = identityResponse.List
identityStatesPerAccount[accountId] = identityResponse.State
}
var quotaResponse QuotaGetResponse
err = retrieveResponseMatchParameters(ctx, body, CommandQuotaGet, mcid(accountId, "Q"), &quotaResponse)
if err != nil {
return nil, "", err
} else {
quotaPerAccount[accountId] = quotaResponse.List
quotaStatesPerAccount[accountId] = quotaResponse.State
}
}
result := map[AccountId]AccountBootstrapResult{}
for accountId, value := range identityPerAccount {
r, ok := result[accountId]
if !ok {
r = AccountBootstrapResult{}
}
r.Identities = value
result[accountId] = r
}
for accountId, value := range quotaPerAccount {
r, ok := result[accountId]
if !ok {
r = AccountBootstrapResult{}
}
r.Quotas = value
result[accountId] = r
}
return result, squashStateMaps(identityStatesPerAccount, quotaStatesPerAccount), nil
})
}
-125
View File
@@ -1,125 +0,0 @@
package jmap
var NS_CALENDARS = ns(JmapCalendars)
func (j *Client) ParseICalendarBlob(accountId AccountId, blobIds []string, ctx Context) (Result[CalendarEventParseResponse], error) {
logger := j.logger("ParseICalendarBlob", ctx)
parse := CalendarEventParseCommand{AccountId: accountId, BlobIds: blobIds}
cmd, err := j.request(ctx.WithLogger(logger), NS_CALENDARS,
invocation(parse, "0"),
)
if err != nil {
return ZeroResultV[CalendarEventParseResponse](), err
}
return command(j, Operation("ParseICalendarBlob"), ctx, cmd, func(body *Response) (CalendarEventParseResponse, State, Error) {
var response CalendarEventParseResponse
err = retrieveParse(ctx, body, parse, "0", &response)
if err != nil {
return CalendarEventParseResponse{}, "", err
}
return response, "", nil
})
}
func (j *Client) GetCalendars(accountId AccountId, ids []string, ctx Context) (Result[CalendarGetResponse], error) {
return get(j, "GetCalendars", CalendarType,
func(accountId AccountId, ids []string) CalendarGetCommand {
return CalendarGetCommand{AccountId: accountId, Ids: ids}
},
CalendarGetResponse{},
identity1,
accountId, ids,
ctx,
)
}
type CalendarChanges ChangesTemplate[Calendar]
var _ Changes[Calendar] = CalendarChanges{}
func (c CalendarChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c CalendarChanges) GetOldState() State { return c.OldState }
func (c CalendarChanges) GetNewState() State { return c.NewState }
func (c CalendarChanges) GetCreated() []Calendar { return c.Created }
func (c CalendarChanges) GetUpdated() []Calendar { return c.Updated }
func (c CalendarChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve Calendar changes since a given state.
// @apidoc calendar,changes
func (j *Client) GetCalendarChanges(accountId AccountId, sinceState State, maxChanges uint, ctx Context) (Result[CalendarChanges], error) {
return changes(j, "GetCalendarChanges", CalendarType,
func() CalendarChangesCommand {
return CalendarChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
CalendarChangesResponse{},
func(path string, rof string) CalendarGetRefCommand {
return CalendarGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandCalendarChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp CalendarGetResponse) []Calendar { return resp.List },
func(oldState, newState State, hasMoreChanges bool, created, updated []Calendar, destroyed []string) CalendarChanges {
return CalendarChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
ctx,
)
}
func (j *Client) CreateCalendar(accountId AccountId, calendar CalendarChange, ctx Context) (Result[*Calendar], error) {
return create(j, "CreateCalendar", CalendarEventType,
func(accountId AccountId, create map[string]CalendarChange) CalendarSetCommand {
return CalendarSetCommand{AccountId: accountId, Create: create}
},
func(accountId AccountId, ref string) CalendarGetCommand {
return CalendarGetCommand{AccountId: accountId, Ids: []string{ref}}
},
func(resp CalendarSetResponse) map[string]*Calendar {
return resp.Created
},
func(resp CalendarGetResponse) []Calendar {
return resp.List
},
accountId, calendar,
ctx,
)
}
func (j *Client) DeleteCalendar(accountId AccountId, destroyIds []string, ctx Context) (Result[map[string]SetError], error) {
return destroy(j, "DeleteCalendar", CalendarEventType,
func(accountId AccountId, destroy []string) CalendarSetCommand {
return CalendarSetCommand{AccountId: accountId, Destroy: destroy}
},
CalendarSetResponse{},
accountId, destroyIds,
ctx,
)
}
func (j *Client) UpdateCalendar(accountId AccountId, id string, changes CalendarChange, ctx Context) (Result[Calendar], error) {
return update(j, "UpdateCalendar", CalendarEventType,
func(update map[string]PatchObject) CalendarSetCommand {
return CalendarSetCommand{AccountId: accountId, Update: update}
},
func(id string) CalendarGetCommand {
return CalendarGetCommand{AccountId: accountId, Ids: []string{id}}
},
func(resp CalendarSetResponse) map[string]SetError { return resp.NotUpdated },
func(resp CalendarGetResponse) Calendar { return resp.List[0] },
id, changes,
ctx,
)
}
-195
View File
@@ -1,195 +0,0 @@
package jmap
import (
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/rs/zerolog"
)
// Note that Quota/changes is currently not supported in Stalwart, as it always gives a
// cannotCalculateChanges error back.
var NS_CHANGES = ns(JmapMail, JmapContacts, JmapCalendars) //, JmapQuota)
type ObjectChanges struct {
MaxChanges uint `json:"maxchanges,omitzero"`
Mailboxes *MailboxChangesResponse `json:"mailboxes,omitempty"`
Emails *EmailChangesResponse `json:"emails,omitempty"`
Calendars *CalendarChangesResponse `json:"calendars,omitempty"`
Events *CalendarEventChangesResponse `json:"events,omitempty"`
Addressbooks *AddressBookChangesResponse `json:"addressbooks,omitempty"`
Contacts *ContactCardChangesResponse `json:"contacts,omitempty"`
Identities *IdentityChangesResponse `json:"identities,omitempty"`
EmailSubmissions *EmailSubmissionChangesResponse `json:"submissions,omitempty"`
// Quotas *QuotaChangesResponse `json:"quotas,omitempty"`
}
type StateMap struct {
Mailboxes *State `json:"mailboxes,omitempty"`
Emails *State `json:"emails,omitempty"`
Calendars *State `json:"calendars,omitempty"`
Events *State `json:"events,omitempty"`
Addressbooks *State `json:"addressbooks,omitempty"`
Contacts *State `json:"contacts,omitempty"`
Identities *State `json:"identities,omitempty"`
EmailSubmissions *State `json:"submissions,omitempty"`
// Quotas *State `json:"quotas,omitempty"`
}
var _ zerolog.LogObjectMarshaler = StateMap{}
func (s StateMap) IsZero() bool {
return s.Mailboxes == nil && s.Emails == nil && s.Calendars == nil &&
s.Events == nil && s.Addressbooks == nil && s.Contacts == nil &&
s.Identities == nil && s.EmailSubmissions == nil
//s.Quotas == nil
}
func (s StateMap) MarshalZerologObject(e *zerolog.Event) {
if s.Mailboxes != nil {
e.Str("mailboxes", string(*s.Mailboxes))
}
if s.Emails != nil {
e.Str("emails", string(*s.Emails))
}
if s.Calendars != nil {
e.Str("calendars", string(*s.Calendars))
}
if s.Events != nil {
e.Str("events", string(*s.Events))
}
if s.Addressbooks != nil {
e.Str("addressbooks", string(*s.Addressbooks))
}
if s.Contacts != nil {
e.Str("contacts", string(*s.Contacts))
}
if s.Identities != nil {
e.Str("identities", string(*s.Identities))
}
if s.EmailSubmissions != nil {
e.Str("submissions", string(*s.EmailSubmissions))
}
// if s.Quotas != nil { e.Str("quotas", string(*s.Quotas)) }
}
// Retrieve the changes in any type of objects at once since a given State.
// @api:tags changes
func (j *Client) GetChanges(accountId AccountId, stateMap StateMap, maxChanges uint, ctx Context) (Result[ObjectChanges], error) { //NOSONAR
logger := log.From(j.logger("GetChanges", ctx).With().Object("state", stateMap).Uint("maxChanges", maxChanges))
ctx = ctx.WithLogger(logger)
methodCalls := []Invocation{}
if stateMap.Mailboxes != nil {
methodCalls = append(methodCalls, invocation(MailboxChangesCommand{AccountId: accountId, SinceState: *stateMap.Mailboxes, MaxChanges: uintPtr(maxChanges)}, "mailboxes"))
}
if stateMap.Emails != nil {
methodCalls = append(methodCalls, invocation(EmailChangesCommand{AccountId: accountId, SinceState: *stateMap.Emails, MaxChanges: uintPtr(maxChanges)}, "emails"))
}
if stateMap.Calendars != nil {
methodCalls = append(methodCalls, invocation(CalendarChangesCommand{AccountId: accountId, SinceState: *stateMap.Calendars, MaxChanges: uintPtr(maxChanges)}, "calendars"))
}
if stateMap.Events != nil {
methodCalls = append(methodCalls, invocation(CalendarEventChangesCommand{AccountId: accountId, SinceState: *stateMap.Events, MaxChanges: uintPtr(maxChanges)}, "events"))
}
if stateMap.Addressbooks != nil {
methodCalls = append(methodCalls, invocation(AddressBookChangesCommand{AccountId: accountId, SinceState: *stateMap.Addressbooks, MaxChanges: uintPtr(maxChanges)}, "addressbooks"))
}
if stateMap.Contacts != nil {
methodCalls = append(methodCalls, invocation(ContactCardChangesCommand{AccountId: accountId, SinceState: *stateMap.Contacts, MaxChanges: uintPtr(maxChanges)}, "contacts"))
}
if stateMap.Identities != nil {
methodCalls = append(methodCalls, invocation(IdentityChangesCommand{AccountId: accountId, SinceState: *stateMap.Identities, MaxChanges: uintPtr(maxChanges)}, "identities"))
}
if stateMap.EmailSubmissions != nil {
methodCalls = append(methodCalls, invocation(EmailSubmissionChangesCommand{AccountId: accountId, SinceState: *stateMap.EmailSubmissions, MaxChanges: uintPtr(maxChanges)}, "submissions"))
}
// if stateMap.Quotas != nil { methodCalls = append(methodCalls, invocation(CommandQuotaChanges, QuotaChangesCommand{AccountId: accountId, SinceState: *stateMap.Quotas, MaxChanges: posUIntPtr(maxChanges)}, "quotas")) }
cmd, err := j.request(ctx, NS_CHANGES, methodCalls...)
if err != nil {
return ZeroResultV[ObjectChanges](), err
}
return command(j, Operation("GetChanges"), ctx, cmd, func(body *Response) (ObjectChanges, State, Error) {
changes := ObjectChanges{
MaxChanges: maxChanges,
}
states := map[string]State{}
var mailboxes MailboxChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandMailboxChanges, "mailboxes", &mailboxes); err != nil {
return ObjectChanges{}, "", err
} else if ok {
changes.Mailboxes = &mailboxes
states["mailbox"] = mailboxes.NewState
}
var emails EmailChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandEmailChanges, "emails", &emails); err != nil {
return ObjectChanges{}, "", err
} else if ok {
changes.Emails = &emails
states["emails"] = emails.NewState
}
var calendars CalendarChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandCalendarChanges, "calendars", &calendars); err != nil {
return ObjectChanges{}, "", err
} else if ok {
changes.Calendars = &calendars
states["calendars"] = calendars.NewState
}
var events CalendarEventChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandCalendarEventChanges, "events", &events); err != nil {
return ObjectChanges{}, "", err
} else if ok {
changes.Events = &events
states["events"] = events.NewState
}
var addressbooks AddressBookChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandAddressBookChanges, "addressbooks", &addressbooks); err != nil {
return ObjectChanges{}, "", err
} else if ok {
changes.Addressbooks = &addressbooks
states["addressbooks"] = addressbooks.NewState
}
var contacts ContactCardChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandContactCardChanges, "contacts", &contacts); err != nil {
return ObjectChanges{}, "", err
} else if ok {
changes.Contacts = &contacts
states["contacts"] = contacts.NewState
}
var identities IdentityChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandIdentityChanges, "identities", &identities); err != nil {
return ObjectChanges{}, "", err
} else if ok {
changes.Identities = &identities
states["identities"] = identities.NewState
}
var submissions EmailSubmissionChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandEmailSubmissionChanges, "submissions", &submissions); err != nil {
return ObjectChanges{}, "", err
} else if ok {
changes.EmailSubmissions = &submissions
states["submissions"] = submissions.NewState
}
/*
var quotas QuotaChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandQuotaChanges, "quotas", &quotas); err != nil {
return Changes{}, "", err
} else if ok {
changes.Quotas = &quotas
states["quotas"] = quotas.NewState
}
*/
return changes, squashKeyedStates(states), nil
})
}
-166
View File
@@ -1,166 +0,0 @@
package jmap
import "github.com/opencloud-eu/opencloud/pkg/jscontact"
var NS_CONTACTS = ns(JmapContacts)
var DEFAULT_CONTACT_CARD_VERSION = jscontact.JSContactVersion_1_0
func (j *Client) GetContactCards(accountId AccountId, contactIds []string, ctx Context) (Result[ContactCardGetResponse], error) {
return get(j, "GetContactCards", ContactCardType,
func(accountId AccountId, ids []string) ContactCardGetCommand {
return ContactCardGetCommand{AccountId: accountId, Ids: contactIds}
},
ContactCardGetResponse{},
identity1,
accountId, contactIds,
ctx,
)
}
type ContactCardChanges ChangesTemplate[ContactCard]
var _ Changes[ContactCard] = ContactCardChanges{}
func (c ContactCardChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c ContactCardChanges) GetOldState() State { return c.OldState }
func (c ContactCardChanges) GetNewState() State { return c.NewState }
func (c ContactCardChanges) GetCreated() []ContactCard { return c.Created }
func (c ContactCardChanges) GetUpdated() []ContactCard { return c.Updated }
func (c ContactCardChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Contact Cards since a given State.
// @api:tags contact,changes
func (j *Client) GetContactCardChanges(accountId AccountId, sinceState State, maxChanges uint, ctx Context) (Result[ContactCardChanges], error) {
return changes(j, "GetContactCardChanges", ContactCardType,
func() ContactCardChangesCommand {
return ContactCardChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
ContactCardChangesResponse{},
func(path string, rof string) ContactCardGetRefCommand {
return ContactCardGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandContactCardChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp ContactCardGetResponse) []ContactCard { return resp.List },
func(oldState, newState State, hasMoreChanges bool, created, updated []ContactCard, destroyed []string) ContactCardChanges {
return ContactCardChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
ctx,
)
}
type ContactCardSearchResults SearchResultsTemplate[ContactCard]
var _ SearchResults[ContactCard] = &ContactCardSearchResults{}
func (r *ContactCardSearchResults) GetResults() []ContactCard { return r.Results }
func (r *ContactCardSearchResults) GetCanCalculateChanges() ChangeCalculation {
return r.CanCalculateChanges
}
func (r *ContactCardSearchResults) GetPosition() *uint { return r.Position }
func (r *ContactCardSearchResults) GetLimit() *uint { return r.Limit }
func (r *ContactCardSearchResults) GetTotal() *uint { return r.Total }
func (r *ContactCardSearchResults) RemoveResults() { r.Results = nil }
func (r *ContactCardSearchResults) SetLimit(limit *uint) { r.Limit = limit }
func (r *ContactCardSearchResults) SetPosition(position *uint) { r.Position = position }
func (j *Client) QueryContactCards(accountIds map[AccountId]QueryParams, //NOSONAR
limit *uint, filter ContactCardFilterElement, sortBy []ContactCardComparator, calculateTotal bool,
ctx Context) (Result[map[AccountId]*ContactCardSearchResults], error) {
return queryN(j, "QueryContactCards", ContactCardType,
[]ContactCardComparator{{Property: ContactCardPropertyUpdated, IsAscending: false}},
func(accountId AccountId, qp QueryParams, limit *uint, filter ContactCardFilterElement, sortBy []ContactCardComparator) ContactCardQueryCommand {
if qp.Anchor != "" {
return ContactCardQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Anchor: qp.Anchor, AnchorOffset: qp.AnchorOffset, Limit: limit, CalculateTotal: calculateTotal}
} else {
return ContactCardQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: qp.Position, Limit: limit, CalculateTotal: calculateTotal}
}
},
func(accountId AccountId, cmd Command, path string, rof string) ContactCardGetRefCommand {
return ContactCardGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
},
func(query ContactCardQueryResponse, queryParams QueryParams, limit *uint) *ContactCardSearchResults {
return &ContactCardSearchResults{
Results: []ContactCard{},
CanCalculateChanges: ChangeCalculation(query.CanCalculateChanges),
Position: valueIf(query.Position, queryParams.Anchor == ""),
Total: valueIf(query.Total, calculateTotal),
Limit: valueIf(query.Limit, limit != nil),
}
},
func(query ContactCardQueryResponse, get ContactCardGetResponse, queryParams QueryParams, limit *uint) *ContactCardSearchResults {
return &ContactCardSearchResults{
Results: get.List,
CanCalculateChanges: ChangeCalculation(query.CanCalculateChanges),
Position: valueIf(query.Position, queryParams.Anchor == ""),
Total: valueIf(query.Total, calculateTotal),
Limit: valueIf(query.Limit, limit != nil),
}
},
accountIds, limit,
filter, sortBy, ctx,
)
}
// @api:example create
func (j *Client) CreateContactCard(accountId AccountId, contact ContactCardChange, ctx Context) (Result[*ContactCard], error) {
if contact.Version == nil {
contact.Version = &DEFAULT_CONTACT_CARD_VERSION
}
return create(j, "CreateContactCard", ContactCardType,
func(accountId AccountId, create map[string]ContactCardChange) ContactCardSetCommand {
return ContactCardSetCommand{AccountId: accountId, Create: create}
},
func(accountId AccountId, ids string) ContactCardGetCommand {
return ContactCardGetCommand{AccountId: accountId, Ids: []string{ids}}
},
func(resp ContactCardSetResponse) map[string]*ContactCard {
return resp.Created
},
func(resp ContactCardGetResponse) []ContactCard {
return resp.List
},
accountId, contact,
ctx,
)
}
func (j *Client) DeleteContactCard(accountId AccountId, destroyIds []string, ctx Context) (Result[map[string]SetError], error) {
return destroy(j, "DeleteContactCard", ContactCardType,
func(accountId AccountId, destroy []string) ContactCardSetCommand {
return ContactCardSetCommand{AccountId: accountId, Destroy: destroy}
},
ContactCardSetResponse{},
accountId, destroyIds,
ctx,
)
}
// @api:example update
func (j *Client) UpdateContactCard(accountId AccountId, id string, changes ContactCardChange, ctx Context) (Result[ContactCard], error) {
return update(j, "UpdateContactCard", ContactCardType,
func(update map[string]PatchObject) ContactCardSetCommand {
return ContactCardSetCommand{AccountId: accountId, Update: update}
},
func(id string) ContactCardGetCommand {
return ContactCardGetCommand{AccountId: accountId, Ids: []string{id}}
},
func(resp ContactCardSetResponse) map[string]SetError { return resp.NotUpdated },
func(resp ContactCardGetResponse) ContactCard { return resp.List[0] },
id, changes,
ctx,
)
}
File diff suppressed because it is too large. Load diff
-154
View File
@@ -1,154 +0,0 @@
package jmap
var NS_CALENDAR_EVENTS = ns(JmapCalendars)
type CalendarEventSearchResults SearchResultsTemplate[CalendarEvent]
var _ SearchResults[CalendarEvent] = &CalendarEventSearchResults{}
func (r *CalendarEventSearchResults) GetResults() []CalendarEvent { return r.Results }
func (r *CalendarEventSearchResults) GetCanCalculateChanges() ChangeCalculation {
return r.CanCalculateChanges
}
func (r *CalendarEventSearchResults) GetPosition() *uint { return r.Position }
func (r *CalendarEventSearchResults) GetLimit() *uint { return r.Limit }
func (r *CalendarEventSearchResults) GetTotal() *uint { return r.Total }
func (r *CalendarEventSearchResults) RemoveResults() { r.Results = nil }
func (r *CalendarEventSearchResults) SetLimit(limit *uint) { r.Limit = limit }
func (r *CalendarEventSearchResults) SetPosition(position *uint) { r.Position = position }
func (j *Client) GetCalendarEvents(accountId AccountId, eventIds []string, ctx Context) (Result[CalendarEventGetResponse], error) {
return get(j, "GetCalendarEvents", CalendarEventType,
func(accountId AccountId, ids []string) CalendarEventGetCommand {
return CalendarEventGetCommand{AccountId: accountId, Ids: eventIds}
},
CalendarEventGetResponse{},
identity1,
accountId, eventIds,
ctx,
)
}
func (j *Client) QueryCalendarEvents(accountIds map[AccountId]QueryParams, limit *uint, //NOSONAR
filter CalendarEventFilterElement, sortBy []CalendarEventComparator, calculateTotal bool,
ctx Context) (Result[map[AccountId]*CalendarEventSearchResults], error) {
return queryN(j, "QueryCalendarEvents", CalendarEventType,
[]CalendarEventComparator{{Property: CalendarEventPropertyStart, IsAscending: false}},
func(accountId AccountId, queryParams QueryParams, limit *uint, filter CalendarEventFilterElement, sortBy []CalendarEventComparator) CalendarEventQueryCommand {
return CalendarEventQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: queryParams.Position, Anchor: queryParams.Anchor, AnchorOffset: queryParams.AnchorOffset, Limit: limit, CalculateTotal: calculateTotal}
},
func(accountId AccountId, cmd Command, path string, rof string) CalendarEventGetRefCommand {
return CalendarEventGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
},
func(query CalendarEventQueryResponse, queryParams QueryParams, limit *uint) *CalendarEventSearchResults {
return &CalendarEventSearchResults{
Results: []CalendarEvent{},
CanCalculateChanges: ChangeCalculation(query.CanCalculateChanges),
Position: valueIf(query.Position, queryParams.Anchor == ""),
Total: valueIf(query.Total, calculateTotal),
Limit: valueIf(query.Limit, limit != nil),
}
},
func(query CalendarEventQueryResponse, get CalendarEventGetResponse, queryParams QueryParams, limit *uint) *CalendarEventSearchResults {
return &CalendarEventSearchResults{
Results: get.List,
CanCalculateChanges: ChangeCalculation(query.CanCalculateChanges),
Position: valueIf(query.Position, queryParams.Anchor == ""),
Total: valueIf(query.Total, calculateTotal),
Limit: valueIf(query.Limit, limit != nil),
}
},
accountIds, limit,
filter, sortBy, ctx,
)
}
type CalendarEventChanges ChangesTemplate[CalendarEvent]
var _ Changes[CalendarEvent] = CalendarEventChanges{}
func (c CalendarEventChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c CalendarEventChanges) GetOldState() State { return c.OldState }
func (c CalendarEventChanges) GetNewState() State { return c.NewState }
func (c CalendarEventChanges) GetCreated() []CalendarEvent { return c.Created }
func (c CalendarEventChanges) GetUpdated() []CalendarEvent { return c.Updated }
func (c CalendarEventChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Calendar Events since a given State.
// @api:tags event,changes
func (j *Client) GetCalendarEventChanges(accountId AccountId, sinceState State, maxChanges uint,
ctx Context) (Result[CalendarEventChanges], error) {
return changes(j, "GetCalendarEventChanges", CalendarEventType,
func() CalendarEventChangesCommand {
return CalendarEventChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
CalendarEventChangesResponse{},
func(path string, rof string) CalendarEventGetRefCommand {
return CalendarEventGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandCalendarEventChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp CalendarEventGetResponse) []CalendarEvent { return resp.List },
func(oldState, newState State, hasMoreChanges bool, created, updated []CalendarEvent, destroyed []string) CalendarEventChanges {
return CalendarEventChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
ctx,
)
}
func (j *Client) CreateCalendarEvent(accountId AccountId, event CalendarEventChange, ctx Context) (Result[*CalendarEvent], error) {
return create(j, "CreateCalendarEvent", CalendarEventType,
func(accountId AccountId, create map[string]CalendarEventChange) CalendarEventSetCommand {
return CalendarEventSetCommand{AccountId: accountId, Create: create}
},
func(accountId AccountId, ref string) CalendarEventGetCommand {
return CalendarEventGetCommand{AccountId: accountId, Ids: []string{ref}}
},
func(resp CalendarEventSetResponse) map[string]*CalendarEvent {
return resp.Created
},
func(resp CalendarEventGetResponse) []CalendarEvent {
return resp.List
},
accountId, event,
ctx,
)
}
func (j *Client) DeleteCalendarEvent(accountId AccountId, destroyIds []string, ctx Context) (Result[map[string]SetError], error) {
return destroy(j, "DeleteCalendarEvent", CalendarEventType,
func(accountId AccountId, destroy []string) CalendarEventSetCommand {
return CalendarEventSetCommand{AccountId: accountId, Destroy: destroy}
},
CalendarEventSetResponse{},
accountId, destroyIds,
ctx,
)
}
func (j *Client) UpdateCalendarEvent(accountId AccountId, id string, changes CalendarEventChange, ctx Context) (Result[CalendarEvent], error) {
return update(j, "UpdateCalendarEvent", CalendarEventType,
func(update map[string]PatchObject) CalendarEventSetCommand {
return CalendarEventSetCommand{AccountId: accountId, Update: update}
},
func(id string) CalendarEventGetCommand {
return CalendarEventGetCommand{AccountId: accountId, Ids: []string{id}}
},
func(resp CalendarEventSetResponse) map[string]SetError { return resp.NotUpdated },
func(resp CalendarEventGetResponse) CalendarEvent { return resp.List[0] },
id, changes,
ctx,
)
}
-176
View File
@@ -1,176 +0,0 @@
package jmap
import (
"strconv"
"github.com/opencloud-eu/opencloud/pkg/structs"
)
var NS_IDENTITY = ns(JmapMail)
func (j *Client) GetIdentities(accountId AccountId, identityIds []string, ctx Context) (Result[IdentityGetResponse], error) {
return get(j, "GetIdentities", IdentityType,
func(accountId AccountId, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId, Ids: ids}
},
IdentityGetResponse{},
identity1,
accountId, identityIds,
ctx,
)
}
func (j *Client) GetIdentitiesForAllAccounts(accountIds []AccountId, ctx Context) (Result[map[AccountId][]Identity], error) {
return getN(j, "GetIdentitiesForAllAccounts", IdentityType,
func(accountId AccountId, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId}
},
IdentityGetResponse{},
func(resp IdentityGetResponse) []Identity { return resp.List },
identity1,
accountIds, []string{},
ctx,
)
}
type IdentitiesAndMailboxesGetResponse struct {
Identities map[AccountId][]Identity `json:"identities,omitempty"`
NotFound []string `json:"notFound,omitempty"`
Mailboxes []Mailbox `json:"mailboxes"`
}
func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId AccountId, accountIds []AccountId, ctx Context) (Result[IdentitiesAndMailboxesGetResponse], error) {
uniqueAccountIds := structs.Uniq(accountIds)
logger := j.logger("GetIdentitiesAndMailboxes", ctx)
ctx = ctx.WithLogger(logger)
calls := make([]Invocation, len(uniqueAccountIds)+1)
calls[0] = invocation(MailboxGetCommand{AccountId: mailboxAccountId}, "0")
for i, accountId := range uniqueAccountIds {
calls[i+1] = invocation(IdentityGetCommand{AccountId: accountId}, strconv.Itoa(i+1))
}
cmd, err := j.request(ctx, NS_IDENTITY, calls...)
if err != nil {
return ZeroResultV[IdentitiesAndMailboxesGetResponse](), err
}
return command(j, Operation("GetIdentitiesAndMailboxes"), ctx, cmd, func(body *Response) (IdentitiesAndMailboxesGetResponse, State, Error) {
identities := make(map[AccountId][]Identity, len(uniqueAccountIds))
stateByAccountId := make(map[AccountId]State, len(uniqueAccountIds))
notFound := []string{}
for i, accountId := range uniqueAccountIds {
var response IdentityGetResponse
err = retrieveResponseMatchParameters(ctx, body, CommandIdentityGet, strconv.Itoa(i+1), &response)
if err != nil {
return IdentitiesAndMailboxesGetResponse{}, "", err
} else {
identities[accountId] = response.List
}
stateByAccountId[accountId] = response.State
notFound = append(notFound, response.NotFound...)
}
var mailboxResponse MailboxGetResponse
err = retrieveResponseMatchParameters(ctx, body, CommandMailboxGet, "0", &mailboxResponse)
if err != nil {
return IdentitiesAndMailboxesGetResponse{}, "", err
}
return IdentitiesAndMailboxesGetResponse{
Identities: identities,
NotFound: structs.Uniq(notFound),
Mailboxes: mailboxResponse.List,
}, squashState(stateByAccountId), nil
})
}
func (j *Client) CreateIdentity(accountId AccountId, identity IdentityChange, ctx Context) (Result[*Identity], error) {
return create(j, "CreateIdentity", IdentityType,
func(accountId AccountId, create map[string]IdentityChange) IdentitySetCommand {
return IdentitySetCommand{AccountId: accountId, Create: create}
},
func(accountId AccountId, ids string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId, Ids: []string{ids}}
},
func(resp IdentitySetResponse) map[string]*Identity {
return resp.Created
},
func(resp IdentityGetResponse) []Identity {
return resp.List
},
accountId, identity,
ctx,
)
}
func (j *Client) UpdateIdentity(accountId AccountId, id string, changes IdentityChange, ctx Context) (Result[Identity], error) {
return update(j, "UpdateIdentity", IdentityType,
func(update map[string]PatchObject) IdentitySetCommand {
return IdentitySetCommand{AccountId: accountId, Update: update}
},
func(id string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId, Ids: []string{id}}
},
func(resp IdentitySetResponse) map[string]SetError { return resp.NotUpdated },
func(resp IdentityGetResponse) Identity { return resp.List[0] },
id, changes,
ctx,
)
}
func (j *Client) DeleteIdentity(accountId AccountId, destroyIds []string, ctx Context) (Result[map[string]SetError], error) {
return destroy(j, "DeleteIdentity", IdentityType,
func(accountId AccountId, destroy []string) IdentitySetCommand {
return IdentitySetCommand{AccountId: accountId, Destroy: destroyIds}
},
IdentitySetResponse{},
accountId, destroyIds,
ctx,
)
}
type IdentityChanges ChangesTemplate[Identity]
var _ Changes[Identity] = IdentityChanges{}
func (c IdentityChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c IdentityChanges) GetOldState() State { return c.OldState }
func (c IdentityChanges) GetNewState() State { return c.NewState }
func (c IdentityChanges) GetCreated() []Identity { return c.Created }
func (c IdentityChanges) GetUpdated() []Identity { return c.Updated }
func (c IdentityChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Email Identities since a given State.
// @api:tags email,changes
func (j *Client) GetIdentityChanges(accountId AccountId, sinceState State, maxChanges uint,
ctx Context) (Result[IdentityChanges], error) {
return changes(j, "GetIdentityChanges", IdentityType,
func() IdentityChangesCommand {
return IdentityChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
IdentityChangesResponse{},
func(path string, rof string) IdentityGetRefCommand {
return IdentityGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandIdentityChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp IdentityGetResponse) []Identity { return resp.List },
func(oldState, newState State, hasMoreChanges bool, created, updated []Identity, destroyed []string) IdentityChanges {
return IdentityChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
ctx,
)
}
-300
View File
@@ -1,300 +0,0 @@
package jmap
import (
"slices"
"github.com/opencloud-eu/opencloud/pkg/structs"
)
var NS_MAILBOX = ns(JmapMail)
func (j *Client) GetMailbox(accountId AccountId, ids []string, ctx Context) (Result[MailboxGetResponse], error) {
return get(j, "GetMailbox", MailboxType,
func(accountId AccountId, ids []string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId, Ids: ids}
},
MailboxGetResponse{},
identity1,
accountId, ids,
ctx,
)
}
func (j *Client) GetAllMailboxes(accountIds []AccountId, ctx Context) (Result[map[AccountId][]Mailbox], error) {
return getAN(j, "GetAllMailboxes", MailboxType,
func(accountId AccountId, ids []string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId}
},
MailboxGetResponse{},
identity1,
accountIds, []string{},
ctx,
)
}
func (j *Client) SearchMailboxes(accountIds []AccountId, filter MailboxFilterElement, ctx Context) (Result[map[AccountId][]Mailbox], error) {
logger := j.logger("SearchMailboxes", ctx)
ctx = ctx.WithLogger(logger)
uniqueAccountIds := structs.Uniq(accountIds)
invocations := make([]Invocation, len(uniqueAccountIds)*2)
for i, accountId := range uniqueAccountIds {
invocations[i*2+0] = invocation(&MailboxQueryCommand{AccountId: accountId, Filter: filter}, mcid(accountId, "0"))
invocations[i*2+1] = invocation(MailboxGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandMailboxQuery,
Path: "/ids/*",
ResultOf: mcid(accountId, "0"),
},
}, mcid(accountId, "1"))
}
cmd, err := j.request(ctx, NS_MAILBOX, invocations...)
if err != nil {
return ZeroResultV[map[AccountId][]Mailbox](), err
}
return command(j, Operation("SearchMailboxes"), ctx, cmd, func(body *Response) (map[AccountId][]Mailbox, State, Error) {
resp := map[AccountId][]Mailbox{}
stateByAccountid := map[AccountId]State{}
for _, accountId := range uniqueAccountIds {
var response MailboxGetResponse
err = retrieveResponseMatchParameters(ctx, body, CommandMailboxGet, mcid(accountId, "1"), &response)
if err != nil {
return nil, "", err
}
resp[accountId] = response.List
stateByAccountid[accountId] = response.State
}
return resp, squashState(stateByAccountid), nil
})
}
func (j *Client) SearchMailboxIdsPerRole(accountIds []AccountId, roles []string, ctx Context) (Result[map[AccountId]map[string]string], error) { //NOSONAR
logger := j.logger("SearchMailboxIdsPerRole", ctx)
ctx = ctx.WithLogger(logger)
uniqueAccountIds := structs.Uniq(accountIds)
invocations := make([]Invocation, len(uniqueAccountIds)*len(roles))
for i, accountId := range uniqueAccountIds {
for j, role := range roles {
invocations[i*len(roles)+j] = invocation(&MailboxQueryCommand{AccountId: accountId, Filter: MailboxFilterCondition{Role: role}}, mcid(accountId, role))
}
}
cmd, err := j.request(ctx, NS_MAILBOX, invocations...)
if err != nil {
return ZeroResultV[map[AccountId]map[string]string](), err
}
return command(j, Operation("SearchMailboxIdsPerRole"), ctx, cmd, func(body *Response) (map[AccountId]map[string]string, State, Error) {
resp := map[AccountId]map[string]string{}
stateByAccountid := map[AccountId]State{}
for _, accountId := range uniqueAccountIds {
mailboxIdsByRole := map[string]string{}
for _, role := range roles {
var response MailboxQueryResponse
err = retrieveResponseMatchParameters(ctx, body, CommandMailboxQuery, mcid(accountId, role), &response)
if err != nil {
return nil, "", err
}
if len(response.Ids) == 1 {
mailboxIdsByRole[role] = response.Ids[0]
}
if _, ok := stateByAccountid[accountId]; !ok {
stateByAccountid[accountId] = response.QueryState
}
}
resp[accountId] = mailboxIdsByRole
}
return resp, squashState(stateByAccountid), nil
})
}
type MailboxChanges ChangesTemplate[Mailbox]
var _ Changes[Mailbox] = MailboxChanges{}
func (c MailboxChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c MailboxChanges) GetOldState() State { return c.OldState }
func (c MailboxChanges) GetNewState() State { return c.NewState }
func (c MailboxChanges) GetCreated() []Mailbox { return c.Created }
func (c MailboxChanges) GetUpdated() []Mailbox { return c.Updated }
func (c MailboxChanges) GetDestroyed() []string { return c.Destroyed }
func newMailboxChanges(oldState, newState State, hasMoreChanges bool, created, updated []Mailbox, destroyed []string) MailboxChanges {
return MailboxChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
}
// Retrieve Mailbox changes since a given state.
// @apidoc mailboxes,changes
func (j *Client) GetMailboxChanges(accountId AccountId, sinceState State, maxChanges uint, ctx Context) (Result[MailboxChanges], error) {
return changesA(j, "GetMailboxChanges", MailboxType,
func() MailboxChangesCommand {
return MailboxChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
MailboxChangesResponse{},
MailboxGetResponse{},
func(path string, rof string) MailboxGetRefCommand {
return MailboxGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandMailboxChanges,
Path: path,
ResultOf: rof,
},
}
},
newMailboxChanges,
ctx,
)
}
// Retrieve Mailbox changes of multiple Accounts.
// @api:tags email,changes
func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []AccountId, //NOSONAR
sinceStateMap map[AccountId]State, maxChanges uint,
ctx Context) (Result[map[AccountId]MailboxChanges], error) {
return changesN(j, "GetMailboxChangesForMultipleAccounts", MailboxType,
accountIds, sinceStateMap,
func(accountId AccountId, state State) MailboxChangesCommand {
return MailboxChangesCommand{AccountId: accountId, SinceState: state, MaxChanges: uintPtr(maxChanges)}
},
MailboxChangesResponse{},
func(accountId AccountId, path string, ref string) MailboxGetRefCommand {
return MailboxGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: path, ResultOf: ref}}
},
func(resp MailboxGetResponse) []Mailbox { return resp.List },
newMailboxChanges,
identity1,
func(resp MailboxGetResponse) State { return resp.State },
ctx,
)
}
func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []AccountId, ctx Context) (Result[map[AccountId]*[]string], error) {
return queryN(j, "GetMailboxRolesForMultipleAccounts", MailboxType,
[]MailboxComparator{{Property: MailboxPropertySortOrder, IsAscending: true}},
func(accountId AccountId, _ QueryParams, _ *uint, filter MailboxFilterCondition, sortBy []MailboxComparator) MailboxQueryCommand {
return MailboxQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, SortAsTree: false, FilterAsTree: false, Position: 0, Anchor: "", AnchorOffset: nil, Limit: nil, CalculateTotal: false}
},
func(accountId AccountId, cmd Command, path, rof string) MailboxGetRefCommand {
return MailboxGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
},
func(_ MailboxQueryResponse, _ QueryParams, _ *uint) *[]string {
return nil // TODO this should never be called
},
func(_ MailboxQueryResponse, get MailboxGetResponse, _ QueryParams, _ *uint) *[]string {
roles := structs.Map(get.List, func(m Mailbox) string { return m.Role })
slices.Sort(roles)
return &roles
},
ToNullQueryParams(accountIds), nil, MailboxFilterCondition{HasAnyRole: truep}, nil,
ctx,
)
}
func (j *Client) GetInboxNameForMultipleAccounts(accountIds []AccountId, ctx Context) (Result[map[AccountId]string], error) {
logger := j.logger("GetInboxNameForMultipleAccounts", ctx)
ctx = ctx.WithLogger(logger)
uniqueAccountIds := structs.Uniq(accountIds)
n := len(uniqueAccountIds)
if n < 1 {
return ZeroResultV[map[AccountId]string](), nil
}
invocations := make([]Invocation, n*2)
for i, accountId := range uniqueAccountIds {
invocations[i*2+0] = invocation(&MailboxQueryCommand{
AccountId: accountId,
Filter: MailboxFilterCondition{
Role: JmapMailboxRoleInbox,
},
}, mcid(accountId, "0"))
}
cmd, err := j.request(ctx, NS_MAILBOX, invocations...)
if err != nil {
return ZeroResultV[map[AccountId]string](), err
}
return command(j, Operation("GetInboxNameForMultipleAccounts"), ctx, cmd, func(body *Response) (map[AccountId]string, State, Error) {
resp := make(map[AccountId]string, n)
stateByAccountId := make(map[AccountId]State, n)
for _, accountId := range uniqueAccountIds {
var r MailboxQueryResponse
err = retrieveResponseMatchParameters(ctx, body, CommandMailboxGet, mcid(accountId, "0"), &r)
if err != nil {
return nil, "", err
}
switch len(r.Ids) {
case 0:
// skip: account has no inbox?
case 1:
resp[accountId] = r.Ids[0]
stateByAccountId[accountId] = r.QueryState
default:
logger.Warn().Msgf("multiple ids for mailbox role='%v' for accountId='%v'", JmapMailboxRoleInbox, accountId)
resp[accountId] = r.Ids[0]
stateByAccountId[accountId] = r.QueryState
}
}
return resp, squashState(stateByAccountId), nil
})
}
func (j *Client) UpdateMailbox(accountId AccountId, mailboxId string, change MailboxChange, //NOSONAR
ctx Context) (Result[Mailbox], error) {
return update(j, "UpdateMailbox", MailboxType,
func(update map[string]PatchObject) MailboxSetCommand {
return MailboxSetCommand{AccountId: accountId, Update: update}
},
func(id string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId, Ids: []string{id}}
},
func(resp MailboxSetResponse) map[string]SetError { return resp.NotUpdated },
func(resp MailboxGetResponse) Mailbox { return resp.List[0] },
mailboxId, change,
ctx,
)
}
func (j *Client) CreateMailbox(accountId AccountId, mailbox MailboxChange, ctx Context) (Result[*Mailbox], error) {
return create(j, "CreateMailbox", MailboxType,
func(accountId AccountId, create map[string]MailboxChange) MailboxSetCommand {
return MailboxSetCommand{AccountId: accountId, Create: create}
},
func(accountId AccountId, ids string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId, Ids: []string{ids}}
},
func(resp MailboxSetResponse) map[string]*Mailbox {
return resp.Created
},
func(resp MailboxGetResponse) []Mailbox {
return resp.List
},
accountId, mailbox,
ctx,
)
}
func (j *Client) DeleteMailboxes(accountId AccountId, destroyIds []string, ctx Context) (Result[map[string]SetError], error) {
return destroy(j, "DeleteMailboxes", MailboxType,
func(accountId AccountId, destroy []string) MailboxSetCommand {
return MailboxSetCommand{AccountId: accountId, Destroy: destroyIds}
},
MailboxSetResponse{},
accountId, destroyIds,
ctx,
)
}
-174
View File
@@ -1,174 +0,0 @@
package jmap
import (
"github.com/opencloud-eu/opencloud/pkg/log"
)
var NS_OBJECTS = ns(JmapMail, JmapSubmission, JmapContacts, JmapCalendars, JmapQuota)
type Objects struct {
Mailboxes *MailboxGetResponse `json:"mailboxes,omitempty"`
Emails *EmailGetResponse `json:"emails,omitempty"`
Calendars *CalendarGetResponse `json:"calendars,omitempty"`
Events *CalendarEventGetResponse `json:"events,omitempty"`
Addressbooks *AddressBookGetResponse `json:"addressbooks,omitempty"`
Contacts *ContactCardGetResponse `json:"contacts,omitempty"`
Quotas *QuotaGetResponse `json:"quotas,omitempty"`
Identities *IdentityGetResponse `json:"identities,omitempty"`
EmailSubmissions *EmailSubmissionGetResponse `json:"submissions,omitempty"`
}
// Retrieve objects of all types by their identifiers in a single batch.
// @api:tags changes
func (j *Client) GetObjects(accountId AccountId, //NOSONAR
mailboxIds []string, emailIds []string,
addressbookIds []string, contactIds []string,
calendarIds []string, eventIds []string,
quotaIds []string, identityIds []string,
emailSubmissionIds []string,
ctx Context,
) (Result[Objects], error) {
l := j.logger("GetObjects", ctx).With()
if len(mailboxIds) > 0 {
l = l.Array("mailboxIds", log.SafeStringArray(mailboxIds))
}
if len(emailIds) > 0 {
l = l.Array("emailIds", log.SafeStringArray(emailIds))
}
if len(addressbookIds) > 0 {
l = l.Array("addressbookIds", log.SafeStringArray(addressbookIds))
}
if len(contactIds) > 0 {
l = l.Array("contactIds", log.SafeStringArray(contactIds))
}
if len(calendarIds) > 0 {
l = l.Array("calendarIds", log.SafeStringArray(calendarIds))
}
if len(eventIds) > 0 {
l = l.Array("eventIds", log.SafeStringArray(eventIds))
}
if len(quotaIds) > 0 {
l = l.Array("quotaIds", log.SafeStringArray(quotaIds))
}
if len(identityIds) > 0 {
l = l.Array("identityIds", log.SafeStringArray(identityIds))
}
if len(emailSubmissionIds) > 0 {
l = l.Array("emailSubmissionIds", log.SafeStringArray(emailSubmissionIds))
}
logger := log.From(l)
ctx = ctx.WithLogger(logger)
methodCalls := []Invocation{}
if len(mailboxIds) > 0 {
methodCalls = append(methodCalls, invocation(MailboxGetCommand{AccountId: accountId, Ids: mailboxIds}, "mailboxes"))
}
if len(emailIds) > 0 {
methodCalls = append(methodCalls, invocation(EmailGetCommand{AccountId: accountId, Ids: emailIds}, "emails"))
}
if len(addressbookIds) > 0 {
methodCalls = append(methodCalls, invocation(AddressBookGetCommand{AccountId: accountId, Ids: addressbookIds}, "addressbooks"))
}
if len(contactIds) > 0 {
methodCalls = append(methodCalls, invocation(ContactCardGetCommand{AccountId: accountId, Ids: contactIds}, "contacts"))
}
if len(calendarIds) > 0 {
methodCalls = append(methodCalls, invocation(CalendarGetCommand{AccountId: accountId, Ids: calendarIds}, "calendars"))
}
if len(eventIds) > 0 {
methodCalls = append(methodCalls, invocation(CalendarEventGetCommand{AccountId: accountId, Ids: eventIds}, "events"))
}
if len(quotaIds) > 0 {
methodCalls = append(methodCalls, invocation(QuotaGetCommand{AccountId: accountId, Ids: quotaIds}, "quotas"))
}
if len(identityIds) > 0 {
methodCalls = append(methodCalls, invocation(IdentityGetCommand{AccountId: accountId, Ids: identityIds}, "identities"))
}
if len(emailSubmissionIds) > 0 {
methodCalls = append(methodCalls, invocation(EmailSubmissionGetCommand{AccountId: accountId, Ids: emailSubmissionIds}, "emailSubmissionIds"))
}
cmd, err := j.request(ctx, NS_OBJECTS, methodCalls...)
if err != nil {
return ZeroResultV[Objects](), err
}
return command(j, Operation("GetObjects"), ctx, cmd, func(body *Response) (Objects, State, Error) {
objs := Objects{}
states := map[string]State{}
var mailboxes MailboxGetResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandMailboxGet, "mailboxes", &mailboxes); err != nil {
return Objects{}, "", err
} else if ok {
objs.Mailboxes = &mailboxes
states["mailbox"] = mailboxes.State
}
var emails EmailGetResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandEmailGet, "emails", &emails); err != nil {
return Objects{}, "", err
} else if ok {
objs.Emails = &emails
states["email"] = emails.State
}
var calendars CalendarGetResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandCalendarGet, "calendars", &calendars); err != nil {
return Objects{}, "", err
} else if ok {
objs.Calendars = &calendars
states["calendar"] = calendars.State
}
var events CalendarEventGetResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandCalendarEventGet, "events", &events); err != nil {
return Objects{}, "", err
} else if ok {
objs.Events = &events
states["event"] = events.State
}
var addressbooks AddressBookGetResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandAddressBookGet, "addressbooks", &addressbooks); err != nil {
return Objects{}, "", err
} else if ok {
objs.Addressbooks = &addressbooks
states["addressbook"] = addressbooks.State
}
var contacts ContactCardGetResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandContactCardGet, "contacts", &contacts); err != nil {
return Objects{}, "", err
} else if ok {
objs.Contacts = &contacts
states["contact"] = contacts.State
}
var quotas QuotaGetResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandQuotaGet, "quotas", &quotas); err != nil {
return Objects{}, "", err
} else if ok {
objs.Quotas = &quotas
states["quota"] = quotas.State
}
var identities IdentityGetResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandIdentityGet, "identities", &identities); err != nil {
return Objects{}, "", err
} else if ok {
objs.Identities = &identities
states["identity"] = identities.State
}
var submissions EmailSubmissionGetResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandEmailSubmissionGet, "submissions", &submissions); err != nil {
return Objects{}, "", err
} else if ok {
objs.EmailSubmissions = &submissions
states["submissions"] = submissions.State
}
return objs, squashKeyedStates(states), nil
})
}
-64
View File
@@ -1,64 +0,0 @@
package jmap
var NS_PRINCIPALS = ns(JmapPrincipals)
func (j *Client) GetPrincipals(accountId AccountId, ids []PrincipalId, ctx Context) (Result[PrincipalGetResponse], error) {
return get(j, "GetPrincipals", PrincipalType,
func(accountId AccountId, ids []PrincipalId) PrincipalGetCommand {
return PrincipalGetCommand{AccountId: accountId, Ids: ids}
},
PrincipalGetResponse{},
identity1,
accountId, ids,
ctx,
)
}
type PrincipalSearchResults SearchResultsTemplate[Principal]
var _ SearchResults[Principal] = &PrincipalSearchResults{}
func (r *PrincipalSearchResults) GetResults() []Principal { return r.Results }
func (r *PrincipalSearchResults) GetCanCalculateChanges() ChangeCalculation {
return r.CanCalculateChanges
}
func (r *PrincipalSearchResults) GetPosition() *uint { return r.Position }
func (r *PrincipalSearchResults) GetLimit() *uint { return r.Limit }
func (r *PrincipalSearchResults) GetTotal() *uint { return r.Total }
func (r *PrincipalSearchResults) RemoveResults() { r.Results = nil }
func (r *PrincipalSearchResults) SetLimit(limit *uint) { r.Limit = limit }
func (r *PrincipalSearchResults) SetPosition(position *uint) { r.Position = position }
func (j *Client) QueryPrincipals(accountIds map[AccountId]QueryParams, limit *uint, //NOSONAR
filter PrincipalFilterElement, sortBy []PrincipalComparator, calculateTotal bool,
ctx Context) (Result[map[AccountId]*PrincipalSearchResults], error) {
return queryN(j, "QueryPrincipals", PrincipalType,
[]PrincipalComparator{{Property: PrincipalPropertyName, IsAscending: true}},
func(accountId AccountId, p QueryParams, limit *uint, filter PrincipalFilterElement, sortBy []PrincipalComparator) PrincipalQueryCommand {
return PrincipalQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: p.Position, Anchor: p.Anchor, AnchorOffset: p.AnchorOffset, Limit: limit, CalculateTotal: calculateTotal}
},
func(accountId AccountId, cmd Command, path, rof string) PrincipalGetRefCommand {
return PrincipalGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
},
func(query PrincipalQueryResponse, queryParams QueryParams, limit *uint) *PrincipalSearchResults {
return &PrincipalSearchResults{
Results: []Principal{},
CanCalculateChanges: ChangeCalculation(query.CanCalculateChanges),
Position: valueIf(query.Position, queryParams.Anchor == ""),
Total: ptrIf(query.Total, calculateTotal),
Limit: valueIf(query.Limit, limit != nil),
}
},
func(query PrincipalQueryResponse, get PrincipalGetResponse, queryParams QueryParams, limit *uint) *PrincipalSearchResults {
return &PrincipalSearchResults{
Results: get.List,
CanCalculateChanges: ChangeCalculation(query.CanCalculateChanges),
Position: valueIf(query.Position, queryParams.Anchor == ""),
Total: ptrIf(query.Total, calculateTotal),
Limit: valueIf(query.Limit, limit != nil),
}
},
accountIds, limit, filter, sortBy,
ctx,
)
}
-96
View File
@@ -1,96 +0,0 @@
package jmap
var NS_QUOTA = ns(JmapQuota)
func (j *Client) GetQuotas(accountIds []AccountId, ctx Context) (Result[map[AccountId]QuotaGetResponse], error) {
return getN(j, "GetQuotas", QuotaType,
func(accountId AccountId, ids []string) QuotaGetCommand {
return QuotaGetCommand{AccountId: accountId}
},
QuotaGetResponse{},
identity1,
identity1,
accountIds, []string{},
ctx,
)
}
type QuotaChanges ChangesTemplate[Quota]
var _ Changes[Quota] = QuotaChanges{}
func (c QuotaChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c QuotaChanges) GetOldState() State { return c.OldState }
func (c QuotaChanges) GetNewState() State { return c.NewState }
func (c QuotaChanges) GetCreated() []Quota { return c.Created }
func (c QuotaChanges) GetUpdated() []Quota { return c.Updated }
func (c QuotaChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Quotas since a given State.
// @api:tags quota,changes
func (j *Client) GetQuotaChanges(accountId AccountId, sinceState State, maxChanges uint,
ctx Context) (Result[QuotaChanges], error) {
return changesA(j, "GetQuotaChanges", QuotaType,
func() QuotaChangesCommand {
return QuotaChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
QuotaChangesResponse{},
QuotaGetResponse{},
func(path string, rof string) QuotaGetRefCommand {
return QuotaGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandQuotaChanges,
Path: path,
ResultOf: rof,
},
}
},
func(oldState, newState State, hasMoreChanges bool, created, updated []Quota, destroyed []string) QuotaChanges {
return QuotaChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
ctx,
)
}
func (j *Client) GetQuotaUsageChanges(accountId AccountId, sinceState State, maxChanges uint,
ctx Context) (Result[QuotaChanges], error) {
return updates(j, "GetQuotaUsageChanges", QuotaType,
func() QuotaChangesCommand {
return QuotaChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
QuotaChangesResponse{},
func(path string, rof string) QuotaGetRefCommand {
return QuotaGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandQuotaChanges,
Path: path,
ResultOf: rof,
},
PropertiesRef: &ResultReference{
Name: CommandQuotaChanges,
Path: "/updatedProperties",
ResultOf: rof,
},
}
},
func(resp QuotaGetResponse) []Quota { return resp.List },
func(oldState, newState State, hasMoreChanges bool, updated []Quota) QuotaChanges {
return QuotaChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Updated: updated,
}
},
ctx,
)
}
-84
View File
@@ -1,84 +0,0 @@
package jmap
import (
"time"
)
var NS_VACATION = ns(JmapVacationResponse)
const (
vacationResponseId = "singleton"
)
func (j *Client) GetVacationResponse(accountId AccountId, ctx Context) (Result[VacationResponseGetResponse], error) {
return get(j, "GetVacationResponse", VacationResponseType,
func(accountId AccountId, ids []string) VacationResponseGetCommand {
return VacationResponseGetCommand{AccountId: accountId}
},
VacationResponseGetResponse{},
identity1,
accountId, []string{},
ctx,
)
}
// Same as VacationResponse but without the id.
type VacationResponseChange struct {
// Should a vacation response be sent if a message arrives between the "fromDate" and "toDate"?
IsEnabled *bool `json:"isEnabled,omitempty"`
// If "isEnabled" is true, messages that arrive on or after this date-time (but before the "toDate" if defined) should receive the
// user's vacation response. If null, the vacation response is effective immediately.
FromDate *time.Time `json:"fromDate,omitzero"`
// If "isEnabled" is true, messages that arrive before this date-time but on or after the "fromDate" if defined) should receive the
// user's vacation response. If null, the vacation response is effective indefinitely.
ToDate *time.Time `json:"toDate,omitzero"`
// The subject that will be used by the message sent in response to messages when the vacation response is enabled.
// If null, an appropriate subject SHOULD be set by the server.
Subject string `json:"subject,omitempty"`
// The plaintext body to send in response to messages when the vacation response is enabled.
// If this is null, the server SHOULD generate a plaintext body part from the "htmlBody" when sending vacation responses
// but MAY choose to send the response as HTML only. If both "textBody" and "htmlBody" are null, an appropriate default
// body SHOULD be generated for responses by the server.
TextBody string `json:"textBody,omitempty"`
// The HTML body to send in response to messages when the vacation response is enabled.
// If this is null, the server MAY choose to generate an HTML body part from the "textBody" when sending vacation responses
// or MAY choose to send the response as plaintext only.
HtmlBody string `json:"htmlBody,omitempty"`
}
var _ Change[VacationResponse] = VacationResponseChange{}
func (m VacationResponseChange) AsPatch() (PatchObject, error) {
return toPatchObject(m)
}
func (m VacationResponseChange) GetMarker() VacationResponse {
return VacationResponse{}
}
type VacationResponseChanges ChangesTemplate[VacationResponse]
var _ Changes[VacationResponse] = VacationResponseChanges{}
func (c VacationResponseChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c VacationResponseChanges) GetOldState() State { return c.OldState }
func (c VacationResponseChanges) GetNewState() State { return c.NewState }
func (c VacationResponseChanges) GetCreated() []VacationResponse { return c.Created }
func (c VacationResponseChanges) GetUpdated() []VacationResponse { return c.Updated }
func (c VacationResponseChanges) GetDestroyed() []string { return c.Destroyed }
func (j *Client) SetVacationResponse(accountId AccountId, change VacationResponseChange,
ctx Context) (Result[VacationResponse], error) {
return update(j, "SetVacationResponse", VacationResponseType,
func(update map[string]PatchObject) VacationResponseSetCommand {
return VacationResponseSetCommand{AccountId: accountId, Update: update}
},
func(_ string) VacationResponseGetCommand {
return VacationResponseGetCommand{AccountId: accountId}
},
func(resp VacationResponseSetResponse) map[string]SetError { return resp.NotUpdated },
func(resp VacationResponseGetResponse) VacationResponse { return resp.List[0] },
vacationResponseId, change,
ctx,
)
}
-11
View File
@@ -1,11 +0,0 @@
package jmap
import "context"
func (j *Client) EnablePushNotifications(ctx context.Context, pushState State, sessionProvider func() (*Session, error)) (WsClient, error) {
return j.ws.EnableNotifications(ctx, pushState, sessionProvider, j)
}
func (j *Client) AddWsPushListener(listener WsPushListener) {
j.wsPushListeners.add(listener)
}
-129
View File
@@ -1,129 +0,0 @@
package jmap
import (
"context"
"errors"
"io"
"net/url"
"slices"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/rs/zerolog"
)
type Client struct {
session SessionClient
api ApiClient
blob BlobClient
ws WsClientFactory
sessionEventListeners *eventListeners[SessionEventListener]
wsPushListeners *eventListeners[WsPushListener]
io.Closer
WsPushListener
}
type ApiSupplier interface {
Api() ApiClient
}
type Hooks interface {
OnSessionOutdated(session *Session, newState SessionState)
}
var _ io.Closer = &Client{}
var _ WsPushListener = &Client{}
var _ ApiSupplier = &Client{}
var _ Hooks = &Client{}
func (j *Client) Close() error {
return errors.Join(j.api.Close(), j.session.Close(), j.blob.Close(), j.ws.Close())
}
func (j *Client) Api() ApiClient {
return j.api
}
func NewClient(session SessionClient, api ApiClient, blob BlobClient, ws WsClientFactory) *Client {
return &Client{
session: session,
api: api,
blob: blob,
ws: ws,
sessionEventListeners: newEventListeners[SessionEventListener](),
wsPushListeners: newEventListeners[WsPushListener](),
}
}
func (j *Client) AddSessionEventListener(listener SessionEventListener) {
j.sessionEventListeners.add(listener)
}
func (j *Client) OnSessionOutdated(session *Session, newSessionState SessionState) {
j.sessionEventListeners.signal(func(listener SessionEventListener) {
listener.OnSessionOutdated(session, newSessionState)
})
}
func (j *Client) OnNotification(username string, stateChange StateChange) {
j.wsPushListeners.signal(func(listener WsPushListener) {
listener.OnNotification(username, stateChange)
})
}
// Retrieve JMAP well-known data from the Stalwart server and create a Session from that.
func (j *Client) FetchSession(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (Session, Error) {
sessionResponse, err := j.session.GetSession(ctx, sessionUrl, username, logger)
if err != nil {
return Session{}, err
}
return newSession(sessionResponse)
}
func (j *Client) logger(operation string, ctx Context) *log.Logger {
l := ctx.Logger.With().Str(logOperation, operation)
return log.From(l)
}
func (j *Client) loggerParams(operation string, ctx Context, params func(zerolog.Context) zerolog.Context) *log.Logger {
l := ctx.Logger.With().Str(logOperation, operation)
if params != nil {
l = params(l)
}
return log.From(l)
}
func (j *Client) maxCallsCheck(calls int, ctx Context) Error {
if calls > ctx.Session.Capabilities.Core.MaxCallsInRequest {
ctx.Logger.Error().
Int("max-calls-in-request", ctx.Session.Capabilities.Core.MaxCallsInRequest).
Int("calls-in-request", calls).
Msgf("number of calls in request payload (%d) exceeds the allowed maximum (%d)", ctx.Session.Capabilities.Core.MaxCallsInRequest, calls)
return jmapError(errTooManyMethodCalls, JmapErrorTooManyMethodCalls)
}
return nil
}
// Construct a Request from the given list of Invocation objects.
//
// If an issue occurs, then it is logged prior to returning it.
func (j *Client) request(ctx Context, using []JmapNamespace, methodCalls ...Invocation) (Request, Error) {
sanitized := structs.Filter(methodCalls, func(inv Invocation) bool { return inv.Command != "" })
err := j.maxCallsCheck(len(sanitized), ctx)
if err != nil {
return Request{}, err
}
if using == nil {
using = JmapNamespaces
}
if !slices.Contains(using, JmapCore) {
using = slices.Insert(using, 0, JmapCore)
}
return Request{
Using: using,
MethodCalls: sanitized,
CreatedIds: nil,
}, nil
}
-127
View File
@@ -1,127 +0,0 @@
package jmap
import (
"errors"
"fmt"
"net/http"
"strings"
)
const (
JmapErrorAuthenticationFailed = iota
JmapErrorInvalidHttpRequest
JmapErrorServerResponse
JmapErrorReadingResponseBody
JmapErrorDecodingResponseBody
JmapErrorEncodingRequestBody
JmapErrorCreatingRequest
JmapErrorSendingRequest
JmapErrorInvalidSessionResponse
JmapErrorInvalidJmapRequestPayload
JmapErrorInvalidJmapResponsePayload
JmapErrorSetError
JmapErrorTooManyMethodCalls
JmapErrorUnspecifiedType
JmapErrorServerUnavailable
JmapErrorServerFail
JmapErrorUnknownMethod
JmapErrorInvalidArguments
JmapErrorInvalidResultReference
JmapErrorForbidden
JmapErrorAccountNotFound
JmapErrorAccountNotSupportedByMethod
JmapErrorAccountReadOnly
JmapErrorFailedToEstablishWssConnection
JmapErrorWssConnectionResponseMissingJmapSubprotocol
JmapErrorWssFailedToSendWebSocketPushEnable
JmapErrorWssFailedToSendWebSocketPushDisable
JmapErrorWssFailedToClose
JmapErrorWssFailedToRetrieveSession
JmapErrorSocketPushUnsupported
JmapErrorMissingCreatedObject
JmapErrorInvalidObjectState
JmapErrorPatchObjectSerialization
JmapErrorInvalidProperties
JmapErrorRequestTracing
)
var (
errTooManyMethodCalls = errors.New("the amount of methodCalls in the request body would exceed the maximum that is configured in the session")
)
type Error interface {
Code() int
error
}
type JmapError struct {
code int
err error
typ string
description string
}
var _ Error = &JmapError{}
var _ error = &JmapError{}
var _ error = JmapError{}
func (e JmapError) Code() int {
return e.code
}
func (e JmapError) Unwrap() error {
return e.err
}
func (e JmapError) Error() string {
if e.err != nil {
return e.err.Error()
} else {
return ""
}
}
func (e JmapError) Type() string {
return e.typ
}
func (e JmapError) Description() string {
return e.description
}
func jmapErrorCode(statusCode int) int {
code := JmapErrorServerResponse
switch statusCode {
case http.StatusUnauthorized:
code = JmapErrorAuthenticationFailed
}
return code
}
func jmapError(err error, code int) Error {
if err != nil {
return JmapError{code: code, err: err}
} else {
return nil
}
}
func jmapResponseError(code int, err error, typ string, description string) JmapError {
return JmapError{
code: code,
err: err,
typ: typ,
description: description,
}
}
func setErrorError(err SetError, objectType ObjectType) Error {
var e error
if len(err.Properties) > 0 {
e = fmt.Errorf("failed to modify %s due to %s error in properties [%s]: %s", objectType, err.Type, strings.Join(err.Properties, ", "), err.Description)
} else {
e = fmt.Errorf("failed to modify %s due to %s error: %s", objectType, err.Type, err.Description)
}
code := JmapErrorSetError
switch err.Type {
case SetErrorTypeInvalidProperties:
code = JmapErrorInvalidProperties
}
return JmapError{code: code, err: e}
}
-81
View File
@@ -1,81 +0,0 @@
package jmap
// This is for functions that are only supposed to be visible in tests.
import (
"fmt"
"go/ast"
"go/token"
"iter"
"log"
"strings"
"golang.org/x/tools/go/packages"
)
func valuesOf(p *packages.Package) iter.Seq[*ast.ValueSpec] { //NOSONAR
return func(yield func(*ast.ValueSpec) bool) {
for _, syn := range p.Syntax {
for _, decl := range syn.Decls {
g, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
for _, s := range g.Specs {
e, ok := s.(*ast.ValueSpec)
if !ok {
continue
}
if !yield(e) {
return
}
}
}
}
}
}
func parseConsts(pkgID string, suffix string, typeName string) (map[string]string, error) { //NOSONAR
result := map[string]string{}
{
cfg := &packages.Config{
Mode: packages.LoadSyntax,
Dir: ".",
Tests: false,
}
pkgs, err := packages.Load(cfg, ".")
if err != nil {
log.Fatal(err)
}
if packages.PrintErrors(pkgs) > 0 {
return nil, fmt.Errorf("failed to parse the package '%s'", pkgID)
}
for _, p := range pkgs {
if p.ID != pkgID {
continue
}
for v := range valuesOf(p) {
for i, ident := range v.Names {
if ident != nil && strings.HasSuffix(ident.Name, suffix) {
value := v.Values[i]
switch c := value.(type) {
case *ast.CallExpr:
switch f := c.Fun.(type) {
case *ast.Ident:
if f.Name == typeName {
switch a := c.Args[0].(type) {
case *ast.BasicLit:
if a.Kind == token.STRING {
result[ident.Name] = strings.Trim(a.Value, `"`)
}
}
}
}
}
}
}
}
}
}
return result, nil
}
-802
View File
@@ -1,802 +0,0 @@
package jmap
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
ochttp "github.com/opencloud-eu/opencloud/pkg/http"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/version"
)
// Implementation of ApiClient, SessionClient and BlobClient that uses
// HTTP to perform JMAP operations.
type HttpJmapClient struct {
client *http.Client
userAgent string
authenticator HttpJmapClientAuthenticator
listener HttpJmapApiClientEventListener
traceRequests bool
traceMaxRequestBodySize int64
traceResponses bool
traceMaxResponseBodySize int64
}
var (
_ ApiClient = &HttpJmapClient{}
_ SessionClient = &HttpJmapClient{}
_ BlobClient = &HttpJmapClient{}
)
const (
logEndpoint = "endpoint"
logUri = "uri"
logMethod = "method"
logHttpStatus = "status"
logHttpStatusCode = "status-code"
logHttpUrl = "url"
logProto = "proto"
logProtoJmap = "jmap"
logProtoJmapWs = "jmapws"
logType = "type"
logTypeRequest = "request"
logTypeResponse = "response"
logTypePush = "push"
logDuration = "duration"
logBodyTruncated = "truncated"
logAuthenticatorId = "auth-id"
responseBodyPeekSize = 2 * 1024
)
// Record JMAP HTTP execution events that may occur, e.g. using metrics.
type HttpJmapApiClientEventListener interface {
OnSuccessfulRequest(endpoint string, op Operation, status int)
OnFailedRequest(endpoint string, op Operation, err error)
OnFailedRequestWithStatus(endpoint string, op Operation, status int)
OnResponseBodyReadingError(endpoint string, op Operation, err error)
OnResponseBodyUnmarshallingError(endpoint string, op Operation, err error)
OnSuccessfulWsRequest(endpoint string, op Operation, status int)
OnFailedWsHandshakeRequestWithStatus(endpoint string, op Operation, status int)
}
type nullHttpJmapApiClientEventListener struct {
}
func (l nullHttpJmapApiClientEventListener) OnSuccessfulRequest(endpoint string, op Operation, status int) {
// null implementation does nothing
}
func (l nullHttpJmapApiClientEventListener) OnFailedRequest(endpoint string, op Operation, err error) {
// null implementation does nothing
}
func (l nullHttpJmapApiClientEventListener) OnFailedRequestWithStatus(endpoint string, op Operation, status int) {
// null implementation does nothing
}
func (l nullHttpJmapApiClientEventListener) OnResponseBodyReadingError(endpoint string, op Operation, err error) {
// null implementation does nothing
}
func (l nullHttpJmapApiClientEventListener) OnResponseBodyUnmarshallingError(endpoint string, op Operation, err error) {
// null implementation does nothing
}
func (l nullHttpJmapApiClientEventListener) OnSuccessfulWsRequest(endpoint string, op Operation, status int) {
// null implementation does nothing
}
func (l nullHttpJmapApiClientEventListener) OnFailedWsHandshakeRequestWithStatus(endpoint string, op Operation, status int) {
// null implementation does nothing
}
var _ HttpJmapApiClientEventListener = nullHttpJmapApiClientEventListener{}
type HttpJmapClientAuthenticator interface {
GetId() string
Authenticate(ctx context.Context, username string, logger *log.Logger, req *http.Request) Error
AuthenticateWS(ctx context.Context, username string, logger *log.Logger, headers http.Header) Error
}
type MasterAuthHttpJmapClientAuthenticator struct {
masterUser string
masterPassword string
}
func NewMasterAuthHttpJmapClientAuthenticator(masterUser string, masterPassword string) HttpJmapClientAuthenticator {
return &MasterAuthHttpJmapClientAuthenticator{masterUser: masterUser, masterPassword: masterPassword}
}
var _ HttpJmapClientAuthenticator = &MasterAuthHttpJmapClientAuthenticator{}
func (h *MasterAuthHttpJmapClientAuthenticator) auth(username string, headers http.Header) {
// not nice to read, but heavily optimized to prevent needless memory allocations, since
// this hot path will be used all the time
var sb strings.Builder
sb.WriteString("Basic ")
enc := base64.NewEncoder(base64.StdEncoding, &sb)
enc.Write([]byte(username))
if username != h.masterUser {
enc.Write([]byte("%"))
enc.Write([]byte(h.masterUser))
}
enc.Write([]byte(":"))
enc.Write([]byte(h.masterPassword))
enc.Close()
headers.Set("Authorization", sb.String())
}
func (h *MasterAuthHttpJmapClientAuthenticator) GetId() string {
return "master"
}
func (h *MasterAuthHttpJmapClientAuthenticator) Authenticate(_ context.Context, username string, _ *log.Logger, req *http.Request) Error {
h.auth(username, req.Header)
return nil
}
func (h *MasterAuthHttpJmapClientAuthenticator) AuthenticateWS(_ context.Context, username string, _ *log.Logger, headers http.Header) Error {
h.auth(username, headers)
return nil
}
// An implementation of HttpJmapApiClientMetricsRecorder that does nothing.
func NewNullHttpJmapApiClientEventListener() HttpJmapApiClientEventListener {
return nullHttpJmapApiClientEventListener{}
}
func NewHttpJmapClient(client *http.Client, authenticator HttpJmapClientAuthenticator, listener HttpJmapApiClientEventListener,
traceRequests bool, traceMaxRequestBodySize int64,
traceResponses bool, traceMaxResponseBodySize int64,
) *HttpJmapClient {
return &HttpJmapClient{
client: client,
authenticator: authenticator,
userAgent: "OpenCloud/" + version.GetString(),
listener: listener,
traceRequests: traceRequests,
traceMaxRequestBodySize: traceMaxRequestBodySize,
traceResponses: traceResponses,
traceMaxResponseBodySize: traceMaxResponseBodySize,
}
}
func (h *HttpJmapClient) Close() error {
h.client.CloseIdleConnections()
return nil
}
type AuthenticationError struct {
Err error
}
func (e AuthenticationError) Error() string {
return fmt.Sprintf("failed to find user for authentication: %v", e.Err.Error())
}
func (e AuthenticationError) Unwrap() error {
return e.Err
}
func (h *HttpJmapClient) auth(ctx context.Context, username string, logger *log.Logger, req *http.Request) (string, Error) {
return h.authenticator.GetId(), h.authenticator.Authenticate(ctx, username, logger, req)
}
var (
errNilBaseUrl = errors.New("sessionUrl is nil")
)
func (h *HttpJmapClient) beforeRequest(_ context.Context, logger *log.Logger, _ string, endpoint string, req *http.Request) Error {
l := logger.Trace()
if h.traceRequests && l.Enabled() {
if err := ochttp.DumpHttpRequest(req, h.traceMaxRequestBodySize, func(method string, uri string, content string, truncated bool) {
if truncated {
l = l.Bool(logBodyTruncated, true)
}
l.Str(logMethod, req.Method).Str(logUri, req.URL.String()).
Str(logEndpoint, endpoint).Str(logProto, logProtoJmap).Str(logType, logTypeRequest).
Msg(content)
}); err != nil {
return jmapError(err, JmapErrorRequestTracing)
}
}
return nil
}
func (h *HttpJmapClient) response(peekSize int64, _ context.Context, logger *log.Logger, _ string, endpoint string, duration time.Duration, resp *http.Response) (io.ReadCloser, []byte, bool, error) {
whole := resp.Body
peek := []byte{}
truncated := false
var err error
if peekSize > 0 {
whole, peek, truncated, err = ochttp.PeekResponse(resp.Body, peekSize)
if err != nil {
return whole, peek, truncated, err
}
}
l := logger.Trace()
if h.traceResponses && l.Enabled() {
body, err := ochttp.DumpHttpResponse(resp, whole, h.traceMaxResponseBodySize, func(method, uri, content string, truncated bool) {
l.Str(logProto, logProtoJmap).Str(logType, logTypeResponse).Str(logEndpoint, endpoint).
Str(logMethod, method).Str(logUri, uri).
Str(logHttpStatus, log.SafeString(resp.Status)).Int(logHttpStatusCode, resp.StatusCode).
Dur(logDuration, duration).
Msg(content)
})
if err != nil {
return body, peek, truncated, err
} else {
return body, peek, truncated, nil
}
} else {
return whole, peek, truncated, nil
}
}
func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (SessionResponse, Error) {
if sessionUrl == nil {
logger.Error().Msg("sessionUrl is nil")
return SessionResponse{}, jmapError(errNilBaseUrl, JmapErrorInvalidHttpRequest)
}
// See the JMAP specification on Service Autodiscovery: https://jmap.io/spec-core.html#service-autodiscovery
// There are two standardised autodiscovery methods in use for Internet protocols:
// - DNS SRV (see [@!RFC2782], [@!RFC6186], and [@!RFC6764])
// - .well-known/servicename (see [@!RFC8615])
// We are currently only supporting RFC8615, using the baseurl that was configured in this HttpJmapApiClient.
//sessionUrl := baseurl.JoinPath(".well-known", "jmap")
sessionUrlStr := sessionUrl.String()
endpoint := endpointOf(sessionUrl)
logger = log.From(logger.With().Str(logEndpoint, endpoint))
req, err := http.NewRequest(http.MethodGet, sessionUrlStr, nil)
if err != nil {
logger.Error().Err(err).Msgf("failed to create GET request for %v", sessionUrl)
return SessionResponse{}, jmapError(err, JmapErrorInvalidHttpRequest)
}
req.Header.Add("Cache-Control", "no-cache, no-store, must-revalidate") // spec recommendation
req.Header.Add("Content-Type", "application/json") //NOSONAR
req.Header.Add("User-Agent", h.userAgent) //NOSONAR
if err := h.beforeRequest(ctx, logger, username, endpoint, req); err != nil {
return SessionResponse{}, err
}
if authenticatorId, err := h.auth(ctx, username, logger, req); err != nil {
return SessionResponse{}, err
} else {
logger = log.From(logger.With().Str(logAuthenticatorId, authenticatorId))
}
before := time.Now()
res, err := h.client.Do(req)
duration := time.Since(before)
if err != nil {
h.listener.OnFailedRequest(endpoint, Operation("GetSession"), err)
logger.Error().Err(err).Msgf("failed to perform GET %v", sessionUrl)
return SessionResponse{}, jmapError(err, JmapErrorInvalidHttpRequest)
}
// dump the response regardless of the status code
body, _, _, err := h.response(responseBodyPeekSize, ctx, logger, username, endpoint, duration, res)
// since we are not returning a stream from this function, we have to close the response body
// before leaving the scope of the function
defer func() {
if err := body.Close(); err != nil {
logger.Error().Err(err).Msg("failed to close response body") //NOSONAR
}
}()
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, Operation("GetSession"), res.StatusCode)
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 200")
return SessionResponse{}, jmapError(fmt.Errorf("JMAP API response status is %v", res.Status), jmapErrorCode(res.StatusCode))
}
h.listener.OnSuccessfulRequest(endpoint, Operation("GetSession"), res.StatusCode)
if err != nil {
logger.Error().Err(err).Msg("failed to read response body") //NOSONAR
h.listener.OnResponseBodyReadingError(endpoint, Operation("GetSession"), err)
return SessionResponse{}, jmapError(err, JmapErrorReadingResponseBody)
}
var data SessionResponse
if err := json.NewDecoder(body).Decode(&data); err != nil {
logger.Error().Str(logHttpUrl, log.SafeString(sessionUrlStr)).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
h.listener.OnResponseBodyUnmarshallingError(endpoint, Operation("GetSession"), err)
return SessionResponse{}, jmapError(err, JmapErrorDecodingResponseBody)
}
return data, nil
}
func (h *HttpJmapClient) Command(operation Operation, request Request, ctx Context) (io.ReadCloser, Language, Error) { //NOSONAR
session := ctx.Session
logger := ctx.Logger
acceptLanguage := ctx.AcceptLanguage
cotx := ctx.Context
jmapUrl := session.JmapUrl.String()
endpoint := session.JmapEndpoint
logger = log.From(logger.With().Str(logEndpoint, endpoint))
bodyBytes, err := json.Marshal(request)
if err != nil {
logger.Error().Err(err).Msg("failed to marshall JSON payload")
return nil, "", jmapError(err, JmapErrorEncodingRequestBody)
}
req, err := http.NewRequestWithContext(cotx, http.MethodPost, jmapUrl, bytes.NewBuffer(bodyBytes))
if err != nil {
logger.Error().Err(err).Msgf("failed to create POST request for %v", jmapUrl)
return nil, "", jmapError(err, JmapErrorCreatingRequest)
}
// Some JMAP APIs use the Accept-Language header to determine which language to use to translate
// texts in attributes.
if acceptLanguage != "" {
req.Header.Add("Accept-Language", acceptLanguage) //NOSONAR
}
req.Header.Add("Content-Type", "application/json") //NOSONAR
req.Header.Add("User-Agent", h.userAgent) //NOSONAR
h.beforeRequest(cotx, logger, session.Username, endpoint, req)
if authenticatorId, err := h.auth(cotx, session.Username, logger, req); err != nil {
return nil, "", err
} else {
logger = log.From(logger.With().Str(logAuthenticatorId, authenticatorId))
}
before := time.Now()
res, err := h.client.Do(req)
duration := time.Since(before)
if err != nil {
h.listener.OnFailedRequest(endpoint, operation, err)
logger.Error().Err(err).Msgf("failed to perform POST %v", jmapUrl)
return nil, "", jmapError(err, JmapErrorSendingRequest)
}
// note that we are omitting the usual deferred response body closer here since we are returning
// an io.ReadCloser to the body and if we close it when leaving the scope of this function, the
// caller won't be able to read the response; instead, it is up to the caller to close the
// ReadCloser we are returning from here
body, _, _, err := h.response(responseBodyPeekSize, cotx, logger, session.Username, endpoint, duration, res)
language := Language(res.Header.Get("Content-Language")) //NOSONAR
if err != nil {
logger.Error().Err(err).Msg("failed to read response body")
h.listener.OnResponseBodyReadingError(endpoint, operation, err)
return nil, language, jmapError(err, JmapErrorServerResponse)
}
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, operation, res.StatusCode)
logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, log.SafeString(res.Status)).Msg("HTTP response status code is not 2xx") //NOSONAR
return nil, language, jmapError(fmt.Errorf("JMAP server responsed with '%s'", res.Status), jmapErrorCode(res.StatusCode))
}
h.listener.OnSuccessfulRequest(endpoint, operation, res.StatusCode)
return body, language, nil
}
func (h *HttpJmapClient) UploadBinary(uploadUrl string, operation Operation, endpoint string, contentType string, body io.Reader, ctx Context) (UploadedBlob, Language, Error) { //NOSONAR
session := ctx.Session
logger := ctx.Logger
acceptLanguage := ctx.AcceptLanguage
cotx := ctx.Context
logger = log.From(logger.With().Str(logEndpoint, endpoint))
req, err := http.NewRequestWithContext(cotx, http.MethodPost, uploadUrl, body)
if err != nil {
logger.Error().Err(err).Msgf("failed to create POST request for %v", uploadUrl)
return UploadedBlob{}, "", jmapError(err, JmapErrorCreatingRequest)
}
req.Header.Add("Content-Type", contentType)
req.Header.Add("User-Agent", h.userAgent)
if acceptLanguage != "" {
req.Header.Add("Accept-Language", acceptLanguage)
}
h.beforeRequest(cotx, logger, session.Username, endpoint, req)
if authenticatorId, err := h.auth(cotx, session.Username, logger, req); err != nil {
return UploadedBlob{}, "", err
} else {
logger = log.From(logger.With().Str(logAuthenticatorId, authenticatorId))
}
before := time.Now()
res, err := h.client.Do(req)
duration := time.Since(before)
if err != nil {
h.listener.OnFailedRequest(endpoint, operation, err)
logger.Error().Err(err).Msgf("failed to perform POST %v", uploadUrl)
return UploadedBlob{}, "", jmapError(err, JmapErrorSendingRequest)
}
responseBody, _, _, err := h.response(responseBodyPeekSize, cotx, logger, session.Username, endpoint, duration, res)
// since we are not returning a stream from this function, we have to close the response body
// before leaving the scope of the function
defer func() {
if err := responseBody.Close(); err != nil {
logger.Error().Err(err).Msg("failed to close response body") //NOSONAR
}
}()
language := Language(res.Header.Get("Content-Language"))
if err != nil {
logger.Error().Err(err).Msg("failed to read response body")
h.listener.OnResponseBodyReadingError(endpoint, operation, err)
return UploadedBlob{}, language, jmapError(err, JmapErrorServerResponse)
}
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, operation, res.StatusCode)
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
return UploadedBlob{}, language, jmapError(err, jmapErrorCode(res.StatusCode))
}
h.listener.OnSuccessfulRequest(endpoint, operation, res.StatusCode)
var result UploadedBlob
if err := json.NewDecoder(responseBody).Decode(&result); err != nil {
logger.Error().Str(logHttpUrl, log.SafeString(uploadUrl)).Err(err).Msg("failed to decode JSON payload from the upload response")
h.listener.OnResponseBodyUnmarshallingError(endpoint, operation, err)
return UploadedBlob{}, language, jmapError(err, JmapErrorDecodingResponseBody)
}
return result, language, nil
}
func (h *HttpJmapClient) DownloadBinary(downloadUrl string, operation Operation, endpoint string, ctx Context) (*BlobDownload, Language, Error) { //NOSONAR
session := ctx.Session
logger := ctx.Logger
acceptLanguage := ctx.AcceptLanguage
cotx := ctx.Context
logger = log.From(logger.With().Str(logEndpoint, endpoint))
req, err := http.NewRequestWithContext(cotx, http.MethodGet, downloadUrl, nil)
if err != nil {
logger.Error().Err(err).Msgf("failed to create GET request for %v", downloadUrl)
return nil, "", jmapError(err, JmapErrorCreatingRequest)
}
req.Header.Add("User-Agent", h.userAgent)
if acceptLanguage != "" {
req.Header.Add("Accept-Language", acceptLanguage)
}
h.beforeRequest(cotx, logger, session.Username, endpoint, req)
if authenticatorId, err := h.auth(cotx, session.Username, logger, req); err != nil {
return nil, "", err
} else {
logger = log.From(logger.With().Str(logAuthenticatorId, authenticatorId))
}
before := time.Now()
res, err := h.client.Do(req)
duration := time.Since(before)
if err != nil {
h.listener.OnFailedRequest(endpoint, operation, err)
logger.Error().Err(err).Msgf("failed to perform GET %v", downloadUrl)
return nil, "", jmapError(err, JmapErrorSendingRequest)
}
// note that we are omitting the usual deferred response body closer here since we are returning
// an io.ReadCloser to the body and if we close it when leaving the scope of this function, the
// caller won't be able to read the response; instead, it is up to the caller to close the
// ReadCloser we are returning from here
responseBody, _, _, err := h.response(responseBodyPeekSize, cotx, logger, session.Username, endpoint, duration, res)
language := Language(res.Header.Get("Content-Language"))
if err != nil {
logger.Error().Err(err).Msg("failed to read response body")
h.listener.OnResponseBodyReadingError(endpoint, operation, err)
return nil, language, jmapError(err, JmapErrorServerResponse)
}
if res.StatusCode == http.StatusNotFound {
return nil, language, nil
}
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, operation, res.StatusCode)
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
return nil, language, jmapError(err, jmapErrorCode(res.StatusCode))
}
h.listener.OnSuccessfulRequest(endpoint, operation, res.StatusCode)
sizeStr := res.Header.Get("Content-Length")
size := -1
if sizeStr != "" {
size, err = strconv.Atoi(sizeStr)
if err != nil {
logger.Warn().Err(err).Msgf("failed to parse Content-Length blob download response header value '%v'", sizeStr)
size = -1
}
}
return &BlobDownload{
Body: responseBody,
Size: size,
Type: res.Header.Get("Content-Type"),
ContentDisposition: res.Header.Get("Content-Disposition"),
CacheControl: res.Header.Get("Cache-Control"),
}, language, nil
}
type WebSocketPushEnableType string
type WebSocketPushDisableType string
const (
WebSocketPushTypeEnable = WebSocketPushEnableType("WebSocketPushEnable")
WebSocketPushTypeDisable = WebSocketPushDisableType("WebSocketPushDisable")
)
type WebSocketPushEnable struct {
// This MUST be the string "WebSocketPushEnable".
Type WebSocketPushEnableType `json:"@type"`
// A list of data type names (e.g., "Mailbox" or "Email") that the client is interested in.
//
// A StateChange notification will only be sent if the data for one of these types changes.
// Other types are omitted from the TypeState object.
//
// If null, changes will be pushed for all supported data types.
DataTypes *[]string `json:"dataTypes"`
// The last "pushState" token that the client received from the server.
// Upon receipt of a "pushState" token, the server SHOULD immediately send all changes since that state token.
PushState State `json:"pushState,omitempty"`
}
type WebSocketPushDisable struct {
// This MUST be the string "WebSocketPushDisable".
Type WebSocketPushDisableType `json:"@type"`
}
type HttpWsClientFactory struct {
dialer *websocket.Dialer
authenticator HttpJmapClientAuthenticator
logger *log.Logger
eventListener HttpJmapApiClientEventListener
traceWsResponses bool
traceMaxResponseBodySize int64
}
var _ WsClientFactory = &HttpWsClientFactory{}
func NewHttpWsClientFactory(dialer *websocket.Dialer, authenticator HttpJmapClientAuthenticator, logger *log.Logger,
eventListener HttpJmapApiClientEventListener,
traceWsResponses bool, traceMaxResponseBodySize int64,
) (*HttpWsClientFactory, error) {
// RFC 8887: Section 4.2:
// Otherwise, the client MUST make an authenticated HTTP request [RFC7235] on the encrypted connection
// and MUST include the value "jmap" in the list of protocols for the "Sec-WebSocket-Protocol" header
// field.
dialer.Subprotocols = []string{"jmap"}
return &HttpWsClientFactory{
dialer: dialer,
authenticator: authenticator,
logger: logger,
eventListener: eventListener,
traceWsResponses: true,
traceMaxResponseBodySize: 4 * 1024,
}, nil
}
func (w *HttpWsClientFactory) auth(ctx context.Context, username string, logger *log.Logger, h http.Header) Error {
return w.authenticator.AuthenticateWS(ctx, username, logger, h)
}
func (w *HttpWsClientFactory) connect(operation Operation, ctx context.Context, sessionProvider func() (*Session, error)) (*websocket.Conn, string, string, Error) {
session, err := sessionProvider()
if err != nil {
return nil, "", "", jmapError(err, JmapErrorWssFailedToRetrieveSession)
}
if session == nil {
return nil, "", "", jmapError(fmt.Errorf("WSS connection failed to retrieve JMAP session"), JmapErrorWssFailedToRetrieveSession)
}
if !session.SupportsWebsocketPush {
return nil, "", "", jmapError(fmt.Errorf("WSS connection returned a session that does not support websocket push"), JmapErrorSocketPushUnsupported)
}
username := session.Username
u := session.WebsocketUrl
endpoint := session.WebsocketEndpoint
logger := log.From(w.logger.With().Str("username", log.SafeString(username)).Str("url", log.SafeString(u.String())))
h := http.Header{}
w.auth(ctx, username, logger, h)
w.logger.Trace().Str("username", log.SafeString(username)).Str("url", log.SafeString(u.String())).Msgf("connecting") // TODO more/better attributes here for WS connection attempts
before := time.Now()
c, res, err := w.dialer.DialContext(ctx, u.String(), h)
duration := time.Since(before)
if err != nil {
return nil, "", endpoint, jmapError(err, JmapErrorFailedToEstablishWssConnection)
}
var body io.ReadCloser = nil
{
l := logger.Trace()
if w.traceWsResponses && l.Enabled() {
body, err = ochttp.DumpHttpResponse(res, res.Body, w.traceMaxResponseBodySize, func(method, uri, content string, truncated bool) {
l.Str(logProto, logProtoJmap).Str(logType, logTypeResponse).Str(logEndpoint, endpoint).
Str(logMethod, method).Str(logUri, uri).
Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).
Dur(logDuration, duration).
Msg(content)
})
}
}
// we are not using the response body at all, at least for now
defer func() {
if body != nil {
if err := body.Close(); err != nil {
logger.Error().Err(err).Msg("failed to close response body") //NOSONAR
}
}
}()
if res.StatusCode != 101 {
w.eventListener.OnFailedRequestWithStatus(endpoint, operation, res.StatusCode)
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 101")
return nil, "", endpoint, jmapError(fmt.Errorf("JMAP WS API response status is %v", res.Status), jmapErrorCode(res.StatusCode))
} else {
w.eventListener.OnSuccessfulWsRequest(endpoint, operation, res.StatusCode)
}
// RFC 8887: Section 4.2:
// The reply from the server MUST also contain a corresponding "Sec-WebSocket-Protocol" header
// field with a value of "jmap" in order for a JMAP subprotocol connection to be established.
if !slices.Contains(res.Header.Values("Sec-WebSocket-Protocol"), "jmap") {
return nil, "", endpoint, jmapError(fmt.Errorf("WSS connection header does not contain Sec-WebSocket-Protocol:jmap"), JmapErrorWssConnectionResponseMissingJmapSubprotocol)
}
return c, username, endpoint, nil
}
type HttpWsClient struct {
client *HttpWsClientFactory
username string
sessionProvider func() (*Session, error)
c *websocket.Conn
logger *log.Logger
endpoint string
listener WsPushListener
WsClient
}
func (w *HttpWsClient) readPump() { //NOSONAR
logger := log.From(w.logger.With().Str("username", w.username))
defer func() {
if err := w.c.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
logger.Warn().Err(err).Msg("failed to close websocket connection")
}
}()
//w.c.SetReadLimit(maxMessageSize)
//c.conn.SetReadDeadline(time.Now().Add(pongWait))
//c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
if _, message, err := w.c.ReadMessage(); err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
logger.Error().Err(err).Msg("unexpected close")
}
break
} else {
if logger.Trace().Enabled() {
logger.Trace().Str(logEndpoint, w.endpoint).Str(logProto, logProtoJmapWs).Str(logType, logTypePush).Msg(string(message))
}
var peek struct {
Type string `json:"@type"`
}
if err := json.Unmarshal(message, &peek); err != nil {
logger.Error().Err(err).Msg("failed to deserialized pushed WS message")
continue
}
switch peek.Type {
case string(TypeOfStateChange):
var stateChange StateChange
if err := json.Unmarshal(message, &stateChange); err != nil {
logger.Error().Err(err).Msgf("failed to deserialized pushed WS message into a %T", stateChange)
continue
} else {
if w.listener != nil {
w.listener.OnNotification(w.username, stateChange)
} else {
logger.Warn().Msgf("no listener to be notified of %v", stateChange)
}
}
default:
logger.Warn().Msgf("unsupported pushed WS message JMAP @type: '%s'", peek.Type)
continue
}
}
}
}
func (w *HttpWsClientFactory) EnableNotifications(ctx context.Context, pushState State, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error) {
c, username, endpoint, jerr := w.connect(Operation("EnableNotifications"), ctx, sessionProvider)
if jerr != nil {
return nil, jerr
}
msg := WebSocketPushEnable{
Type: WebSocketPushTypeEnable,
DataTypes: nil, // = all datatypes
PushState: pushState, // will be omitted if empty string
}
data, err := json.Marshal(msg)
if err != nil {
return nil, jmapError(err, JmapErrorWssFailedToSendWebSocketPushEnable)
}
if w.logger.Trace().Enabled() {
w.logger.Trace().Str(logEndpoint, endpoint).Str(logProto, logProtoJmapWs).Str(logType, logTypeRequest).Msg(string(data))
}
if err := c.WriteMessage(websocket.TextMessage, data); err != nil {
return nil, jmapError(err, JmapErrorWssFailedToSendWebSocketPushEnable)
}
wsc := &HttpWsClient{
client: w,
username: username,
sessionProvider: sessionProvider,
c: c,
logger: w.logger,
endpoint: endpoint,
listener: listener,
}
go wsc.readPump()
return wsc, nil
}
func (w *HttpWsClientFactory) Close() error {
return nil
}
func (c *HttpWsClient) DisableNotifications() Error {
if c.c == nil {
return nil
}
werr := c.c.WriteJSON(WebSocketPushDisable{Type: WebSocketPushTypeDisable})
merr := c.c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
cerr := c.c.Close()
if werr != nil {
return jmapError(werr, JmapErrorWssFailedToClose)
}
if merr != nil {
return jmapError(merr, JmapErrorWssFailedToClose)
}
if cerr != nil {
return jmapError(cerr, JmapErrorWssFailedToClose)
}
return nil
}
func (c *HttpWsClient) Close() error {
return c.DisableNotifications()
}
Loaded 100 of 2610 files, more files were not shown because too many files have changed in this diff. Show more